From e11ff6a15eeaee7c855dabbdfffb2728763d18ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Apr 2021 20:31:08 +0200 Subject: [PATCH 001/140] Bumped version to 2021.5.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d05a7c03f4..761ccb92a8d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From ca1cf64cc25a097c119d1a95adce6fc6a55fc53c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 28 Apr 2021 22:18:00 +0200 Subject: [PATCH 002/140] 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 622db888eca11e58f7be43a26219667d06f4e888 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 28 Apr 2021 22:31:40 +0200 Subject: [PATCH 003/140] 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 2288d60ce4b0a48454dca89f81f29e15f9e51b48 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Apr 2021 13:34:19 -0600 Subject: [PATCH 004/140] 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 c93f3745100..2bff64dc8a6 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 04f1f90b03c..9e449cb8cae 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 92ce79675bb1e21885db94ef28e89f47a502971a 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/140] 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 c4a2dd5c3d91aea60f082d1ba2c253988b3b4c79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Apr 2021 22:43:40 +0200 Subject: [PATCH 006/140] 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 962ccc93eb1e6e77c76143fb21fbd5c907b3ed99 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Apr 2021 22:46:25 +0200 Subject: [PATCH 007/140] Bumped version to 2021.5.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 761ccb92a8d..68a3e2bde58 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From f009f251c90605462299df443acdade1e392582f Mon Sep 17 00:00:00 2001 From: karliemeads <68717336+karliemeads@users.noreply.github.com> Date: Thu, 29 Apr 2021 16:06:09 -0400 Subject: [PATCH 008/140] Apply default light profile only when light is toggled from off to on (#49376) Co-authored-by: Franck Nijhof --- homeassistant/components/light/__init__.py | 37 +++++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index cdb34910492..97aa2468145 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -205,6 +205,8 @@ LIGHT_TURN_ON_SCHEMA = { ATTR_EFFECT: cv.string, } +LIGHT_TURN_OFF_SCHEMA = {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH} + _LOGGER = logging.getLogger(__name__) @@ -295,8 +297,10 @@ async def async_setup(hass, config): # noqa: C901 preprocess_turn_on_alternatives(hass, params) - if ATTR_PROFILE not in params: - profiles.apply_default(light.entity_id, params) + if (not params or not light.is_on) or ( + params and ATTR_TRANSITION not in params + ): + profiles.apply_default(light.entity_id, light.is_on, params) supported_color_modes = light.supported_color_modes # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W @@ -366,17 +370,24 @@ async def async_setup(hass, config): # noqa: C901 if supported_color_modes: params.pop(ATTR_WHITE_VALUE, None) - # Zero brightness: Light will be turned off if params.get(ATTR_BRIGHTNESS) == 0: - await light.async_turn_off(**filter_turn_off_params(params)) + await async_handle_light_off_service(light, call) else: await light.async_turn_on(**params) + async def async_handle_light_off_service(light, call): + """Handle turning off a light.""" + params = dict(call.data["params"]) + + if ATTR_TRANSITION not in params: + profiles.apply_default(light.entity_id, True, params) + + await light.async_turn_off(**filter_turn_off_params(params)) + async def async_handle_toggle_service(light, call): """Handle toggling a light.""" if light.is_on: - off_params = filter_turn_off_params(call.data["params"]) - await light.async_turn_off(**off_params) + await async_handle_light_off_service(light, call) else: await async_handle_light_on_service(light, call) @@ -390,8 +401,8 @@ async def async_setup(hass, config): # noqa: C901 component.async_register_entity_service( SERVICE_TURN_OFF, - {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH}, - "async_turn_off", + vol.All(cv.make_entity_service_schema(LIGHT_TURN_OFF_SCHEMA), preprocess_data), + async_handle_light_off_service, ) component.async_register_entity_service( @@ -519,13 +530,15 @@ class Profiles: self.data = await self.hass.async_add_executor_job(self._load_profile_data) @callback - def apply_default(self, entity_id: str, params: dict) -> None: - """Return the default turn-on profile for the given light.""" + def apply_default(self, entity_id: str, state_on: bool, params: dict) -> None: + """Return the default profile for the given light.""" for _entity_id in (entity_id, "group.all_lights"): name = f"{_entity_id}.default" if name in self.data: - self.apply_profile(name, params) - return + if not state_on or not params: + self.apply_profile(name, params) + elif self.data[name].transition is not None: + params.setdefault(ATTR_TRANSITION, self.data[name].transition) @callback def apply_profile(self, name: str, params: dict) -> None: From ba7b05442ce5cfd3a51958848964e063591f0ca4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Apr 2021 09:25:34 -0700 Subject: [PATCH 009/140] Add auto_off to binary sensor template entity (#49615) --- .../components/template/binary_sensor.py | 73 +++++++++++++------ .../components/template/test_binary_sensor.py | 20 ++++- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 2e1d2f71590..4d316388eae 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from functools import partial import logging import voluptuous as vol @@ -49,6 +50,7 @@ from .trigger_entity import TriggerEntity CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" +CONF_AUTO_OFF = "auto_off" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" LEGACY_FIELDS = { @@ -74,6 +76,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template), } ) @@ -353,15 +356,13 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): """Initialize the entity.""" super().__init__(hass, coordinator, config) - if isinstance(config.get(CONF_DELAY_ON), template.Template): - self._to_render.append(CONF_DELAY_ON) - self._parse_result.add(CONF_DELAY_ON) - - if isinstance(config.get(CONF_DELAY_OFF), template.Template): - self._to_render.append(CONF_DELAY_OFF) - self._parse_result.add(CONF_DELAY_OFF) + for key in (CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF): + if isinstance(config.get(key), template.Template): + self._to_render.append(key) + self._parse_result.add(key) self._delay_cancel = None + self._auto_off_cancel = None self._state = False @property @@ -378,22 +379,23 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): self._delay_cancel() self._delay_cancel = None + if self._auto_off_cancel: + self._auto_off_cancel() + self._auto_off_cancel = None + if not self.available: + self.async_write_ha_state() return raw = self._rendered.get(CONF_STATE) state = template.result_as_boolean(raw) - if state == self._state: - return - key = CONF_DELAY_ON if state else CONF_DELAY_OFF delay = self._rendered.get(key) or self._config.get(key) # state without delay. None means rendering failed. - if state is None or delay is None: - self._state = state - self.async_write_ha_state() + if self._state == state or state is None or delay is None: + self._set_state(state) return if not isinstance(delay, timedelta): @@ -405,14 +407,43 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): ) return - @callback - def _set_state(_): - """Set state of template binary sensor.""" - self._state = state - self.async_set_context(self.coordinator.data["context"]) - self.async_write_ha_state() - # state with delay. Cancelled if new trigger received self._delay_cancel = async_call_later( - self.hass, delay.total_seconds(), _set_state + self.hass, delay.total_seconds(), partial(self._set_state, state) + ) + + @callback + def _set_state(self, state, _=None): + """Set up auto off.""" + self._state = state + self.async_set_context(self.coordinator.data["context"]) + self.async_write_ha_state() + + if not state: + return + + auto_off_time = self._rendered.get(CONF_AUTO_OFF) or self._config.get( + CONF_AUTO_OFF + ) + + if auto_off_time is None: + return + + if not isinstance(auto_off_time, timedelta): + try: + auto_off_time = cv.positive_time_period(auto_off_time) + except vol.Invalid as err: + logging.getLogger(__name__).warning( + "Error rendering %s template: %s", CONF_AUTO_OFF, err + ) + return + + @callback + def _auto_off(_): + """Set state of template binary sensor.""" + self._state = False + self.async_write_ha_state() + + self._auto_off_cancel = async_call_later( + self.hass, auto_off_time.total_seconds(), _auto_off ) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 70356405867..ccadef5aa96 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -965,7 +965,8 @@ async def test_trigger_entity(hass): "picture": "{{ '/local/dogs.png' }}", "icon": "{{ 'mdi:pirate' }}", "attributes": { - "plus_one": "{{ trigger.event.data.beer + 1 }}" + "plus_one": "{{ trigger.event.data.beer + 1 }}", + "another": "{{ trigger.event.data.uno_mas or 1 }}", }, } ], @@ -1021,8 +1022,16 @@ async def test_trigger_entity(hass): assert state.attributes.get("icon") == "mdi:pirate" assert state.attributes.get("entity_picture") == "/local/dogs.png" assert state.attributes.get("plus_one") == 3 + assert state.attributes.get("another") == 1 assert state.context is context + # Even if state itself didn't change, attributes might have changed + hass.bus.async_fire("test_event", {"beer": 2, "uno_mas": "si"}) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.via_list") + assert state.state == "on" + assert state.attributes.get("another") == "si" + async def test_template_with_trigger_templated_delay_on(hass): """Test binary sensor template with template delay on.""" @@ -1034,6 +1043,7 @@ async def test_template_with_trigger_templated_delay_on(hass): "state": "{{ trigger.event.data.beer == 2 }}", "device_class": "motion", "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', + "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', }, } } @@ -1054,3 +1064,11 @@ async def test_template_with_trigger_templated_delay_on(hass): state = hass.states.get("binary_sensor.test") assert state.state == "on" + + # Now wait for the auto-off + future = dt_util.utcnow() + timedelta(seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" From f459cef967960c7aeaa93218faad9a610068cfbf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 29 Apr 2021 15:59:17 +0200 Subject: [PATCH 010/140] Catch missing/unavailable response from modbus (#49632) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/modbus/modbus.py | 38 +++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index 05a752764c3..3439bc1f148 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/climate.py homeassistant/components/modbus/cover.py + homeassistant/components/modbus/modbus.py homeassistant/components/modbus/switch.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index ad53bd2aa53..f04c019e6a6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -5,6 +5,7 @@ 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 ( @@ -161,10 +162,11 @@ class ModbusHub: return self._config_name def _log_error(self, exception_error: ModbusException, error_state=True): + log_text = "Pymodbus: " + str(exception_error) if self._in_error: - _LOGGER.debug(str(exception_error)) + _LOGGER.debug(log_text) else: - _LOGGER.error(str(exception_error)) + _LOGGER.error(log_text) self._in_error = error_state def setup(self): @@ -236,6 +238,9 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(exception_error) return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None self._in_error = False return result @@ -248,6 +253,9 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(exception_error) return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None self._in_error = False return result @@ -260,6 +268,9 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(exception_error) return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None self._in_error = False return result @@ -272,6 +283,9 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(exception_error) return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None self._in_error = False return result @@ -280,10 +294,13 @@ class ModbusHub: with self._lock: kwargs = {"unit": unit} if unit else {} try: - self._client.write_coil(address, value, **kwargs) + 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)): + self._log_error(result) + return False self._in_error = False return True @@ -292,10 +309,13 @@ class ModbusHub: with self._lock: kwargs = {"unit": unit} if unit else {} try: - self._client.write_coils(address, values, **kwargs) + 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)): + self._log_error(result) + return False self._in_error = False return True @@ -304,10 +324,13 @@ class ModbusHub: with self._lock: kwargs = {"unit": unit} if unit else {} try: - self._client.write_register(address, value, **kwargs) + 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)): + self._log_error(result) + return False self._in_error = False return True @@ -316,9 +339,12 @@ class ModbusHub: with self._lock: kwargs = {"unit": unit} if unit else {} try: - self._client.write_registers(address, values, **kwargs) + 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)): + self._log_error(result) + return False self._in_error = False return True From 28fadad208fd03feef7f764eca28c69e50db565a Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Thu, 29 Apr 2021 20:10:36 +0200 Subject: [PATCH 011/140] Fix Fritz device tracker multiple routers (#49808) Co-authored-by: Paulus Schoutsen --- homeassistant/components/fritz/__init__.py | 13 +++++++++-- homeassistant/components/fritz/common.py | 8 +++++++ homeassistant/components/fritz/const.py | 1 + .../components/fritz/device_tracker.py | 22 ++++++++++++++----- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 507804bb857..afa3229c585 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -15,8 +15,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.typing import ConfigType -from .common import FritzBoxTools -from .const import DOMAIN, PLATFORMS +from .common import FritzBoxTools, FritzData +from .const import DATA_FRITZ, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -43,6 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = fritz_tools + if DATA_FRITZ not in hass.data: + hass.data[DATA_FRITZ] = FritzData() + @callback def _async_unload(event): fritz_tools.async_unload() @@ -61,6 +64,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] fritzbox.async_unload() + fritz_data = hass.data[DATA_FRITZ] + fritz_data.tracked.pop(fritzbox.unique_id) + + if not bool(fritz_data.tracked): + hass.data.pop(DATA_FRITZ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 45f211b352d..6a6f0b4a7d9 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -184,6 +184,14 @@ class FritzBoxTools: return dev_info +class FritzData: + """Storage class for platform global data.""" + + def __init__(self) -> None: + """Initialize the data.""" + self.tracked = {} + + class FritzDevice: """FritzScanner device.""" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 90b7d1554e7..1a3b176deb7 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -4,6 +4,7 @@ DOMAIN = "fritz" PLATFORMS = ["device_tracker"] +DATA_FRITZ = "fritz_data" DEFAULT_DEVICE_NAME = "Unknown device" DEFAULT_HOST = "192.168.178.1" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 42da58d7336..8ccce78964f 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -21,7 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType from .common import FritzBoxTools -from .const import DEFAULT_DEVICE_NAME, DOMAIN +from .const import DATA_FRITZ, DEFAULT_DEVICE_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -69,12 +69,12 @@ async def async_setup_entry( """Set up device tracker for FRITZ!Box component.""" _LOGGER.debug("Starting FRITZ!Box device tracker") router = hass.data[DOMAIN][entry.entry_id] - tracked = set() + data_fritz = hass.data[DATA_FRITZ] @callback def update_router(): """Update the values of the router.""" - _async_add_entities(router, async_add_entities, tracked) + _async_add_entities(router, async_add_entities, data_fritz) async_dispatcher_connect(hass, router.signal_device_new, update_router) @@ -82,16 +82,26 @@ async def async_setup_entry( @callback -def _async_add_entities(router, async_add_entities, tracked): +def _async_add_entities(router, async_add_entities, data_fritz): """Add new tracker entities from the router.""" + + def _is_tracked(mac, device): + for tracked in data_fritz.tracked.values(): + if mac in tracked: + return True + + return False + new_tracked = [] + if router.unique_id not in data_fritz.tracked: + data_fritz.tracked[router.unique_id] = set() for mac, device in router.devices.items(): - if mac in tracked: + if device.ip_address == "" or _is_tracked(mac, device): continue new_tracked.append(FritzBoxTracker(router, device)) - tracked.add(mac) + data_fritz.tracked[router.unique_id].add(mac) if new_tracked: async_add_entities(new_tracked) From 44aaa0499a103c52beda58172ff2a23104cad57b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Apr 2021 09:49:16 -0400 Subject: [PATCH 012/140] Improve Waze Travel Time import and naming logic (#49838) --- .../waze_travel_time/config_flow.py | 46 +++++++++---------- .../components/waze_travel_time/sensor.py | 6 +-- .../components/waze_travel_time/strings.json | 1 + .../waze_travel_time/test_config_flow.py | 5 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index 05dd372f9d9..4100284273e 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -103,31 +103,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} - if user_input is not None: - if await self.hass.async_add_executor_job( - is_valid_config_entry, - self.hass, - _LOGGER, - user_input[CONF_ORIGIN], - user_input[CONF_DESTINATION], - user_input[CONF_REGION], - ): - await self.async_set_unique_id( - slugify( - f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" - ) + 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() + ) + self._abort_if_unique_id_configured() + if ( + self.source == config_entries.SOURCE_IMPORT + or await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + user_input[CONF_REGION], + ) + ): return self.async_create_entry( - title=( - user_input.get( - CONF_NAME, - ( - f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> " - f"{user_input[CONF_DESTINATION]}" - ), - ) - ), + title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input, ) @@ -138,6 +135,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): cv.string, vol.Required(CONF_ORIGIN): cv.string, vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_REGION): vol.In(REGIONS), diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 8e7ca04fec6..02446849e98 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -43,7 +43,6 @@ from .const import ( DEFAULT_AVOID_FERRIES, DEFAULT_AVOID_SUBSCRIPTION_ROADS, DEFAULT_AVOID_TOLL_ROADS, - DEFAULT_NAME, DEFAULT_REALTIME, DEFAULT_VEHICLE_TYPE, DOMAIN, @@ -117,10 +116,9 @@ async def async_setup_entry( CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS, CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS, } - name = None + if not config_entry.options: new_data = config_entry.data.copy() - name = new_data.pop(CONF_NAME, None) options = {} for key in [ CONF_INCL_FILTER, @@ -144,7 +142,7 @@ async def async_setup_entry( destination = config_entry.data[CONF_DESTINATION] origin = config_entry.data[CONF_ORIGIN] region = config_entry.data[CONF_REGION] - name = name or f"{DEFAULT_NAME}: {origin} -> {destination}" + name = config_entry.data[CONF_NAME] if not await hass.async_add_executor_job( is_valid_config_entry, hass, _LOGGER, origin, destination, region diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index 082ee31db73..0417962ccbf 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -5,6 +5,7 @@ "user": { "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name.", "data": { + "name": "[%key:common::config_flow::data::name%]", "origin": "Origin", "destination": "Destination", "region": "Region" diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index f6f1614ca25..b6690be4d86 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.waze_travel_time.const import ( DEFAULT_NAME, DOMAIN, ) -from homeassistant.const import CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL +from homeassistant.const import CONF_NAME, CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL from tests.common import MockConfigEntry @@ -38,8 +38,9 @@ async def test_minimum_fields(hass, validate_config_entry, bypass_setup): await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2" + assert result2["title"] == DEFAULT_NAME assert result2["data"] == { + CONF_NAME: DEFAULT_NAME, CONF_ORIGIN: "location1", CONF_DESTINATION: "location2", CONF_REGION: "US", From 5c39fd586378357b6ce3e9e16a7fff3f19e40a5f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Apr 2021 11:10:03 -0400 Subject: [PATCH 013/140] Improve Google Travel Time import and naming logic (#49839) --- .../google_travel_time/config_flow.py | 43 ++++++++++--------- .../components/google_travel_time/sensor.py | 5 +-- .../google_travel_time/strings.json | 1 + .../google_travel_time/test_config_flow.py | 4 +- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 5c66220af02..899ce804796 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -122,29 +122,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} - if user_input is not None: - if await self.hass.async_add_executor_job( - is_valid_config_entry, - self.hass, - _LOGGER, - user_input[CONF_API_KEY], - user_input[CONF_ORIGIN], - user_input[CONF_DESTINATION], - ): - await self.async_set_unique_id( - slugify( - f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" - ) + 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() + ) + self._abort_if_unique_id_configured() + if ( + self.source == config_entries.SOURCE_IMPORT + or await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_API_KEY], + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + ) + ): return self.async_create_entry( - title=user_input.get( - CONF_NAME, - ( - f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> " - f"{user_input[CONF_DESTINATION]}" - ), - ), + title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input, ) @@ -155,6 +153,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_ORIGIN): cv.string, diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 3980d0323b2..92a359a5780 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -40,7 +40,6 @@ from .const import ( CONF_TRANSIT_ROUTING_PREFERENCE, CONF_TRAVEL_MODE, CONF_UNITS, - DEFAULT_NAME, DOMAIN, TRACKABLE_DOMAINS, TRANSIT_PREFS, @@ -100,11 +99,9 @@ async def async_setup_entry( async_add_entities: Callable[[list[SensorEntity], bool], None], ) -> None: """Set up a Google travel time sensor entry.""" - name = None if not config_entry.options: new_data = config_entry.data.copy() options = new_data.pop(CONF_OPTIONS, {}) - name = new_data.pop(CONF_NAME, None) if CONF_UNITS not in options: options[CONF_UNITS] = hass.config.units.name @@ -129,7 +126,7 @@ async def async_setup_entry( api_key = config_entry.data[CONF_API_KEY] origin = config_entry.data[CONF_ORIGIN] destination = config_entry.data[CONF_DESTINATION] - name = name or f"{DEFAULT_NAME}: {origin} -> {destination}" + name = config_entry.data[CONF_NAME] if not await hass.async_add_executor_job( is_valid_config_entry, hass, _LOGGER, api_key, origin, destination diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 8dcc8f2fa1b..769c9a4dac7 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -5,6 +5,7 @@ "user": { "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.", "data": { + "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", "origin": "Origin", "destination": "Destination" diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 64dc77903ff..a0767246087 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -47,8 +47,9 @@ async def test_minimum_fields(hass, validate_config_entry, bypass_setup): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2" + assert result2["title"] == DEFAULT_NAME assert result2["data"] == { + CONF_NAME: DEFAULT_NAME, CONF_API_KEY: "api_key", CONF_ORIGIN: "location1", CONF_DESTINATION: "location2", @@ -281,6 +282,7 @@ async def test_import_flow(hass, validate_config_entry, bypass_update): entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { + CONF_NAME: "test_name", CONF_API_KEY: "api_key", CONF_ORIGIN: "location1", CONF_DESTINATION: "location2", From 735bd121e1310333902072806151a6ef7698258e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Apr 2021 11:43:23 +0200 Subject: [PATCH 014/140] 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 560a7a3ed37142cd0a38c020b2370e6a679ad560 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 29 Apr 2021 12:40:51 +0100 Subject: [PATCH 015/140] Rename FlowResultDict to FlowResult (#49847) --- homeassistant/auth/__init__.py | 6 +-- homeassistant/auth/mfa_modules/__init__.py | 4 +- homeassistant/auth/mfa_modules/notify.py | 6 +-- homeassistant/auth/mfa_modules/totp.py | 4 +- homeassistant/auth/providers/__init__.py | 10 ++--- homeassistant/auth/providers/command_line.py | 4 +- homeassistant/auth/providers/homeassistant.py | 4 +- .../auth/providers/insecure_example.py | 4 +- .../auth/providers/legacy_api_password.py | 4 +- .../auth/providers/trusted_networks.py | 4 +- .../components/adguard/config_flow.py | 12 +++--- homeassistant/components/bond/config_flow.py | 8 ++-- .../components/bsblan/config_flow.py | 8 ++-- .../components/canary/config_flow.py | 8 ++-- .../components/climacell/config_flow.py | 10 ++--- .../components/coronavirus/config_flow.py | 4 +- .../components/denonavr/config_flow.py | 10 ++--- .../components/directv/config_flow.py | 14 +++---- .../components/elgato/config_flow.py | 14 +++---- .../components/enphase_envoy/config_flow.py | 4 +- .../homematicip_cloud/config_flow.py | 10 ++--- .../components/huawei_lte/config_flow.py | 14 +++---- homeassistant/components/hue/config_flow.py | 6 +-- .../components/hyperion/config_flow.py | 24 ++++++------ homeassistant/components/ipp/config_flow.py | 12 +++--- .../components/litejet/config_flow.py | 4 +- homeassistant/components/met/config_flow.py | 4 +- .../components/mutesync/config_flow.py | 4 +- .../components/mysensors/config_flow.py | 4 +- .../components/nzbget/config_flow.py | 8 ++-- .../components/plum_lightpad/config_flow.py | 10 ++--- .../rituals_perfume_genie/config_flow.py | 4 +- homeassistant/components/roku/config_flow.py | 12 +++--- .../components/rpi_power/config_flow.py | 4 +- .../components/sentry/config_flow.py | 6 +-- homeassistant/components/sma/config_flow.py | 6 +-- .../components/solaredge/config_flow.py | 6 +-- .../components/sonarr/config_flow.py | 14 +++---- .../components/spotify/config_flow.py | 8 ++-- homeassistant/components/toon/config_flow.py | 10 ++--- .../components/twentemilieu/config_flow.py | 6 +-- .../components/verisure/config_flow.py | 14 +++---- homeassistant/components/vizio/config_flow.py | 26 +++++-------- homeassistant/components/wled/config_flow.py | 16 ++++---- .../components/zwave_js/config_flow.py | 26 ++++++------- homeassistant/config_entries.py | 34 ++++++++--------- homeassistant/data_entry_flow.py | 38 +++++++++---------- homeassistant/helpers/config_entry_flow.py | 12 +++--- .../helpers/config_entry_oauth2_flow.py | 10 ++--- homeassistant/helpers/data_entry_flow.py | 4 +- 50 files changed, 229 insertions(+), 269 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 89a05e20eb2..14981d0df09 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -11,7 +11,7 @@ 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 FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.util import dt as dt_util from . import auth_store, models @@ -98,8 +98,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): return await auth_provider.async_login_flow(context) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: FlowResultDict - ) -> FlowResultDict: + self, flow: data_entry_flow.FlowHandler, result: FlowResult + ) -> FlowResult: """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 80e0a0d834a..4adaf4776a0 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -12,7 +12,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry @@ -106,7 +106,7 @@ class SetupFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index c590b6195e4..31210e2d39a 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv @@ -293,7 +293,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Let user select available notify services.""" errors: dict[str, str] = {} @@ -319,7 +319,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_setup( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Verify user can receive one-time password.""" errors: dict[str, str] = {} diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index cb9ff95f808..20030ae166b 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from . import ( MULTI_FACTOR_AUTH_MODULE_SCHEMA, @@ -190,7 +190,7 @@ class TotpSetupFlow(SetupFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index cdd5029f1d9..d2dfa0e1c6d 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -13,7 +13,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry @@ -200,7 +200,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -210,7 +210,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_select_mfa_module( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of select mfa module.""" errors = {} @@ -235,7 +235,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_mfa( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of mfa validation.""" assert self.credential assert self.user @@ -287,6 +287,6 @@ class LoginFlow(data_entry_flow.FlowHandler): errors=errors, ) - async def async_finish(self, flow_result: Any) -> FlowResultDict: + async def async_finish(self, flow_result: Any) -> FlowResult: """Handle the pass of login flow.""" return self.async_create_entry(title=self._auth_provider.name, data=flow_result) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 9413072fd4b..65d553d4eb2 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -11,7 +11,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_COMMAND -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -129,7 +129,7 @@ class CommandLineLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 7544ae9aa14..dfbf077a89d 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -321,7 +321,7 @@ class HassLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index ac6171a346c..5a3a890ff66 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -9,7 +9,7 @@ from typing import cast import voluptuous as vol from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -99,7 +99,7 @@ class ExampleLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 5ffb59638db..b385aa0ed59 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -12,7 +12,7 @@ from typing import cast import voluptuous as vol from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -84,7 +84,7 @@ class LegacyLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index a6a5cfb94f0..2f120e56652 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -19,7 +19,7 @@ from typing import Any, Dict, List, Union, cast import voluptuous as vol from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -201,7 +201,7 @@ class TrustedNetworksLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the step of the form.""" try: cast( diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index f209e8c21b6..d8d0b5db6a8 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -32,7 +32,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -51,7 +51,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_hassio_form( self, errors: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Show the Hass.io confirmation form to the user.""" return self.async_show_form( step_id="hassio_confirm", @@ -62,7 +62,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) @@ -107,7 +107,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: """Prepare configuration for a Hass.io AdGuard Home add-on. This flow is triggered by the discovery component. @@ -119,7 +119,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Confirm Supervisor discovery.""" if user_input is None: return await self._show_hassio_form() diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 6829cfd4cc6..f8af7b70c92 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -94,7 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: DiscoveryInfoType - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info[CONF_NAME] host: str = discovery_info[CONF_HOST] @@ -118,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: @@ -159,7 +159,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index 9ccad6089b2..0e08c703632 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -23,9 +23,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -57,7 +55,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index f01c558024e..fbe573ccaae 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import ( @@ -53,13 +53,11 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index 5c5bb86a479..e24a720b199 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -88,9 +88,7 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): """Initialize ClimaCell options flow.""" self._config_entry = config_entry - async def async_step_init( - self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: """Manage the ClimaCell options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -121,9 +119,7 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return ClimaCellOptionsConfigFlow(config_entry) - async def async_step_user( - self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 85027b35d92..79152c07861 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from . import get_coordinator from .const import DOMAIN, OPTION_WORLDWIDE @@ -22,7 +22,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 190c5e55af9..bb06b4aeb66 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -13,7 +13,7 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from .receiver import ConnectDenonAVR @@ -135,7 +135,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle multiple receivers found.""" errors = {} if user_input is not None: @@ -156,7 +156,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_connect() @@ -166,7 +166,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Connect to the receiver.""" connect_denonavr = ConnectDenonAVR( self.host, @@ -215,7 +215,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 78e44f2f1ee..c0dbaea54ce 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -46,9 +46,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): """Set up the instance.""" self.discovery_info = {} - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -68,9 +66,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_ssdp( - self, discovery_info: DiscoveryInfoType - ) -> FlowResultDict: + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle SSDP discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname receiver_id = None @@ -103,7 +99,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: ConfigType = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: return self.async_show_form( @@ -117,7 +113,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 2f3e25d4fb5..3b943196573 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SERIAL_NUMBER, DOMAIN @@ -27,7 +27,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._async_show_setup_form() @@ -42,9 +42,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_zeroconf( - self, discovery_info: dict[str, Any] - ) -> FlowResultDict: + async def async_step_zeroconf(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] self.port = discovery_info[CONF_PORT] @@ -62,14 +60,14 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, _: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by zeroconf.""" return self._async_create_entry() @callback def _async_show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -83,7 +81,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry(self) -> FlowResultDict: + def _async_create_entry(self) -> FlowResult: return self.async_create_entry( title=self.serial_number, data={ diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 5f8fdcfd0e7..9fe7f74cc47 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client @@ -132,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 4e18e4fdb6b..de7d2ff3db6 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from .const import ( _LOGGER, @@ -28,11 +28,11 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): """Initialize HomematicIP Cloud config flow.""" self.auth = None - async def async_step_user(self, user_input=None) -> FlowResultDict: + async def async_step_user(self, user_input=None) -> FlowResult: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None) -> FlowResultDict: + async def async_step_init(self, user_input=None) -> FlowResult: """Handle a flow start.""" errors = {} @@ -63,7 +63,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_link(self, user_input=None) -> FlowResultDict: + async def async_step_link(self, user_input=None) -> FlowResult: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -85,7 +85,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info) -> FlowResultDict: + async def async_step_import(self, import_info) -> FlowResult: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID].replace("-", "").upper() authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index beeeab6e0bb..0071182a6e4 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -29,7 +29,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( @@ -62,7 +62,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResultDict: + ) -> FlowResult: if user_input is None: user_input = {} return self.async_show_form( @@ -89,7 +89,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle import initiated config flow.""" return await self.async_step_user(user_input) @@ -103,7 +103,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( # noqa: C901 self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle user initiated config flow.""" if user_input is None: return await self._async_show_user_form() @@ -215,9 +215,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp( - self, discovery_info: DiscoveryInfoType - ) -> FlowResultDict: + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle SSDP initiated config flow.""" await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() @@ -258,7 +256,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle options flow.""" # Recipients are persisted as a list, but handled as comma separated string in UI diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 1864d724b8c..f2960a0e99a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries, core from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge @@ -118,7 +118,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle manual bridge setup.""" if user_input is None: return self.async_show_form( @@ -253,7 +253,7 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 229859111ac..054425c7d3e 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -131,7 +131,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def _advance_to_auth_step_if_necessary( self, hyperion_client: client.HyperionClient - ) -> FlowResultDict: + ) -> FlowResult: """Determine if auth is required.""" auth_resp = await hyperion_client.async_is_auth_required() @@ -146,7 +146,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, config_data: ConfigType, - ) -> FlowResultDict: + ) -> FlowResult: """Handle a reauthentication flow.""" self._data = dict(config_data) async with self._create_client(raw_connection=True) as hyperion_client: @@ -154,7 +154,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', @@ -225,7 +225,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: ConfigType | None = None, - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input: @@ -296,7 +296,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: ConfigType | None = None, - ) -> FlowResultDict: + ) -> FlowResult: """Handle the auth step of a flow.""" errors = {} if user_input: @@ -325,7 +325,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token( self, user_input: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Send a request for a new token.""" if user_input is None: self._auth_id = client.generate_random_auth_id() @@ -351,7 +351,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_external( self, auth_resp: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle completion of the request for a new token.""" if auth_resp is not None and client.ResponseOK(auth_resp): token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN) @@ -364,7 +364,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_success( self, _: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Create an entry after successful token creation.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -380,7 +380,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_fail( self, _: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Show an error on the auth form.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -388,7 +388,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: return self.async_show_form( @@ -448,7 +448,7 @@ class HyperionOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Manage the options.""" effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES} diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 6f2d036600f..10a5a89ccdd 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -63,9 +63,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): """Set up the instance.""" self.discovery_info = {} - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -101,7 +99,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_zeroconf(self, discovery_info: ConfigType) -> FlowResultDict: + async def async_step_zeroconf(self, discovery_info: ConfigType) -> FlowResult: """Handle zeroconf discovery.""" port = discovery_info[CONF_PORT] zctype = discovery_info["type"] @@ -169,7 +167,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -183,7 +181,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index d453d6a90aa..a4de9d883e4 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -22,7 +22,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Create a LiteJet config entry based upon user input.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 895a6e33d2d..28c94139031 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -80,7 +80,7 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import(self, user_input: dict | None = None) -> FlowResultDict: + async def async_step_import(self, user_input: dict | None = None) -> FlowResult: """Handle configuration by yaml file.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py index 94d9b53a9d6..72002d072f3 100644 --- a/homeassistant/components/mutesync/config_flow.py +++ b/homeassistant/components/mutesync/config_flow.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -46,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 5299b76d2f5..59dff4829de 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -27,7 +27,7 @@ from homeassistant.components.mysensors import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -282,7 +282,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_create_entry( self, user_input: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index c0feffd9cef..22c940f6fda 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import ( @@ -67,7 +67,7 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by configuration file.""" if CONF_SCAN_INTERVAL in user_input: user_input[CONF_SCAN_INTERVAL] = user_input[ @@ -76,9 +76,7 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index f5ff0f2e06d..64c424ae74b 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -35,9 +35,7 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() @@ -59,8 +57,6 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password} ) - async def async_step_import( - self, import_config: ConfigType | None - ) -> FlowResultDict: + async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 4c46cf09d55..86ef2d915f2 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACCOUNT_HASH, DOMAIN @@ -28,7 +28,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input=None) -> FlowResultDict: + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index ae79c214d3f..c39397ce8bc 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.ssdp import ( from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -54,7 +54,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = {} @callback - def _show_form(self, errors: dict | None = None) -> FlowResultDict: + def _show_form(self, errors: dict | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -62,7 +62,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user(self, user_input: dict | None = None) -> FlowResultDict: + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() @@ -113,9 +113,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_ssdp( - self, discovery_info: dict | None = None - ) -> FlowResultDict: + async def async_step_ssdp(self, discovery_info: dict | None = None) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] @@ -141,7 +139,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle user-confirmation of discovered device.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index 01e789f23b7..1994aeb7b97 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -7,7 +7,7 @@ from rpi_bad_power import new_under_voltage from homeassistant import config_entries from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -35,7 +35,7 @@ class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN): async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initialized by onboarding.""" has_devices = await self._discovery_function(self.hass) diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index 115a4747a7a..0fb65badd0f 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_DSN, @@ -49,7 +49,7 @@ class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a user config flow.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -80,7 +80,7 @@ class SentryOptionsFlow(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Manage Sentry options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 95be954dba5..b7fb1066e65 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -69,7 +69,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """First step in config flow.""" errors = {} if user_input is not None: @@ -118,7 +118,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, import_config: dict[str, Any] | None - ) -> FlowResultDict: + ) -> FlowResult: """Import a config flow from configuration.""" device_info = await validate_input(self.hass, import_config) import_config[DEVICE_INFO] = device_info diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 07f987fb009..4f2a7207d67 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @@ -57,7 +57,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -93,7 +93,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Import a config entry.""" if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): return self.async_abort(reason="already_configured") diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index 7b1d991c871..b68cc33712a 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -76,7 +76,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth(self, data: ConfigType | None = None) -> FlowResultDict: + async def async_step_reauth(self, data: ConfigType | None = None) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True self._entry_data = dict(data) @@ -87,7 +87,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: ConfigType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form( @@ -99,9 +99,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -138,9 +136,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_reauth_update_entry( - self, entry_id: str, data: dict - ) -> FlowResultDict: + async def _async_reauth_update_entry(self, entry_id: str, data: dict) -> FlowResult: """Update existing config entry.""" entry = self.hass.config_entries.async_get_entry(entry_id) self.hass.config_entries.async_update_entry(entry, data=data) diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index 0fb23f7c56f..3b7724a21a9 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import persistent_notification -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, SPOTIFY_SCOPES @@ -39,7 +39,7 @@ class SpotifyFlowHandler( """Extra data that needs to be appended to the authorize url.""" return {"scope": ",".join(SPOTIFY_SCOPES)} - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResultDict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for Spotify.""" spotify = Spotify(auth=data["token"]["access_token"]) @@ -61,7 +61,7 @@ class SpotifyFlowHandler( return self.async_create_entry(title=name, data=data) - async def async_step_reauth(self, entry: dict[str, Any]) -> FlowResultDict: + async def async_step_reauth(self, entry: dict[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" if entry: self.entry = entry @@ -77,7 +77,7 @@ class SpotifyFlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index ee76f6472ce..0e2f48d608d 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -8,7 +8,7 @@ from toonapi import Agreement, Toon, ToonError import voluptuous as vol from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler @@ -30,7 +30,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Return logger.""" return logging.getLogger(__name__) - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResultDict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Test connection and load up agreements.""" self.data = data @@ -50,7 +50,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_import( self, config: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Start a configuration flow based on imported data. This step is merely here to trigger "discovery" when the `toon` @@ -67,7 +67,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_agreement( self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + ) -> FlowResult: """Select Toon agreement to add.""" if len(self.agreements) == 1: return await self._create_entry(self.agreements[0]) @@ -88,7 +88,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): agreement_index = agreements_list.index(user_input[CONF_AGREEMENT]) return await self._create_entry(self.agreements[agreement_index]) - async def _create_entry(self, agreement: Agreement) -> FlowResultDict: + async def _create_entry(self, agreement: Agreement) -> FlowResult: if CONF_MIGRATE in self.context: await self.hass.config_entries.async_remove(self.context[CONF_MIGRATE]) diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 7dedf705f91..45189b63e05 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ID -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN @@ -27,7 +27,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -43,7 +43,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 6e984d72e2d..2c472212bda 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_GIID, @@ -58,7 +58,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -97,7 +97,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_installation( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Select Verisure installation to add.""" if len(self.installations) == 1: user_input = {CONF_GIID: list(self.installations)[0]} @@ -125,14 +125,14 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResultDict: + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Verisure.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle re-authentication with Verisure.""" errors: dict[str, str] = {} @@ -174,7 +174,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResultDict: + 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] @@ -204,7 +204,7 @@ class VerisureOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Manage Verisure options.""" errors = {} diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index da76f8081b4..55504a753f1 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -110,9 +110,7 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): """Initialize vizio options flow.""" self.config_entry = config_entry - async def async_step_init( - self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: """Manage the vizio options.""" if user_input is not None: if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): @@ -194,7 +192,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data = None self._apps = {} - async def _create_entry(self, input_dict: dict[str, Any]) -> FlowResultDict: + async def _create_entry(self, input_dict: dict[str, Any]) -> FlowResult: """Create vizio config entry.""" # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) @@ -205,9 +203,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict) - async def async_step_user( - self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -280,7 +276,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResultDict: + 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): @@ -344,7 +340,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: DiscoveryInfoType | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle zeroconf discovery.""" # If host already has port, no need to add it again if ":" not in discovery_info[CONF_HOST]: @@ -379,9 +375,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._must_show_form = True return await self.async_step_user(user_input=discovery_info) - async def async_step_pair_tv( - self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + async def async_step_pair_tv(self, user_input: dict[str, Any] = None) -> FlowResult: """ Start pairing process for TV. @@ -446,7 +440,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _pairing_complete(self, step_id: str) -> FlowResultDict: + async def _pairing_complete(self, step_id: str) -> FlowResult: """Handle config flow completion.""" if not self._must_show_form: return await self._create_entry(self._data) @@ -460,7 +454,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing_complete( self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + ) -> FlowResult: """ Complete non-import sourced config flow. @@ -470,7 +464,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing_complete_import( self, user_input: dict[str, Any] = None - ) -> FlowResultDict: + ) -> FlowResult: """ Complete import sourced config flow. diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 052fc858a1a..e3f0d8224e6 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ( ConfigFlow, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -23,15 +23,13 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> FlowResultDict: + async def async_step_user(self, user_input: ConfigType | 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 - ) -> FlowResultDict: + ) -> FlowResult: """Handle zeroconf discovery.""" if discovery_info is None: return self.async_abort(reason="cannot_connect") @@ -54,13 +52,13 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> FlowResultDict: + ) -> 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 - ) -> FlowResultDict: + ) -> FlowResult: """Config flow handler for WLED.""" source = self.context.get("source") @@ -101,7 +99,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResultDict: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -109,7 +107,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResultDict: + def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResult: """Show the confirm dialog to the user.""" name = self.context.get(CONF_NAME) return self.async_show_form( diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index b0f6c76b3f1..58cb37edcfc 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries, exceptions from homeassistant.components.hassio import is_hassio from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow, FlowResultDict +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 @@ -89,7 +89,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the initial step.""" if is_hassio(self.hass): return await self.async_step_on_supervisor() @@ -98,7 +98,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -134,7 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. @@ -152,7 +152,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Confirm the add-on discovery.""" if user_input is not None: return await self.async_step_on_supervisor( @@ -162,7 +162,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="hassio_confirm") @callback - def _async_create_entry_from_vars(self) -> FlowResultDict: + def _async_create_entry_from_vars(self) -> FlowResult: """Return a config entry for the flow.""" return self.async_create_entry( title=TITLE, @@ -177,7 +177,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -201,7 +201,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_install_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Install Z-Wave JS add-on.""" if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) @@ -221,13 +221,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_install_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Ask for config for Z-Wave JS add-on.""" addon_config = await self._async_get_addon_config() @@ -263,7 +263,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Start Z-Wave JS add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -281,7 +281,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Add-on start failed.""" return self.async_abort(reason="addon_start_failed") @@ -318,7 +318,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 5ad04ac96cf..09325b118bf 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -579,8 +579,8 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): self._hass_config = hass_config async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict - ) -> data_entry_flow.FlowResultDict: + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) @@ -688,7 +688,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): return flow async def async_post_init( - self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult ) -> None: """After a flow is initialised trigger new flow notifications.""" source = flow.context["source"] @@ -1190,7 +1190,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _async_in_progress( self, include_uninitialized: bool = False - ) -> list[data_entry_flow.FlowResultDict]: + ) -> list[data_entry_flow.FlowResult]: """Return other in progress flows for current domain.""" return [ flw @@ -1202,20 +1202,20 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def async_step_ignore( self, user_input: dict[str, Any] - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) return self.async_create_entry(title=user_input["title"], data={}) async def async_step_unignore( self, user_input: dict[str, Any] - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Rediscover a config entry by it's unique_id.""" return self.async_abort(reason="not_implemented") async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initiated by the user.""" return self.async_abort(reason="not_implemented") @@ -1245,7 +1245,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def async_step_discovery( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() @@ -1253,7 +1253,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def async_abort( self, *, reason: str, description_placeholders: dict | None = None - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Abort the config flow.""" # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( @@ -1271,37 +1271,37 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def async_step_hassio( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by HASS IO discovery.""" return await self.async_step_discovery(discovery_info) async def async_step_homekit( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Homekit discovery.""" return await self.async_step_discovery(discovery_info) async def async_step_mqtt( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by MQTT discovery.""" return await self.async_step_discovery(discovery_info) async def async_step_ssdp( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by SSDP discovery.""" return await self.async_step_discovery(discovery_info) async def async_step_zeroconf( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Zeroconf discovery.""" return await self.async_step_discovery(discovery_info) async def async_step_dhcp( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResultDict: + ) -> data_entry_flow.FlowResult: """Handle a flow initialized by DHCP discovery.""" return await self.async_step_discovery(discovery_info) @@ -1330,8 +1330,8 @@ class OptionsFlowManager(data_entry_flow.FlowManager): return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict - ) -> data_entry_flow.FlowResultDict: + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index a43f3035426..f442e4662c6 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -51,7 +51,7 @@ class AbortFlow(FlowError): self.description_placeholders = description_placeholders -class FlowResultDict(TypedDict, total=False): +class FlowResult(TypedDict, total=False): """Typed result dict.""" version: int @@ -112,17 +112,15 @@ class FlowManager(abc.ABC): @abc.abstractmethod async def async_finish_flow( - self, flow: FlowHandler, result: FlowResultDict - ) -> FlowResultDict: + self, flow: FlowHandler, result: FlowResult + ) -> FlowResult: """Finish a config flow and add an entry.""" - async def async_post_init(self, flow: FlowHandler, result: FlowResultDict) -> None: + async def async_post_init(self, flow: FlowHandler, result: FlowResult) -> None: """Entry has finished executing its first step asynchronously.""" @callback - def async_progress( - self, include_uninitialized: bool = False - ) -> list[FlowResultDict]: + def async_progress(self, include_uninitialized: bool = False) -> list[FlowResult]: """Return the flows in progress.""" return [ { @@ -137,7 +135,7 @@ class FlowManager(abc.ABC): async def async_init( self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None - ) -> FlowResultDict: + ) -> FlowResult: """Start a configuration flow.""" if context is None: context = {} @@ -165,7 +163,7 @@ class FlowManager(abc.ABC): handler: str, context: dict, data: Any, - ) -> tuple[FlowHandler, FlowResultDict]: + ) -> tuple[FlowHandler, FlowResult]: """Run the init in a task to allow it to be canceled at shutdown.""" flow = await self.async_create_flow(handler, context=context, data=data) if not flow: @@ -186,7 +184,7 @@ class FlowManager(abc.ABC): async def async_configure( self, flow_id: str, user_input: dict | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Continue a configuration flow.""" flow = self._progress.get(flow_id) @@ -243,7 +241,7 @@ class FlowManager(abc.ABC): step_id: str, user_input: dict | None, step_done: asyncio.Future | None = None, - ) -> FlowResultDict: + ) -> FlowResult: """Handle a step of a flow.""" method = f"async_step_{step_id}" @@ -256,7 +254,7 @@ class FlowManager(abc.ABC): ) try: - result: FlowResultDict = await getattr(flow, method)(user_input) + result: FlowResult = await getattr(flow, method)(user_input) except AbortFlow as err: result = _create_abort_data( flow.flow_id, flow.handler, err.reason, err.description_placeholders @@ -347,7 +345,7 @@ class FlowHandler: errors: dict[str, str] | None = None, description_placeholders: dict[str, Any] | None = None, last_step: bool | None = None, - ) -> FlowResultDict: + ) -> FlowResult: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -368,7 +366,7 @@ class FlowHandler: data: Mapping[str, Any], description: str | None = None, description_placeholders: dict | None = None, - ) -> FlowResultDict: + ) -> FlowResult: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -384,7 +382,7 @@ class FlowHandler: @callback def async_abort( self, *, reason: str, description_placeholders: dict | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Abort the config flow.""" return _create_abort_data( self.flow_id, self.handler, reason, description_placeholders @@ -393,7 +391,7 @@ class FlowHandler: @callback def async_external_step( self, *, step_id: str, url: str, description_placeholders: dict | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -405,7 +403,7 @@ class FlowHandler: } @callback - def async_external_step_done(self, *, next_step_id: str) -> FlowResultDict: + def async_external_step_done(self, *, next_step_id: str) -> FlowResult: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, @@ -421,7 +419,7 @@ class FlowHandler: step_id: str, progress_action: str, description_placeholders: dict | None = None, - ) -> FlowResultDict: + ) -> FlowResult: """Show a progress message to the user, without user input allowed.""" return { "type": RESULT_TYPE_SHOW_PROGRESS, @@ -433,7 +431,7 @@ class FlowHandler: } @callback - def async_show_progress_done(self, *, next_step_id: str) -> FlowResultDict: + def async_show_progress_done(self, *, next_step_id: str) -> FlowResult: """Mark the progress done.""" return { "type": RESULT_TYPE_SHOW_PROGRESS_DONE, @@ -449,7 +447,7 @@ def _create_abort_data( handler: str, reason: str, description_placeholders: dict | None = None, -) -> FlowResultDict: +) -> FlowResult: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_ABORT, diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index c9ac765ecbb..4020c9bb44c 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,7 +5,7 @@ from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import DiscoveryInfoType DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] @@ -31,7 +31,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -42,7 +42,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Confirm setup.""" if user_input is None: self._set_confirm_only() @@ -72,7 +72,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async def async_step_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow initialized by discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -87,7 +87,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_homekit = async_step_discovery async_step_dhcp = async_step_discovery - async def async_step_import(self, _: dict[str, Any] | None) -> FlowResultDict: + async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: """Handle a flow initialized by import.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -137,7 +137,7 @@ class WebhookFlowHandler(config_entries.ConfigFlow): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index e19a065eb02..ede345ce7de 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -24,7 +24,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components import http from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.network import NoURLAvailableError from .aiohttp_client import async_get_clientsession @@ -236,7 +236,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_pick_implementation( self, user_input: dict | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle a flow start.""" implementations = await async_get_implementations(self.hass, self.DOMAIN) @@ -267,7 +267,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Create an entry for auth.""" # Flow has been triggered by external data if user_input: @@ -293,7 +293,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_creation( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Create config entry from external data.""" token = await self.flow_impl.async_resolve_external_data(self.external_data) # Force int for non-compliant oauth2 providers @@ -310,7 +310,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): {"auth_implementation": self.flow_impl.domain, "token": token} ) - async def async_oauth_create_entry(self, data: dict) -> FlowResultDict: + async def async_oauth_create_entry(self, data: dict) -> FlowResult: """Create an entry for the flow. Ok to override if you want to fetch extra info or even add another step. diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index af0ea22d503..07f12a08262 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -22,8 +22,8 @@ class _BaseFlowManagerView(HomeAssistantView): # pylint: disable=no-self-use def _prepare_result_json( - self, result: data_entry_flow.FlowResultDict - ) -> data_entry_flow.FlowResultDict: + self, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy() From 3e47d2abd28b99b5698352cedcf251d39519e2e3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Apr 2021 16:09:59 +0200 Subject: [PATCH 016/140] Fix `host_valid()` logic in BraviaTV config flow (#49857) --- .../components/braviatv/config_flow.py | 2 +- tests/components/braviatv/test_config_flow.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 1ac31972f33..0004d85421d 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) def host_valid(host): """Return True if hostname or IP address is valid.""" try: - if ipaddress.ip_address(host).version == (4 or 6): + if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: disallowed = re.compile(r"[^a-zA-Z\d\-]") diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index cbe87f14839..36c7ae9955a 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -239,6 +239,39 @@ async def test_create_entry(hass): } +async def test_create_entry_with_ipv6_address(hass): + """Test that the user step works with device IPv6 address.""" + with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( + "bravia_tv.BraviaRC.is_connected", return_value=True + ), patch( + "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + ), patch( + "homeassistant.components.braviatv.async_setup_entry", return_value=True + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "2001:db8::1428:57ab"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authorize" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == "very_unique_string" + assert result["title"] == "TV-Model" + assert result["data"] == { + CONF_HOST: "2001:db8::1428:57ab", + CONF_PIN: "1234", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + async def test_options_flow(hass): """Test config flow options.""" config_entry = MockConfigEntry( From fddbed944d53d354b1c91d19963d5aca77902347 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Apr 2021 17:05:09 +0200 Subject: [PATCH 017/140] Fix `host_valid()` logic in DuneHD config flow (#49860) --- .../components/dunehd/config_flow.py | 2 +- tests/components/dunehd/test_config_flow.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 998ff408f36..034b1568c97 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) def host_valid(host): """Return True if hostname or IP address is valid.""" try: - if ipaddress.ip_address(host).version == (4 or 6): + if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: if len(host) > 253: diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index b6e57a71668..278c94864b8 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -64,6 +64,16 @@ async def test_user_invalid_host(hass): assert result["errors"] == {CONF_HOST: "invalid_host"} +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_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} + ) + + assert result["errors"] == {CONF_HOST: "invalid_host"} + + async def test_user_cannot_connect(hass): """Test that errors are shown when cannot connect to the host.""" with patch("pdunehd.DuneHDPlayer.update_state", return_value={}): @@ -101,3 +111,17 @@ async def test_create_entry(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "dunehd-host" assert result["data"] == {CONF_HOST: "dunehd-host"} + + +async def test_create_entry_with_ipv6_address(hass): + """Test that the user step works with device IPv6 address..""" + with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "2001:db8::1428:57ab"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "2001:db8::1428:57ab" + assert result["data"] == {CONF_HOST: "2001:db8::1428:57ab"} From 9574465acc217e2bc6cd2a457b94cc5bc36c47d6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Apr 2021 11:06:27 -0400 Subject: [PATCH 018/140] Use EVENT_HOMEASSISTANT_STARTED instead of EVENT_HOMEASSISTANT_START (#49861) --- homeassistant/components/google_travel_time/sensor.py | 4 ++-- homeassistant/components/waze_travel_time/sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 92a359a5780..7256697fe03 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_MODE, CONF_NAME, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, TIME_MINUTES, ) from homeassistant.core import CoreState, HomeAssistant @@ -189,7 +189,7 @@ class GoogleTravelTimeSensor(SensorEntity): """Handle when entity is added.""" if self.hass.state != CoreState.running: self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self.first_update + EVENT_HOMEASSISTANT_STARTED, self.first_update ) else: await self.first_update() diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 02446849e98..f85e25cf0d3 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_NAME, CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, TIME_MINUTES, ) from homeassistant.core import Config, CoreState, HomeAssistant @@ -189,7 +189,7 @@ class WazeTravelTime(SensorEntity): """Handle when entity is added.""" if self.hass.state != CoreState.running: self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self.first_update + EVENT_HOMEASSISTANT_STARTED, self.first_update ) else: await self.first_update() From 81712cbd0da7855e02f897c3c5edb44e564046b2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Apr 2021 17:11:22 +0200 Subject: [PATCH 019/140] Fix `host_valid()` logic in Vilfo config flow (#49862) --- homeassistant/components/vilfo/config_flow.py | 2 +- tests/components/vilfo/test_config_flow.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py index d5646c8caf3..569ce7992fa 100644 --- a/homeassistant/components/vilfo/config_flow.py +++ b/homeassistant/components/vilfo/config_flow.py @@ -32,7 +32,7 @@ RESULT_INVALID_AUTH = "invalid_auth" def host_valid(host): """Return True if hostname or IP address is valid.""" try: - if ipaddress.ip_address(host).version == (4 or 6): + if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: disallowed = re.compile(r"[^a-zA-Z\d\-]") diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index d828963ba39..d3face657de 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -155,6 +155,7 @@ async def test_validate_input_returns_data(hass): """Test we handle the MAC address being resolved or not.""" mock_data = {"host": "testadmin.vilfo.com", "access_token": "test-token"} mock_data_with_ip = {"host": "192.168.0.1", "access_token": "test-token"} + mock_data_with_ipv6 = {"host": "2001:db8::1428:57ab", "access_token": "test-token"} mock_mac = "FF-00-00-00-00-00" with patch("vilfo.Client.ping", return_value=None), patch( @@ -178,6 +179,9 @@ async def test_validate_input_returns_data(hass): result3 = await hass.components.vilfo.config_flow.validate_input( hass, data=mock_data_with_ip ) + result4 = await hass.components.vilfo.config_flow.validate_input( + hass, data=mock_data_with_ipv6 + ) assert result2["title"] == mock_data["host"] assert result2[CONF_HOST] == mock_data["host"] @@ -188,3 +192,8 @@ async def test_validate_input_returns_data(hass): assert result3[CONF_HOST] == mock_data_with_ip["host"] assert result3[CONF_MAC] == mock_mac assert result3[CONF_ID] == mock_mac + + assert result4["title"] == mock_data_with_ipv6["host"] + assert result4[CONF_HOST] == mock_data_with_ipv6["host"] + assert result4[CONF_MAC] == mock_mac + assert result4[CONF_ID] == mock_mac From eb17adf4fa673932b798cf436b27d88f97da4bf5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Apr 2021 15:57:48 -0400 Subject: [PATCH 020/140] Fix options flow bugs in Google/Waze Time Travel (#49866) --- .../google_travel_time/config_flow.py | 17 ++++++++++------- .../components/waze_travel_time/config_flow.py | 9 ++++++--- .../components/waze_travel_time/sensor.py | 6 +++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 899ce804796..3bef7d35d50 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -56,14 +56,17 @@ class GoogleOptionsFlow(config_entries.OptionsFlow): user_input[CONF_ARRIVAL_TIME] = time else: user_input[CONF_DEPARTURE_TIME] = time - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry( + title="", + data={k: v for k, v in user_input.items() if v not in (None, "")}, + ) if CONF_ARRIVAL_TIME in self.config_entry.options: default_time_type = ARRIVAL_TIME default_time = self.config_entry.options[CONF_ARRIVAL_TIME] else: default_time_type = DEPARTURE_TIME - default_time = self.config_entry.options.get(CONF_ARRIVAL_TIME) + default_time = self.config_entry.options.get(CONF_ARRIVAL_TIME, "") return self.async_show_form( step_id="init", @@ -75,10 +78,10 @@ class GoogleOptionsFlow(config_entries.OptionsFlow): vol.Optional( CONF_LANGUAGE, default=self.config_entry.options.get(CONF_LANGUAGE), - ): vol.In(ALL_LANGUAGES), + ): vol.In([None, *ALL_LANGUAGES]), vol.Optional( CONF_AVOID, default=self.config_entry.options.get(CONF_AVOID) - ): vol.In(AVOID), + ): vol.In([None, *AVOID]), vol.Optional( CONF_UNITS, default=self.config_entry.options[CONF_UNITS] ): vol.In(UNITS), @@ -89,17 +92,17 @@ class GoogleOptionsFlow(config_entries.OptionsFlow): vol.Optional( CONF_TRAFFIC_MODEL, default=self.config_entry.options.get(CONF_TRAFFIC_MODEL), - ): vol.In(TRAVEL_MODEL), + ): vol.In([None, *TRAVEL_MODEL]), vol.Optional( CONF_TRANSIT_MODE, default=self.config_entry.options.get(CONF_TRANSIT_MODE), - ): vol.In(TRANSPORT_TYPE), + ): vol.In([None, *TRANSPORT_TYPE]), vol.Optional( CONF_TRANSIT_ROUTING_PREFERENCE, default=self.config_entry.options.get( CONF_TRANSIT_ROUTING_PREFERENCE ), - ): vol.In(TRANSIT_PREFS), + ): vol.In([None, *TRANSIT_PREFS]), } ), ) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index 4100284273e..10262e12804 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -41,7 +41,10 @@ class WazeOptionsFlow(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle the initial step.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry( + title="", + data={k: v for k, v in user_input.items() if v not in (None, "")}, + ) return self.async_show_form( step_id="init", @@ -49,11 +52,11 @@ class WazeOptionsFlow(config_entries.OptionsFlow): { vol.Optional( CONF_INCL_FILTER, - default=self.config_entry.options.get(CONF_INCL_FILTER), + default=self.config_entry.options.get(CONF_INCL_FILTER, ""), ): cv.string, vol.Optional( CONF_EXCL_FILTER, - default=self.config_entry.options.get(CONF_EXCL_FILTER), + default=self.config_entry.options.get(CONF_EXCL_FILTER, ""), ): cv.string, vol.Optional( CONF_REALTIME, diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index f85e25cf0d3..965cd5c8a37 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -331,7 +331,11 @@ class WazeTravelTimeData: if excl_filter.lower() not in k.lower() } - route = list(routes)[0] + if len(routes) > 0: + route = list(routes)[0] + else: + _LOGGER.warning("No routes found") + return self.duration, distance = routes[route] From 6bb738ee277fbd2e52981bf9ada45da83c912a4b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 29 Apr 2021 19:13:58 +0300 Subject: [PATCH 021/140] Add color modes to Shelly light (#49867) --- homeassistant/components/shelly/light.py | 117 +++++++++++++---------- 1 file changed, 68 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 370522415fd..61cb961e224 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -6,18 +6,18 @@ from aioshelly import Block 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, + brightness_supported, ) from homeassistant.core import callback from homeassistant.util.color import ( - color_hs_to_RGB, - color_RGB_to_hs, color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) @@ -68,24 +68,24 @@ class ShellyLight(ShellyBlockEntity, LightEntity): super().__init__(wrapper, block) self.control_result = None self.mode_result = None - self._supported_features = 0 + self._supported_color_modes = set() self._min_kelvin = KELVIN_MIN_VALUE_WHITE self._max_kelvin = KELVIN_MAX_VALUE - if hasattr(block, "brightness") or hasattr(block, "gain"): - self._supported_features |= SUPPORT_BRIGHTNESS - if hasattr(block, "colorTemp"): - self._supported_features |= SUPPORT_COLOR_TEMP - if hasattr(block, "white"): - self._supported_features |= SUPPORT_WHITE_VALUE if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): - self._supported_features |= SUPPORT_COLOR 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) - @property - def supported_features(self) -> int: - """Supported features.""" - return self._supported_features + if hasattr(block, "colorTemp"): + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + + if not self._supported_color_modes: + if hasattr(block, "brightness") or hasattr(block, "gain"): + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + else: + self._supported_color_modes.add(COLOR_MODE_ONOFF) @property def is_on(self) -> bool: @@ -114,8 +114,8 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return "white" @property - def brightness(self) -> int: - """Brightness of light.""" + def brightness(self) -> int | None: + """Return the brightness of this light between 0..255.""" if self.mode == "color": if self.control_result: brightness_pct = self.control_result["gain"] @@ -130,20 +130,24 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return round(255 * brightness_pct / 100) @property - def white_value(self) -> int: - """White value of light.""" - if self.control_result: - white = self.control_result["white"] - else: - white = self.block.white - return int(white) + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if self.mode == "color": + if hasattr(self.block, "white"): + return COLOR_MODE_RGBW + return COLOR_MODE_RGB + + if hasattr(self.block, "colorTemp"): + return COLOR_MODE_COLOR_TEMP + + if hasattr(self.block, "brightness") or hasattr(self.block, "gain"): + return COLOR_MODE_BRIGHTNESS + + return COLOR_MODE_ONOFF @property - def hs_color(self) -> tuple[float, float]: - """Return the hue and saturation color value of light.""" - if self.mode == "white": - return color_RGB_to_hs(255, 255, 255) - + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" if self.control_result: red = self.control_result["red"] green = self.control_result["green"] @@ -152,14 +156,21 @@ class ShellyLight(ShellyBlockEntity, LightEntity): red = self.block.red green = self.block.green blue = self.block.blue - return color_RGB_to_hs(red, green, blue) + return [red, green, blue] + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """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] @property def color_temp(self) -> int | None: """Return the CT color value in mireds.""" - if self.mode == "color": - return None - if self.control_result: color_temp = self.control_result["temp"] else: @@ -179,6 +190,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity): """Return the warmest color_temp that this light supports.""" return int(color_temperature_kelvin_to_mired(self._min_kelvin)) + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return self._supported_color_modes + async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" if self.block.type == "relay": @@ -187,31 +203,34 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return set_mode = None + supported_color_modes = self._supported_color_modes params = {"turn": "on"} - if ATTR_BRIGHTNESS in kwargs: + + if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): params["gain"] = brightness_pct if hasattr(self.block, "brightness"): params["brightness"] = brightness_pct - if ATTR_COLOR_TEMP in kwargs: + + if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) # Color temperature change - used only in white mode, switch device mode to white set_mode = "white" - params["red"] = params["green"] = params["blue"] = 255 params["temp"] = int(color_temp) - if ATTR_HS_COLOR in kwargs: - red, green, blue = color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + + if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" - params["red"] = red - params["green"] = green - params["blue"] = blue - if ATTR_WHITE_VALUE in kwargs: - # White channel change - used only in color mode, switch device mode device to color + (params["red"], params["green"], params["blue"]) = kwargs[ATTR_RGB_COLOR] + + if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: + # Color channels change - used only in color mode, switch device mode to color set_mode = "color" - params["white"] = int(kwargs[ATTR_WHITE_VALUE]) + (params["red"], params["green"], params["blue"], params["white"]) = kwargs[ + ATTR_RGBW_COLOR + ] if set_mode and self.mode != set_mode: self.mode_result = await self.wrapper.device.switch_light_mode(set_mode) From 809bb4d5ed6c44a544837b97d59e25e66a4b98c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 29 Apr 2021 21:45:25 +0300 Subject: [PATCH 022/140] Restore missing Huawei LTE cleanup on HA stop (#49871) Refs https://github.com/home-assistant/core/pull/49788#discussion_r623071013 --- homeassistant/components/huawei_lte/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c256fc2e7f2..690d9ed63a4 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -39,6 +39,7 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_URL, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady @@ -442,6 +443,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async_track_time_interval(hass, _update_router, SCAN_INTERVAL) ) + # Clean up at end + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + ) + return True From 615ee6c6c429c526d1f9cf5cd404c5307af3a095 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Apr 2021 23:31:34 +0200 Subject: [PATCH 023/140] Fix jumpy Verisure Wallplug state (#49880) --- homeassistant/components/verisure/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index a97758d17f9..1284ed5fde4 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -89,9 +89,11 @@ class VerisureSmartplug(CoordinatorEntity, SwitchEntity): self.coordinator.verisure.set_smartplug_state(self.serial_number, True) self._state = True self._change_timestamp = monotonic() + self.schedule_update_ha_state() def turn_off(self, **kwargs) -> None: """Set smartplug status off.""" self.coordinator.verisure.set_smartplug_state(self.serial_number, False) self._state = False self._change_timestamp = monotonic() + self.schedule_update_ha_state() From ff6396d9c2fa2bfa414183f136aa11afdb1af297 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Apr 2021 23:31:51 +0200 Subject: [PATCH 024/140] Update frontend to 20210407.1 (#49882) --- 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 e108a507f93..b910c0acc46 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==20210428.0" + "home-assistant-frontend==20210407.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d5b164c662c..f0aff98b571 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==20210428.0 +home-assistant-frontend==20210407.1 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2bff64dc8a6..ad6b87ecc59 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==20210428.0 +home-assistant-frontend==20210407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e449cb8cae..657f3c967d0 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==20210428.0 +home-assistant-frontend==20210407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 4087a173cd14fea8885a07dfcfa44df1502af40b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Apr 2021 14:34:47 -0700 Subject: [PATCH 025/140] Bumped version to 2021.5.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 68a3e2bde58..2e871bab5b5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 41cf66c294e94bfc3b955375652bd40abac66d2d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Apr 2021 19:02:45 -0600 Subject: [PATCH 026/140] Remove no-longer-functioning SimpliSafe websocket support (#49876) * Remove no-longer-functioning SimpliSafe websocket support * Linting --- .../components/simplisafe/__init__.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 9997b86c288..80784ce51b7 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -17,13 +17,7 @@ from simplipy.websocket import ( ) import voluptuous as vol -from homeassistant.const import ( - ATTR_CODE, - CONF_CODE, - CONF_TOKEN, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -454,17 +448,21 @@ class SimpliSafe: async def async_init(self): """Initialize the data class.""" - asyncio.create_task(self.websocket.async_connect()) + # 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() + # async def async_websocket_disconnect(_): + # """Define an event handler to disconnect from the websocket.""" + # await self.websocket.async_disconnect() - self._hass.data[DOMAIN][DATA_LISTENER][self.config_entry.entry_id].append( - self._hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_websocket_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(): From aa46549be950f9fdd2bd8629a5a823dbbb0540fc Mon Sep 17 00:00:00 2001 From: Tom Toor Date: Thu, 29 Apr 2021 22:55:51 -0700 Subject: [PATCH 027/140] Reduced polling interval for mutesync integration (#49884) --- homeassistant/components/mutesync/__init__.py | 4 ++-- homeassistant/components/mutesync/config_flow.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index 9ed00f84feb..7bee5ff5a9b 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_data(): """Update the data.""" - async with async_timeout.timeout(5): + async with async_timeout.timeout(2.5): return await client.get_state() coordinator = hass.data.setdefault(DOMAIN, {})[ @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, logging.getLogger(__name__), name=DOMAIN, - update_interval=timedelta(seconds=10), + update_interval=timedelta(seconds=5), update_method=update_data, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py index 72002d072f3..d2d00f68ae6 100644 --- a/homeassistant/components/mutesync/config_flow.py +++ b/homeassistant/components/mutesync/config_flow.py @@ -26,7 +26,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, """ session = hass.helpers.aiohttp_client.async_get_clientsession() try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(5): token = await mutesync.authenticate(session, data["host"]) except aiohttp.ClientResponseError as error: if error.status == 403: From 83ff5bfa377566684f6878867038576bb980b5a6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 30 Apr 2021 02:33:15 -0400 Subject: [PATCH 028/140] Don't validate inputs on platform setup for Google/Waze travel time (#49886) * Don't validate inputs on platform setup for Google/Waze travel time * feedback from other PR --- .../components/google_travel_time/sensor.py | 11 +++-------- homeassistant/components/waze_travel_time/sensor.py | 13 ++++--------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 7256697fe03..2b347679420 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( TIME_MINUTES, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -40,6 +39,7 @@ from .const import ( CONF_TRANSIT_ROUTING_PREFERENCE, CONF_TRAVEL_MODE, CONF_UNITS, + DEFAULT_NAME, DOMAIN, TRACKABLE_DOMAINS, TRANSIT_PREFS, @@ -48,7 +48,7 @@ from .const import ( TRAVEL_MODEL, UNITS, ) -from .helpers import get_location_from_entity, is_valid_config_entry, resolve_zone +from .helpers import get_location_from_entity, resolve_zone _LOGGER = logging.getLogger(__name__) @@ -126,12 +126,7 @@ async def async_setup_entry( api_key = config_entry.data[CONF_API_KEY] origin = config_entry.data[CONF_ORIGIN] destination = config_entry.data[CONF_DESTINATION] - name = config_entry.data[CONF_NAME] - - if not await hass.async_add_executor_job( - is_valid_config_entry, hass, _LOGGER, api_key, origin, destination - ): - raise ConfigEntryNotReady + name = config_entry.data.get(CONF_NAME, DEFAULT_NAME) client = Client(api_key, timeout=10) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 965cd5c8a37..2e93ee2a85a 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( TIME_MINUTES, ) from homeassistant.core import Config, CoreState, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from .const import ( @@ -43,6 +42,7 @@ from .const import ( DEFAULT_AVOID_FERRIES, DEFAULT_AVOID_SUBSCRIPTION_ROADS, DEFAULT_AVOID_TOLL_ROADS, + DEFAULT_NAME, DEFAULT_REALTIME, DEFAULT_VEHICLE_TYPE, DOMAIN, @@ -52,7 +52,7 @@ from .const import ( UNITS, VEHICLE_TYPES, ) -from .helpers import get_location_from_entity, is_valid_config_entry, resolve_zone +from .helpers import get_location_from_entity, resolve_zone _LOGGER = logging.getLogger(__name__) @@ -142,12 +142,7 @@ async def async_setup_entry( destination = config_entry.data[CONF_DESTINATION] origin = config_entry.data[CONF_ORIGIN] region = config_entry.data[CONF_REGION] - name = config_entry.data[CONF_NAME] - - if not await hass.async_add_executor_job( - is_valid_config_entry, hass, _LOGGER, origin, destination, region - ): - raise ConfigEntryNotReady + name = config_entry.data.get(CONF_NAME, DEFAULT_NAME) data = WazeTravelTimeData( None, @@ -331,7 +326,7 @@ class WazeTravelTimeData: if excl_filter.lower() not in k.lower() } - if len(routes) > 0: + if routes: route = list(routes)[0] else: _LOGGER.warning("No routes found") From cd3633c48f7e4939a87d722bdf18d5c83ecdf392 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Apr 2021 10:26:56 +0200 Subject: [PATCH 029/140] Update frontend to 20210429.0 (#49896) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b910c0acc46..97e12b1312d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20210407.1" - ], + "requirements": ["home-assistant-frontend==20210429.0"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f0aff98b571..472cde3ec27 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==20210407.1 +home-assistant-frontend==20210429.0 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index ad6b87ecc59..dc57434b7dd 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==20210407.1 +home-assistant-frontend==20210429.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 657f3c967d0..33592c64249 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==20210407.1 +home-assistant-frontend==20210429.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0ae392df75ccd758343c16f02d62f016f6f07686 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Apr 2021 11:12:31 +0200 Subject: [PATCH 030/140] Fix unexpected data in Waze config flow import (#49902) --- homeassistant/components/waze_travel_time/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 2e93ee2a85a..2796838ee50 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -13,8 +13,10 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + CONF_ENTITY_NAMESPACE, CONF_NAME, CONF_REGION, + CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM_IMPERIAL, EVENT_HOMEASSISTANT_STARTED, TIME_MINUTES, @@ -78,7 +80,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( CONF_AVOID_SUBSCRIPTION_ROADS, default=DEFAULT_AVOID_SUBSCRIPTION_ROADS ): cv.boolean, vol.Optional(CONF_AVOID_FERRIES, default=DEFAULT_AVOID_FERRIES): cv.boolean, - } + # Remove options to exclude from import + vol.Remove(CONF_ENTITY_NAMESPACE): cv.string, + vol.Remove(CONF_SCAN_INTERVAL): cv.time_period, + }, + extra=vol.REMOVE_EXTRA, ) From 1e3d90ac0ac22626ec770667a1fd7ee8a07c7897 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Apr 2021 11:12:13 +0200 Subject: [PATCH 031/140] Fix unexpected data in Google Travel Time config flow import (#49903) --- homeassistant/components/google_travel_time/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 2b347679420..0669d9e983e 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -14,8 +14,10 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, + CONF_ENTITY_NAMESPACE, CONF_MODE, CONF_NAME, + CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED, TIME_MINUTES, ) @@ -79,7 +81,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ), ), - } + # Remove options to exclude from import + vol.Remove(CONF_ENTITY_NAMESPACE): cv.string, + vol.Remove(CONF_SCAN_INTERVAL): cv.time_period, + }, + extra=vol.REMOVE_EXTRA, ) From 2593dc507deedb7027a41a0fb0ac54c9ad652af7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Apr 2021 11:14:58 +0200 Subject: [PATCH 032/140] Bumped version to 2021.5.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2e871bab5b5..8e7c387283f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 8951b9ede477cac46be0c1ed0d4ca81636314d5b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 30 Apr 2021 12:29:34 -0400 Subject: [PATCH 033/140] Move ZHA config panel section translations to the backend (#49816) --- homeassistant/components/zha/strings.json | 13 +++++++++++++ homeassistant/components/zha/translations/en.json | 13 +++++++++++++ script/hassfest/translations.py | 6 ++++++ 3 files changed, 32 insertions(+) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 550fad3c2c5..2c2a93aa4d6 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -29,6 +29,19 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "config_panel": { + "zha_options": { + "title": "Global Options", + "enable_identify_on_join": "Enable identify effect when devices join the network", + "default_light_transition": "Default light transition time (seconds)" + }, + "zha_alarm_options": { + "title": "Alarm Control Panel Options", + "alarm_master_code": "Master code for the alarm control panel(s)", + "alarm_failed_tries": "The number of consecutive failed code entries to trigger an alarm", + "alarm_arm_requires_code": "Code required for arming actions" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", "warn": "Warn" }, "trigger_type": { diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index d3ed2ddfce4..2a6aa34d886 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Code required for arming actions", + "alarm_failed_tries": "The number of consecutive failed code entries to trigger an alarm", + "alarm_master_code": "Master code for the alarm control panel(s)", + "title": "Alarm Control Panel Options" + }, + "zha_options": { + "default_light_transition": "Default light transition time (seconds)", + "enable_identify_on_join": "Enable identify effect when devices join the network", + "title": "Global Options" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index b7a6341d25c..4143d61ca5d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -142,6 +142,12 @@ def gen_strings_schema(config: Config, integration: Integration): vol.Optional("system_health"): { vol.Optional("info"): {str: cv.string_with_no_html} }, + vol.Optional("config_panel"): cv.schema_with_slug_keys( + cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=lowercase_validator + ), + slug_validator=vol.Any("_", cv.slug), + ), } ) From 1577276e87abc364a478db94c2cbcfb0f0cdd213 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 30 Apr 2021 07:48:11 -0400 Subject: [PATCH 034/140] Small ZHA code cleanup (#49908) --- homeassistant/components/zha/alarm_control_panel.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index bd11ce07741..dd705e44798 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -5,9 +5,6 @@ import logging from zigpy.zcl.clusters.security import IasAce from homeassistant.components.alarm_control_panel import ( - ATTR_CHANGED_BY, - ATTR_CODE_ARM_REQUIRED, - ATTR_CODE_FORMAT, DOMAIN, FORMAT_TEXT, SUPPORT_ALARM_ARM_AWAY, @@ -162,13 +159,3 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): def state(self): """Return the state of the entity.""" return IAS_ACE_STATE_MAP.get(self._channel.armed_state) - - @property - def state_attributes(self): - """Return the state attributes.""" - state_attr = { - ATTR_CODE_FORMAT: self.code_format, - ATTR_CHANGED_BY: self.changed_by, - ATTR_CODE_ARM_REQUIRED: self.code_arm_required, - } - return state_attr From ea3978be98964e16e468c689da44e676c4f6eb6f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 30 Apr 2021 16:15:59 -0500 Subject: [PATCH 035/140] Use header URI in Plex config flow (#49915) --- homeassistant/components/plex/config_flow.py | 10 +- tests/components/plex/test_config_flow.py | 149 +++++++++---------- 2 files changed, 80 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index d1fa5684cf5..24303dedecd 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -10,6 +10,7 @@ import requests.exceptions import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import http from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( @@ -25,7 +26,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.network import get_url from .const import ( AUTH_CALLBACK_NAME, @@ -52,6 +52,8 @@ from .const import ( from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer +HEADER_FRONTEND_BASE = "HA-Frontend-Base" + _LOGGER = logging.getLogger(__package__) @@ -286,7 +288,11 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_plex_website_auth(self): """Begin external auth flow on Plex website.""" self.hass.http.register_view(PlexAuthorizationCallbackView) - hass_url = get_url(self.hass) + if (req := http.current_request.get()) is None: + raise RuntimeError("No current request in context") + if (hass_url := req.headers.get(HEADER_FRONTEND_BASE)) is None: + raise RuntimeError("No header in request") + headers = {"Origin": hass_url} payload = { "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index d5209201d94..05bf15b4729 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -4,6 +4,7 @@ import ssl from unittest.mock import patch import plexapi.exceptions +import pytest import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -21,7 +22,6 @@ from homeassistant.components.plex.const import ( PLEX_SERVER_CONFIG, SERVERS, ) -from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ( ENTRY_STATE_LOADED, SOURCE_INTEGRATION_DISCOVERY, @@ -45,13 +45,8 @@ from .mock_classes import MockGDM from tests.common import MockConfigEntry -async def test_bad_credentials(hass): +async def test_bad_credentials(hass, current_request_with_host): """Test when provided credentials are rejected.""" - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -78,13 +73,8 @@ async def test_bad_credentials(hass): assert result["errors"][CONF_TOKEN] == "faulty_credentials" -async def test_bad_hostname(hass, mock_plex_calls): +async def test_bad_hostname(hass, mock_plex_calls, current_request_with_host): """Test when an invalid address is provided.""" - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -112,13 +102,8 @@ async def test_bad_hostname(hass, mock_plex_calls): assert result["errors"][CONF_HOST] == "not_found" -async def test_unknown_exception(hass): +async def test_unknown_exception(hass, current_request_with_host): """Test when an unknown exception is encountered.""" - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -141,15 +126,12 @@ async def test_unknown_exception(hass): assert result["reason"] == "unknown" -async def test_no_servers_found(hass, mock_plex_calls, requests_mock, empty_payload): +async def test_no_servers_found( + hass, mock_plex_calls, requests_mock, empty_payload, current_request_with_host +): """Test when no servers are on an account.""" requests_mock.get("https://plex.tv/api/resources", text=empty_payload) - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -173,14 +155,10 @@ async def test_no_servers_found(hass, mock_plex_calls, requests_mock, empty_payl assert result["errors"]["base"] == "no_servers" -async def test_single_available_server(hass, mock_plex_calls): +async def test_single_available_server( + hass, mock_plex_calls, current_request_with_host +): """Test creating an entry with one server available.""" - - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -217,15 +195,13 @@ async def test_single_available_server(hass, mock_plex_calls): async def test_multiple_servers_with_selection( - hass, mock_plex_calls, requests_mock, plextv_resources_base + hass, + mock_plex_calls, + requests_mock, + plextv_resources_base, + current_request_with_host, ): """Test creating an entry with multiple servers available.""" - - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -273,15 +249,13 @@ async def test_multiple_servers_with_selection( async def test_adding_last_unconfigured_server( - hass, mock_plex_calls, requests_mock, plextv_resources_base + hass, + mock_plex_calls, + requests_mock, + plextv_resources_base, + current_request_with_host, ): """Test automatically adding last unconfigured server when multiple servers on account.""" - - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - MockConfigEntry( domain=DOMAIN, data={ @@ -331,15 +305,14 @@ async def test_adding_last_unconfigured_server( async def test_all_available_servers_configured( - hass, entry, requests_mock, plextv_account, plextv_resources_base + hass, + entry, + requests_mock, + plextv_account, + plextv_resources_base, + current_request_with_host, ): """Test when all available servers are already configured.""" - - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - entry.add_to_hass(hass) MockConfigEntry( @@ -470,14 +443,8 @@ async def test_option_flow_new_users_available(hass, entry, setup_plex_server): assert "[New]" in multiselect_defaults[user] -async def test_external_timed_out(hass): +async def test_external_timed_out(hass, current_request_with_host): """Test when external flow times out.""" - - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -500,14 +467,8 @@ async def test_external_timed_out(hass): assert result["reason"] == "token_request_timeout" -async def test_callback_view(hass, aiohttp_client): +async def test_callback_view(hass, aiohttp_client, current_request_with_host): """Test callback view.""" - - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -529,12 +490,8 @@ async def test_callback_view(hass, aiohttp_client): assert resp.status == 200 -async def test_manual_config(hass, mock_plex_calls): +async def test_manual_config(hass, mock_plex_calls, current_request_with_host): """Test creating via manual configuration.""" - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) class WrongCertValidaitionException(requests.exceptions.SSLError): """Mock the exception showing an unmatched error.""" @@ -744,13 +701,11 @@ async def test_integration_discovery(hass): assert flow["step_id"] == "user" -async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): +async def test_trigger_reauth( + hass, entry, mock_plex_server, mock_websocket, current_request_with_host +): """Test setup and reauthorization of a Plex token.""" await async_setup_component(hass, "persistent_notification", {}) - await async_process_ha_core_config( - hass, - {"internal_url": "http://example.local:8123"}, - ) assert entry.state == ENTRY_STATE_LOADED @@ -791,3 +746,43 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN" + + +async def test_client_request_missing(hass): + """Test when client headers are not set properly.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("plexauth.PlexAuth.initiate_auth"), patch( + "plexauth.PlexAuth.token", return_value=None + ): + with pytest.raises(RuntimeError): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + +async def test_client_header_issues(hass, current_request_with_host): + """Test when client headers are not set properly.""" + + class MockRequest: + headers = {} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("plexauth.PlexAuth.initiate_auth"), patch( + "plexauth.PlexAuth.token", return_value=None + ), patch( + "homeassistant.components.http.current_request.get", return_value=MockRequest() + ): + with pytest.raises(RuntimeError): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) From adb00c1db1bbe082dc527f91b18456a9ba977e47 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Apr 2021 22:25:57 +0200 Subject: [PATCH 036/140] Update frontend to 20210430.0 (#49928) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 97e12b1312d..549a64886c1 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20210429.0"], + "requirements": [ + "home-assistant-frontend==20210430.0" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 472cde3ec27..be78796e8de 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==20210429.0 +home-assistant-frontend==20210430.0 httpx==0.18.0 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index dc57434b7dd..e0f9e52f68f 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==20210429.0 +home-assistant-frontend==20210430.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33592c64249..69a44550d16 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==20210429.0 +home-assistant-frontend==20210430.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From cbd292491c6bf5b8a0a263ad3fbfe152494086ba Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 30 Apr 2021 16:34:46 -0400 Subject: [PATCH 037/140] Bump xbox-webapi to 2.0.11 (#49929) --- homeassistant/components/xbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 64cda6055c0..d02f9cc2279 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -3,7 +3,7 @@ "name": "Xbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", - "requirements": ["xbox-webapi==2.0.8"], + "requirements": ["xbox-webapi==2.0.11"], "dependencies": ["http"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index e0f9e52f68f..74d7efb3738 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2350,7 +2350,7 @@ wolf_smartset==0.1.8 xbee-helper==0.0.7 # homeassistant.components.xbox -xbox-webapi==2.0.8 +xbox-webapi==2.0.11 # homeassistant.components.xbox_live xboxapi==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69a44550d16..0cc701db9e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1244,7 +1244,7 @@ wled==0.4.4 wolf_smartset==0.1.8 # homeassistant.components.xbox -xbox-webapi==2.0.8 +xbox-webapi==2.0.11 # homeassistant.components.knx xknx==0.18.1 From c10d09f2f488834573c1406900ae4ffb9daf9fd7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Apr 2021 14:04:59 -0700 Subject: [PATCH 038/140] Iqvia to check bad fetch during setup (#49931) --- homeassistant/components/iqvia/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 14e6353a064..65914bb1756 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -36,15 +36,9 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) PLATFORMS = ["sensor"] -async def async_setup(hass, config): - """Set up the IQVIA component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} - return True - - async def async_setup_entry(hass, entry): """Set up IQVIA as config entry.""" - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + coordinators = {} if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -72,19 +66,18 @@ async def async_setup_entry(hass, entry): (TYPE_DISEASE_FORECAST, client.disease.extended), (TYPE_DISEASE_INDEX, client.disease.current), ]: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - sensor_type - ] = DataUpdateCoordinator( + coordinator = coordinators[sensor_type] = DataUpdateCoordinator( hass, LOGGER, name=f"{entry.data[CONF_ZIP_CODE]} {sensor_type}", update_interval=DEFAULT_SCAN_INTERVAL, update_method=partial(async_get_data_from_api, api_coro), ) - init_data_update_tasks.append(coordinator.async_refresh()) + init_data_update_tasks.append(coordinator.async_config_entry_first_refresh()) await asyncio.gather(*init_data_update_tasks) + hass.data[DOMAIN].setdefault(DATA_COORDINATOR, {})[entry.entry_id] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True From 8189cf8c0b9f627687e8cd04661fbf435397b830 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Apr 2021 14:59:50 -0700 Subject: [PATCH 039/140] Bumped version to 2021.5.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8e7c387283f..0f694f097c2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 35bca704f8d25f9ecee5bd5eeb38effe2f69b02c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 29 Apr 2021 00:03:34 +0000 Subject: [PATCH 040/140] [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 3542f58b18d6aa0e812f56b2259d37e2e33b1a2c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 30 Apr 2021 00:04:58 +0000 Subject: [PATCH 041/140] [ci skip] Translation update --- .../components/fritz/translations/pl.json | 44 +++++++++++++++++++ .../components/goalzero/translations/ca.json | 2 +- .../components/goalzero/translations/pl.json | 2 +- .../components/goalzero/translations/ru.json | 2 +- .../goalzero/translations/zh-Hant.json | 2 +- .../google_travel_time/translations/ca.json | 1 + .../google_travel_time/translations/en.json | 1 + .../google_travel_time/translations/et.json | 1 + .../google_travel_time/translations/pl.json | 1 + .../google_travel_time/translations/ru.json | 1 + .../components/motioneye/translations/pl.json | 25 +++++++++++ .../components/mutesync/translations/pl.json | 16 +++++++ .../components/omnilogic/translations/ca.json | 1 + .../components/omnilogic/translations/et.json | 1 + .../components/omnilogic/translations/pl.json | 1 + .../components/omnilogic/translations/ru.json | 1 + .../waze_travel_time/translations/ca.json | 1 + .../waze_travel_time/translations/en.json | 1 + .../waze_travel_time/translations/et.json | 1 + .../waze_travel_time/translations/pl.json | 1 + .../waze_travel_time/translations/ru.json | 1 + 21 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/fritz/translations/pl.json create mode 100644 homeassistant/components/motioneye/translations/pl.json create mode 100644 homeassistant/components/mutesync/translations/pl.json diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json new file mode 100644 index 00000000000..9d3f934d177 --- /dev/null +++ b/homeassistant/components/fritz/translations/pl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "Narz\u0119dzia FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wykryto FRITZ!Box: {name} \n\nSkonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 {name}", + "title": "Konfiguracja narz\u0119dzi FRITZ! Box" + }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Zaktualizuj dane logowania narz\u0119dzi FRITZ!Box dla: {host} . \n\nNarz\u0119dzia FRITZ!Box nie mo\u017ce zalogowa\u0107 si\u0119 do urz\u0105dzenia FRITZ!Box.", + "title": "Aktualizacja danych logowania narz\u0119dzi FRITZ!Box" + }, + "start_config": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ! Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.", + "title": "Konfiguracja narz\u0119dzi FRITZ!Box - obowi\u0105zkowe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index 22e229d1c7e..5fb9643d097 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -14,7 +14,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. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu router. Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu, per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del router.", + "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.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json index c71b35d54a6..4b06301953e 100644 --- a/homeassistant/components/goalzero/translations/pl.json +++ b/homeassistant/components/goalzero/translations/pl.json @@ -14,7 +14,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. Nast\u0119pnie uzyskaj adres IP hosta z routera. W ustawieniach routera nale\u017cy skonfigurowa\u0107 DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.", + "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. W ustawieniach routera nale\u017cy skonfigurowa\u0107 rezerwacj\u0119 adres\u00f3w DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index 5eee4e70e98..066c93545d6 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -14,7 +14,7 @@ "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. \u0417\u0430\u0442\u0435\u043c \u0443\u0437\u043d\u0430\u0439\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \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. \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 5c25a8cb98c..5560def5eb1 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -14,7 +14,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u60a8\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\u63a5\u8005\u7531\u8def\u7531\u5668\u53d6\u5f97\u4e3b\u6a5f\u7aef IP\uff0c \u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u88dd\u7f6e\u7684 DHCP \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\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", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json index 0edced29690..41ebdd99bc9 100644 --- a/homeassistant/components/google_travel_time/translations/ca.json +++ b/homeassistant/components/google_travel_time/translations/ca.json @@ -11,6 +11,7 @@ "data": { "api_key": "Clau API", "destination": "Destinaci\u00f3", + "name": "Nom", "origin": "Origen" }, "description": "Quan especifiquis l'origen i la destinaci\u00f3, pots proporcionar m\u00e9s d'una ubicaci\u00f3 (les has de separar pel car\u00e0cter 'pipe'); poden ser en forma d'adre\u00e7a, coordenades de latitud/longitud o un identificador de lloc de Google. En especificar la ubicaci\u00f3 mitjan\u00e7ant un ID de lloc de Google, l'identificador ha de tenir el prefix `place_id:`." diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json index 464d518a70b..b0e08c1d63d 100644 --- a/homeassistant/components/google_travel_time/translations/en.json +++ b/homeassistant/components/google_travel_time/translations/en.json @@ -11,6 +11,7 @@ "data": { "api_key": "API Key", "destination": "Destination", + "name": "Name", "origin": "Origin" }, "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`." diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json index e99472f46a3..a93451e9b67 100644 --- a/homeassistant/components/google_travel_time/translations/et.json +++ b/homeassistant/components/google_travel_time/translations/et.json @@ -11,6 +11,7 @@ "data": { "api_key": "API v\u00f5ti", "destination": "Sihtkoht", + "name": "Nimi", "origin": "L\u00e4htekoht" }, "description": "L\u00e4hte- ja sihtkoha m\u00e4\u00e4ramisel v\u00f5ib sisestada \u00fche v\u00f5i mitu eraldusm\u00e4rgiga eraldatud asukohta aadressi, laius- / pikkuskraadi koordinaatide v\u00f5i Google'i koha ID kujul. Asukoha m\u00e4\u00e4ramisel Google'i koha ID abil tuleb ID-le lisada eesliide \"place_id:\"." diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json index c420e65912f..d6c88cf8822 100644 --- a/homeassistant/components/google_travel_time/translations/pl.json +++ b/homeassistant/components/google_travel_time/translations/pl.json @@ -11,6 +11,7 @@ "data": { "api_key": "Klucz API", "destination": "Punkt docelowy", + "name": "Nazwa", "origin": "Punkt pocz\u0105tkowy" }, "description": "Okre\u015blaj\u0105c punkt pocz\u0105tkowy i docelowy, mo\u017cesz poda\u0107 jedn\u0105 lub wi\u0119cej lokalizacji oddzielonych pionow\u0105 kresk\u0105, w postaci adresu, wsp\u00f3\u0142rz\u0119dnych szeroko\u015bci / d\u0142ugo\u015bci geograficznej lub identyfikatora miejsca Google. Okre\u015blaj\u0105c lokalizacj\u0119 za pomoc\u0105 identyfikatora miejsca Google, identyfikator musi by\u0107 poprzedzony przedrostkiem \u201eplace_id:\u201d." diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json index 1198c3d62f9..1e2706de347 100644 --- a/homeassistant/components/google_travel_time/translations/ru.json +++ b/homeassistant/components/google_travel_time/translations/ru.json @@ -11,6 +11,7 @@ "data": { "api_key": "\u041a\u043b\u044e\u0447 API", "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" }, "description": "\u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043f\u0443\u043d\u043a\u0442\u043e\u0432 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0434\u043d\u043e \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0447\u0435\u0440\u0442\u043e\u0439, \u0432 \u0432\u0438\u0434\u0435 \u0430\u0434\u0440\u0435\u0441\u0430, \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b \u0438\u043b\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \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 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google, \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 `place_id:`." diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json new file mode 100644 index 00000000000..dca40bdcd3d --- /dev/null +++ b/homeassistant/components/motioneye/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_url": "Nieprawid\u0142owy adres URL", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "admin_password": "Has\u0142o admina", + "admin_username": "Nazwa u\u017cytkownika admina", + "surveillance_password": "Has\u0142o podgl\u0105du", + "surveillance_username": "[%key::common::config_flow::data::username%] podgl\u0105du", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/pl.json b/homeassistant/components/mutesync/translations/pl.json new file mode 100644 index 00000000000..dbc143f8504 --- /dev/null +++ b/homeassistant/components/mutesync/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "W\u0142\u0105cz uwierzytelnianie w Preferencjach m\u00fctesync > Uwierzytelnianie", + "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/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json index b460e47f2b8..7425fbcead6 100644 --- a/homeassistant/components/omnilogic/translations/ca.json +++ b/homeassistant/components/omnilogic/translations/ca.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Compensaci\u00f3 de pH (positiu o negatiu)", "polling_interval": "Interval d'escaneig (segons)" } } diff --git a/homeassistant/components/omnilogic/translations/et.json b/homeassistant/components/omnilogic/translations/et.json index c9a06f01117..d9803ffd352 100644 --- a/homeassistant/components/omnilogic/translations/et.json +++ b/homeassistant/components/omnilogic/translations/et.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH nihe (positiivne v\u00f5i negatiivne)", "polling_interval": "P\u00e4ringute intervall (sekundites)" } } diff --git a/homeassistant/components/omnilogic/translations/pl.json b/homeassistant/components/omnilogic/translations/pl.json index 5fafc963760..f0fadfbfd6a 100644 --- a/homeassistant/components/omnilogic/translations/pl.json +++ b/homeassistant/components/omnilogic/translations/pl.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "przesuni\u0119cie pH (dodatnie lub ujemne)", "polling_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w sekundach)" } } diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 5b00efefa1a..51111556fb3 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "\u0421\u043c\u0435\u0449\u0435\u043d\u0438\u0435 pH (\u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435)", "polling_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } diff --git a/homeassistant/components/waze_travel_time/translations/ca.json b/homeassistant/components/waze_travel_time/translations/ca.json index f8f1db711c0..35e25e07808 100644 --- a/homeassistant/components/waze_travel_time/translations/ca.json +++ b/homeassistant/components/waze_travel_time/translations/ca.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destinaci\u00f3", + "name": "Nom", "origin": "Origen", "region": "Regi\u00f3" }, diff --git a/homeassistant/components/waze_travel_time/translations/en.json b/homeassistant/components/waze_travel_time/translations/en.json index 31fd8d0793f..1533db74a51 100644 --- a/homeassistant/components/waze_travel_time/translations/en.json +++ b/homeassistant/components/waze_travel_time/translations/en.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destination", + "name": "Name", "origin": "Origin", "region": "Region" }, diff --git a/homeassistant/components/waze_travel_time/translations/et.json b/homeassistant/components/waze_travel_time/translations/et.json index a2ff51e1bb9..1ce25b87208 100644 --- a/homeassistant/components/waze_travel_time/translations/et.json +++ b/homeassistant/components/waze_travel_time/translations/et.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Sihtkoht", + "name": "Nimi", "origin": "L\u00e4htekoht", "region": "Piirkond" }, diff --git a/homeassistant/components/waze_travel_time/translations/pl.json b/homeassistant/components/waze_travel_time/translations/pl.json index e46469f59d7..5243e5c258b 100644 --- a/homeassistant/components/waze_travel_time/translations/pl.json +++ b/homeassistant/components/waze_travel_time/translations/pl.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Punkt docelowy", + "name": "Nazwa", "origin": "Punkt pocz\u0105tkowy", "region": "Region" }, diff --git a/homeassistant/components/waze_travel_time/translations/ru.json b/homeassistant/components/waze_travel_time/translations/ru.json index 5d0c0990c28..90cb1655b07 100644 --- a/homeassistant/components/waze_travel_time/translations/ru.json +++ b/homeassistant/components/waze_travel_time/translations/ru.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, From 1c75d5cb94a6573a102fa9485240b9af3ed3dda1 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 1 May 2021 17:04:37 -0700 Subject: [PATCH 042/140] 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 7b3060c5072..bb0b434ae48 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, @@ -36,6 +40,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.""" @@ -47,10 +52,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: @@ -82,6 +84,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 @@ -147,26 +150,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 74d7efb3738..d52ca51c291 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 0cc701db9e5..fc3a0095b9b 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 9828a58d9a46fdfe839d9b8878284cd607b77f4b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 May 2021 17:43:03 +0300 Subject: [PATCH 043/140] 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 c2df82b04436791fc23c4cbd7aeeda1f0c9d76c7 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 May 2021 16:00:40 +0300 Subject: [PATCH 044/140] 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 bc88f6af09b3ae237c5615f32b074bfa773c4137 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 1 May 2021 15:55:04 -0400 Subject: [PATCH 045/140] 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 d52ca51c291..eecbb763440 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 fc3a0095b9b..40c157cae0e 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 04ccbb92c12bc4bc2632d392ecb6a6d904de277c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 1 May 2021 15:18:36 -0600 Subject: [PATCH 046/140] 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 eecbb763440..c621676f5ea 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 40c157cae0e..f532fc54cb4 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 21a5d5959c7e00ab4f4e84cfa7f7d7dd2d7a2f14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 May 2021 12:26:10 -1000 Subject: [PATCH 047/140] 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 a3aa499c128..d31881959b3 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 c621676f5ea..76b7244b24a 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 f532fc54cb4..58355f4a610 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 d927932c47deb7ebae519183a4a7e3e440a3b69e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 1 May 2021 17:56:50 -0600 Subject: [PATCH 048/140] 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 2f58b7d950264a3ad852f9e900fa7c3c07931e55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 May 2021 17:25:34 -0700 Subject: [PATCH 049/140] Bumped version to 2021.5.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0f694f097c2..bdb3d30bdab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 2a5adce0b9644d32fb59455b3a7a6e69e6bd2e64 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 May 2021 06:07:26 +0200 Subject: [PATCH 050/140] 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 b17b664c0b7a976dba28dba7d17715bbfed9379a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 May 2021 20:30:28 -0700 Subject: [PATCH 051/140] 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 617e47cfa86cb0c240b181708575dfcf7819e3ee Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 3 May 2021 06:58:14 +0300 Subject: [PATCH 052/140] 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 8d2f6bd363858bf486a90a9c08f1ab60f08fdef3 Mon Sep 17 00:00:00 2001 From: Florian Gareis Date: Mon, 3 May 2021 00:05:40 +0200 Subject: [PATCH 053/140] 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 76b7244b24a..0c43f783303 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 58355f4a610..ced9ecd8209 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 cd05195155842307def0fd47c193c659a5140639 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 May 2021 17:57:42 -1000 Subject: [PATCH 054/140] 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 5e2d30a1707e80699b570a69d62b2f1a742a6898 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 May 2021 21:48:21 -0700 Subject: [PATCH 055/140] Bumped version to 2021.5.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bdb3d30bdab..406acf79736 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 3681363418955e1ce8650d9d8522fb3b1f4d175c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 3 May 2021 09:49:13 +0300 Subject: [PATCH 056/140] 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 33fa6cd79420ec9a13cc5e49458a4936b2f1c8bb Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 3 May 2021 11:12:06 +0200 Subject: [PATCH 057/140] 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 4273a3d63fe10d2bc8d128e7b909194222c9f685 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 May 2021 07:11:57 +0200 Subject: [PATCH 058/140] 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 0a4856b88b156267a3dfce6650e9190af7b56261 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 059/140] 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 40f3bbe005896026f7377616ca95e30e197ea0e9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 14:57:11 +0200 Subject: [PATCH 060/140] 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 9e8d01893f4d8abb63d3af3c82106bac18685d21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:40:01 +0200 Subject: [PATCH 061/140] 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 756166c86a6..8387c75772d 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 5f0281f0bae5203342665c0a41e7c314c5ca9417 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:41:59 +0200 Subject: [PATCH 062/140] 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 faff6b8e2f9..02bb6e0d777 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 c3f757c1ae4c2f9f5bfd4d7b621abeb205680ab8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:41:16 +0200 Subject: [PATCH 063/140] 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 54d6b0fa7d1..847763e095b 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 b8b3e8f879e40158e248a6e125525445576a4ae1 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 064/140] 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 5679fbf2197dc0c8c58f84129d6d3f57a5959105 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 May 2021 18:40:49 +0200 Subject: [PATCH 065/140] 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 aff3711cdae..d8808ba2047 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 398ebccbde907442f0382ed00f8518d2ca56cd6f 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 066/140] 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 0c43f783303..1db9ecba1cb 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 ced9ecd8209..d56e413007e 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 88b2425059720a9738c1b15d9973fbc1dfa0b25a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 May 2021 18:32:45 +0200 Subject: [PATCH 067/140] 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 1db9ecba1cb..57402eabc25 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 d56e413007e..13c1307c8fe 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 46a3b26b674e51c0bfbbb6e98424fbc107cb2933 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 May 2021 10:34:45 -0700 Subject: [PATCH 068/140] Bumped version to 2021.5.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 406acf79736..97352a52ee4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0b7" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 14e02b1bcaf24481ddfc90c1bf3eeaf7acbcfb26 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 May 2021 14:23:22 -0700 Subject: [PATCH 069/140] 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 fa2a98fc96031676b029a549b2da159ac2a7e9f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 May 2021 20:54:31 -1000 Subject: [PATCH 070/140] 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 73d144f6b0c..b179f480724 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -486,7 +486,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 1b3489b10750c5fcc0998f168684f614c1749cdb Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 4 May 2021 23:45:25 +0200 Subject: [PATCH 071/140] 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 b059c86e8a0..de87127f489 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -68,7 +68,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) @@ -81,7 +98,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 445deec645de105d93b7c0f0ef8e8204ed274f78 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 4 May 2021 19:10:28 +0300 Subject: [PATCH 072/140] 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 b6385cc4b344ce0e2efd485ce544c59f6acc10e6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 May 2021 00:11:03 +0200 Subject: [PATCH 073/140] 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 57402eabc25..d417ff10b94 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 13c1307c8fe..9142235f14d 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 bb7b6e38e4f0e4e781727f9a74c2cf84260fe31b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 May 2021 15:12:30 -0700 Subject: [PATCH 074/140] Bumped version to 2021.5.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 97352a52ee4..9c59b18f77f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b7" +PATCH_VERSION = "0b8" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From d87c9c6ffe30fb1ed2ce2abf572fd5e4df2f27e8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 May 2021 13:04:37 +0200 Subject: [PATCH 075/140] 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 4b7100614bba72129bf3fdf591d61b06ade2255f Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Wed, 5 May 2021 09:19:51 +0200 Subject: [PATCH 076/140] 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 1b9f7cdacfcb70e085689b94c1e6b6714d7e65ca Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 4 May 2021 22:58:20 -0700 Subject: [PATCH 077/140] 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 d417ff10b94..6a55af80d8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -954,7 +954,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 9142235f14d..9cc4476781f 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 d7d32cff9550242280bc6b3c12e2dda3e58a277c Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 5 May 2021 17:13:06 +0200 Subject: [PATCH 078/140] 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 418606d634e9830769020ae037b5468d6d28c5ee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 May 2021 17:59:26 +0200 Subject: [PATCH 079/140] 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 3654d22af58d04acbbd471af5cace992882aeca7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 May 2021 18:17:06 +0200 Subject: [PATCH 080/140] Bumped version to 2021.5.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9c59b18f77f..f7ffd77bcbf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0b8" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 66c9658cc70ebb3881f40fa4d12b6287bea99152 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 7 May 2021 13:37:38 +0200 Subject: [PATCH 081/140] 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 ca9a68535fdcddd35296b4df051b58641cd5979d Mon Sep 17 00:00:00 2001 From: Sezer K Date: Fri, 7 May 2021 07:29:37 +0200 Subject: [PATCH 082/140] 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 2aafc88ab52f81cf513d76e3cd1f2f4d02a095a4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 7 May 2021 05:23:46 +0200 Subject: [PATCH 083/140] 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 14520f0ddaf..e745c89f5c8 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 6a55af80d8f..c58d6701c04 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 9cc4476781f..c317612837e 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 74b958df82c7deeab752e1de5f1242cb4f942525 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Wed, 5 May 2021 23:15:21 -0400 Subject: [PATCH 084/140] 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 c75d99c883fc3716c8a2cf2f8d85f46c71b5d0ea 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 085/140] 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 a7ef642721ee934a49c998c4d229b8ed6f61f80f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 7 May 2021 13:22:08 +0200 Subject: [PATCH 086/140] 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 70a7339279072970fdd740a3132f866edce707eb Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 6 May 2021 02:19:52 +0200 Subject: [PATCH 087/140] 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 c58d6701c04..f444a676ee3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1819,7 +1819,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 c317612837e..bc37f9f5283 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 d1a75b2fe023a97c55d35017590db7c6255e7236 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 May 2021 22:08:48 -0500 Subject: [PATCH 088/140] 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 f444a676ee3..c67261af31b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2142,7 +2142,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 bc37f9f5283..c9c7b445f38 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 bb3ebad92f5b0e88474900cbc9554d1fada05b0a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 6 May 2021 16:43:14 +0200 Subject: [PATCH 089/140] 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 1e604fec7d996d58dd4126ae0c18dec8c873cdc2 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 090/140] 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 b3a5be0a1ca..c39862f9bab 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -256,7 +256,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 21e512a2229..44e8d15bff6 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -36,7 +36,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", connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", @@ -65,7 +64,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", connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", From 27e0c0bbc8ac77d36cb6eca671b6e048be02e5a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 May 2021 07:34:51 +0200 Subject: [PATCH 091/140] 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 df740a7a182af093e79ac7188946077d1fa199a6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 7 May 2021 02:59:03 +0200 Subject: [PATCH 092/140] 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 5c6b9c7e37b8cf7c661a8cf6ccfabf76dd8a5ef2 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 093/140] 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 50e051813c4..56812c9b2af 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 c67261af31b..0d224f074e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1322,7 +1322,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 c9c7b445f38..1ab9ad00cbe 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 cf7f27750719b7a56b383d19ac80ecede0dc8e06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 May 2021 19:58:44 -0500 Subject: [PATCH 094/140] 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 b144550a424cfe426cf26cc71db4f4f435d09a0d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 6 May 2021 20:03:35 -0600 Subject: [PATCH 095/140] 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 ead7a90f9a413405df790a26dfd5136b4e0f5376 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 May 2021 05:24:47 -0700 Subject: [PATCH 096/140] 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 0d224f074e9..862119fd519 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 1ab9ad00cbe..15f6ce2aa97 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 193a0dfc02417fce15219f825359a8d380394b02 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 7 May 2021 08:31:16 -0400 Subject: [PATCH 097/140] 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 a9bb905f053dc528e79324a5d232ca7fae469aaa Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 7 May 2021 15:10:46 +0200 Subject: [PATCH 098/140] 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 9993b4efac2..3b36e3089a2 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 dfc5d22c9bdbc26e35f1f1e352323f16c73f686b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 May 2021 15:19:11 +0200 Subject: [PATCH 099/140] Bumped version to 2021.5.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f7ffd77bcbf..56fe2b36e57 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 1e072b73f75635e5d78131262b43ccdd24f4cee5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 May 2021 17:08:46 +0200 Subject: [PATCH 100/140] 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 dabd398c6da1041f9c9551289b34f0b6b200d6a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 29 Apr 2021 10:45:17 +0200 Subject: [PATCH 101/140] 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 22e62fa61ab380ee4295ca929b45cd72909c9e8a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 2 May 2021 00:03:52 +0200 Subject: [PATCH 102/140] 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 e482827975b58fe2ae9d0d246617fd5378716a28 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 103/140] 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 1d5716b505615cb945fa756245dc993d73f77eab Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Mon, 10 May 2021 19:49:08 +0200 Subject: [PATCH 104/140] 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 83db35d2d6d7cf3cdb9224f07be4e87eba3bb5e3 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 105/140] 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 690d9ed63a4..70fb051ce30 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 2d3b8fb32998e4750760577388ee74ff3745be80 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 9 May 2021 04:09:56 -0500 Subject: [PATCH 106/140] 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 989cee662daccea84a27979126c654f1e0cc1c91 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 8 May 2021 04:05:09 +0100 Subject: [PATCH 107/140] 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 862119fd519..833b749a2da 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 15f6ce2aa97..aa64973cd08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -576,7 +576,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 0a1439d16bbf159b0f75ab227182186a289bd3e8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 8 May 2021 17:53:08 +0200 Subject: [PATCH 108/140] 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 833b749a2da..9bb6b9550c6 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 aa64973cd08..c415382f18a 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 7acefbefb3d1fc92c5ade11175e3f20e5d4dc3b9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 8 May 2021 20:33:55 +0200 Subject: [PATCH 109/140] 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 9bb6b9550c6..7a32917366b 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 c415382f18a..b60373e37b0 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 03b786268eccf850e919e71acda68afc1cd186f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 May 2021 12:20:22 -0500 Subject: [PATCH 110/140] 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 e35fbde5fb12af1eff014bd79ef667d87c06dc16 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 May 2021 19:37:09 +0200 Subject: [PATCH 111/140] 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 3362ac24f622f25bcf855cfa9547cd2fff7772fd Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 8 May 2021 23:09:31 -0400 Subject: [PATCH 112/140] 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 41b253d97ee..fd17699f592 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 0ce3bf08594d69d05aa1371aa6e3730bbc5b8b1c Mon Sep 17 00:00:00 2001 From: Colin Robbins Date: Mon, 10 May 2021 18:48:00 +0100 Subject: [PATCH 113/140] 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 70fd8275bd7..bfaf7231945 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -203,7 +203,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] @@ -225,7 +225,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) @@ -236,7 +238,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.""" @@ -245,11 +247,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 @@ -295,7 +297,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: @@ -304,6 +306,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: @@ -353,7 +356,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 420161039e8f51b704219489def06f6dd3a98960 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 9 May 2021 16:52:24 -0700 Subject: [PATCH 114/140] 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 7a32917366b..fc6d7fb9f45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -254,7 +254,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 b60373e37b0..a512bc7261a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,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 0dd247aed06f607302d44aba27ad8eba3dcf70ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 9 May 2021 21:57:27 +0200 Subject: [PATCH 115/140] 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 fc6d7fb9f45..8dfc4e88617 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 a512bc7261a..c4dda2d537e 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 51c78e00855be9fc67135e4f82e0ca658303f6cc Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 9 May 2021 14:30:38 -0700 Subject: [PATCH 116/140] 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 bb0b434ae48..6ec696855cd 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -150,7 +150,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 6f9c21e1c0a096cd94b41ed8955640958a303273 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 10 May 2021 07:49:11 -0500 Subject: [PATCH 117/140] 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 b179f480724..11f59f2f432 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -498,7 +498,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 a670156352b378bc0518fa75268bc66f48792362 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 10 May 2021 10:56:18 -0500 Subject: [PATCH 118/140] 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 d31881959b3..a87280aaabd 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 8dfc4e88617..1e3328a6c3a 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 c4dda2d537e..f199871ff9f 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.44 +pysonos==0.0.45 # homeassistant.components.spc pyspcwebgw==0.4.0 From 88574034b75d4a24005fa59b16d60e0a7960d2f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 May 2021 10:50:07 -0700 Subject: [PATCH 119/140] Bumped version to 2021.5.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 56fe2b36e57..51f762ad026 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 2c492d71f7fe38319b46093bd2c684b6a66c43ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 May 2021 23:55:10 -0500 Subject: [PATCH 120/140] Handle transport errors when updating media via events (#50480) Co-authored-by: Jason Lawrence --- .../components/sonos/media_player.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 11f59f2f432..f1780920336 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -498,9 +498,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if new_status == "TRANSITIONING": return - self._play_mode = ( - variables["current_play_mode"] if variables else self.soco.play_mode - ) + if variables and "transport_state" in variables: + self._play_mode = variables["current_play_mode"] + track_uri = variables["current_track_uri"] + music_source = self.soco.music_source_from_uri(track_uri) + else: + self._play_mode = self.soco.play_mode + music_source = self.soco.music_source + self._uri = None self._media_duration = None self._media_image_url = None @@ -514,13 +519,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): 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: @@ -556,7 +554,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self._media_title = source self._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._clear_media_position() From 17c1031973a96ed267797c90df5244dfb81c05e9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 11 May 2021 23:55:32 -0500 Subject: [PATCH 121/140] Hotfix for Sonos favorites race condition (#50495) --- homeassistant/components/sonos/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f1780920336..022beef3219 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -454,7 +454,8 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_reconnect_player(self) -> None: """Set basic information when player is reconnected.""" - await self.hass.async_add_executor_job(self._reconnect_player) + async with self.data.topology_condition: + await self.hass.async_add_executor_job(self._reconnect_player) def _reconnect_player(self) -> None: """Set basic information when player is reconnected.""" From 99044f5454c6e11ca73a049d600e0e778b6f1f31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 May 2021 07:27:11 +0200 Subject: [PATCH 122/140] 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 8a3514ed44f9e13e04d34f6f27124de3fa8f5b3e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 12 May 2021 00:09:22 +0200 Subject: [PATCH 123/140] 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 1e3328a6c3a..c8951f78950 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.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 f199871ff9f..52c9d3de192 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.7 +denonavr==0.10.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 From c477c311902a9eb2b74eed384c03118e1b2dabfb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 May 2021 22:15:36 -0700 Subject: [PATCH 124/140] 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 c8951f78950..263c6dc8e1d 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 52c9d3de192..8543f74a4f8 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 421e1b99566190ee8b007750654c75e858ebd4aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 May 2021 22:27:42 -0700 Subject: [PATCH 125/140] Bumped version to 2021.5.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 51f762ad026..f246d7407fa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From c52a33478e8b450d29bdea67ed43e663484c9504 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 13 May 2021 22:32:34 -0500 Subject: [PATCH 126/140] Fix Sonos favorites race condition v2 (#50575) --- homeassistant/components/sonos/media_player.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 022beef3219..230948ee894 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -440,6 +440,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self._media_position = None self._media_position_updated_at = None + async def async_set_favorites(self) -> None: + """Wrap favorites update method with an async lock.""" + async with self.data.topology_condition: + await self.hass.async_add_executor_job(self._set_favorites) + def _set_favorites(self) -> None: """Set available favorites.""" self._favorites = [] @@ -741,7 +746,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): 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.hass.async_add_job(self.async_set_favorites) self.async_write_ha_state() @property From d8379c57b41dd468853aaae2f117094ec9bcb7cd Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 10 May 2021 20:40:44 +0200 Subject: [PATCH 127/140] 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 3b36e3089a2..dcf4f1bc765 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 8e3894a441d20f941362298b415524f4dd21f68e Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Mon, 10 May 2021 20:35:32 +0200 Subject: [PATCH 128/140] 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 263c6dc8e1d..98a8807d343 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 8543f74a4f8..697caf50685 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 3e8d840905533a9df408e9729bba1fad5578b8cc Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 12 May 2021 13:40:10 -0500 Subject: [PATCH 129/140] 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 03cce67e4d8..56eba68abe1 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -96,7 +96,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 0b5a077d55cab4d6a173e853872f8d601e3ac9a2 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 130/140] 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 98a8807d343..af2e557446a 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 697caf50685..91f7c1422ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -778,7 +778,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 728bead3b72c78221817a6ba03d92e7544653889 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 May 2021 22:58:37 +0200 Subject: [PATCH 131/140] 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 57d8e7483d84c927d45442a2664d7ebbdabb38d2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 14 May 2021 15:23:16 -0600 Subject: [PATCH 132/140] 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 2a949cff0e70231938f58c2305c3744d7d743e0d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 May 2021 23:23:29 +0200 Subject: [PATCH 133/140] 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 77a6d7317f495891942de9eaa1d565516a5723a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 May 2021 14:25:39 -0700 Subject: [PATCH 134/140] Bumped version to 2021.5.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f246d7407fa..6df593e984b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From bcd022ff34b8da0e6bbb35c8a29de8d7127738af Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 May 2021 12:48:00 +0200 Subject: [PATCH 135/140] Upgrade freesms to 0.2.0 (#50853) --- homeassistant/components/free_mobile/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index ea6ea921a38..7fb7f998643 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -2,7 +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" } diff --git a/requirements_all.txt b/requirements_all.txt index af2e557446a..63457206d6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -618,7 +618,7 @@ fortiosapi==0.10.8 freebox-api==0.0.10 # homeassistant.components.free_mobile -freesms==0.1.2 +freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor From b86cb7a072c7d99a9d41cd17ea7bd8c17613610d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 19 May 2021 09:47:43 -0500 Subject: [PATCH 136/140] Backport Sonos handle subscription failures (#50796) --- homeassistant/components/sonos/speaker.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 56eba68abe1..f090a7062e5 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -159,8 +159,9 @@ 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=1200) subscription.callback = sub_callback + subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) @callback @@ -241,11 +242,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 9a8317db1de760c86415535334319cd990fd44b2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 15 May 2021 19:36:08 +0200 Subject: [PATCH 137/140] 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 63457206d6e..2046d448902 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.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 91f7c1422ee..ca520da3349 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.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 74e8d2a5e59..44f57581694 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 7603bf81453a5f7b45445e38da157309973c6458 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 17 May 2021 16:06:13 -0500 Subject: [PATCH 138/140] 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 a87280aaabd..c117d262cbf 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.47"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 2046d448902..b9e8ddf9b90 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.47 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca520da3349..c3777345e40 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.45 +pysonos==0.0.47 # homeassistant.components.spc pyspcwebgw==0.4.0 From f141ed6977663c2aba303a3d7c0d2c5f8b136e2d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 19 May 2021 08:47:06 +0200 Subject: [PATCH 139/140] 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 dcf4f1bc765..dfc524bf16c 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 b9e8ddf9b90..9d227d63288 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1286,7 +1286,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 c3777345e40..dedf01de7ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,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 163d2788b627407f51d40c16fdc98c6473e3b0df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 May 2021 16:54:05 +0200 Subject: [PATCH 140/140] Bumped version to 2021.5.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6df593e984b..bd802526e0a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0)