From 8b04c676ace77acd6d30dcf2cb080a0051192f28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Mar 2022 09:50:21 -1000 Subject: [PATCH 0001/1224] Fix typing on recorder.history (#68917) --- homeassistant/components/recorder/history.py | 40 +++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 82a74c36a83..c5f50399b81 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -7,7 +7,7 @@ from datetime import datetime from itertools import groupby import logging import time -from typing import Any +from typing import Any, cast from sqlalchemy import Column, Text, and_, bindparam, func, or_ from sqlalchemy.ext import baked @@ -262,7 +262,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: +) -> MutableMapping[str, Iterable[LazyState]]: """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: baked_query, join_attributes = bake_query_and_join_attributes( @@ -302,19 +302,22 @@ def state_changes_during_period( entity_ids = [entity_id] if entity_id is not None else None - return _sorted_states_to_dict( - hass, - session, - states, - start_time, - entity_ids, - include_start_time_state=include_start_time_state, + return cast( + MutableMapping[str, Iterable[LazyState]], + _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + include_start_time_state=include_start_time_state, + ), ) def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: +) -> MutableMapping[str, Iterable[LazyState]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() @@ -345,13 +348,16 @@ def get_last_state_changes( entity_ids = [entity_id] if entity_id is not None else None - return _sorted_states_to_dict( - hass, - session, - reversed(states), - start_time, - entity_ids, - include_start_time_state=False, + return cast( + MutableMapping[str, Iterable[LazyState]], + _sorted_states_to_dict( + hass, + session, + reversed(states), + start_time, + entity_ids, + include_start_time_state=False, + ), ) From c4a2204cc777df440cc5d9c3b9671ebf4e8812c3 Mon Sep 17 00:00:00 2001 From: rianadon Date: Wed, 30 Mar 2022 13:49:28 -0700 Subject: [PATCH 0002/1224] Calculate temperature precision based on user units (#59560) * Calculate temperature precision based on user units * Fix a few more failing tests * Fix failing test Co-authored-by: Erik --- homeassistant/components/weather/__init__.py | 2 +- tests/components/climacell/test_weather.py | 62 ++++++++++---------- tests/components/demo/test_weather.py | 2 +- tests/components/nws/const.py | 4 +- tests/components/tomorrowio/test_weather.py | 58 +++++++++--------- 5 files changed, 65 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 15250059fec..2e0f8912867 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -198,7 +198,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self.temperature_unit == TEMP_CELSIUS + if self.hass.config.units.temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index d5385b6cfd5..3c02f6b9b1f 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -94,127 +94,127 @@ async def test_v3_weather( ATTR_FORECAST_TIME: "2021-03-07T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 7, - ATTR_FORECAST_TEMP_LOW: -5, + ATTR_FORECAST_TEMP: 7.2, + ATTR_FORECAST_TEMP_LOW: -4.7, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-08T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 10, - ATTR_FORECAST_TEMP_LOW: -4, + ATTR_FORECAST_TEMP: 9.7, + ATTR_FORECAST_TEMP_LOW: -4.0, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-09T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_TEMP: 19.4, + ATTR_FORECAST_TEMP_LOW: -0.3, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-10T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 18.5, + ATTR_FORECAST_TEMP_LOW: 3.0, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-11T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 9, + ATTR_FORECAST_TEMP: 19.7, + ATTR_FORECAST_TEMP_LOW: 9.3, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0.0457, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 12, + ATTR_FORECAST_TEMP: 19.9, + ATTR_FORECAST_TEMP_LOW: 12.1, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-13T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 16, - ATTR_FORECAST_TEMP_LOW: 7, + ATTR_FORECAST_TEMP: 15.8, + ATTR_FORECAST_TEMP_LOW: 7.5, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 1.0744, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 6.4, + ATTR_FORECAST_TEMP_LOW: 3.2, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts ATTR_FORECAST_PRECIPITATION: 7.3050, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 1, - ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_TEMP: 1.2, + ATTR_FORECAST_TEMP_LOW: 0.2, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.0051, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -2, + ATTR_FORECAST_TEMP: 6.1, + ATTR_FORECAST_TEMP_LOW: -1.6, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-17T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11, - ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_TEMP: 11.3, + ATTR_FORECAST_TEMP_LOW: 1.3, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-18T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 6, + ATTR_FORECAST_TEMP: 12.3, + ATTR_FORECAST_TEMP_LOW: 5.6, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.1778, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, - ATTR_FORECAST_TEMP: 9, - ATTR_FORECAST_TEMP_LOW: 5, + ATTR_FORECAST_TEMP: 9.4, + ATTR_FORECAST_TEMP_LOW: 4.7, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 1.2319, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 5, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 5.0, + ATTR_FORECAST_TEMP_LOW: 3.1, }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.0432, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, - ATTR_FORECAST_TEMP: 7, - ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_TEMP: 6.8, + ATTR_FORECAST_TEMP_LOW: 0.9, }, ] assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.1246 - assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.6 assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289 diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index c4ae8fcd79c..db3f3441df1 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -69,4 +69,4 @@ async def test_temperature_convert(hass): assert state.state == "rainy" data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == -24 + assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4 diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index c7387d4bf8c..dcf591b83ae 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -157,7 +157,9 @@ EXPECTED_FORECAST_IMPERIAL = { EXPECTED_FORECAST_METRIC = { ATTR_FORECAST_CONDITION: ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", - ATTR_FORECAST_TEMP: round(convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS)), + ATTR_FORECAST_TEMP: round( + convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS), 1 + ), ATTR_FORECAST_WIND_SPEED: round( convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR) ), diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index f47e8ed22d8..3e5eb81edec 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -99,8 +99,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 8, - ATTR_FORECAST_TEMP_LOW: -3, + ATTR_FORECAST_TEMP: 7.7, + ATTR_FORECAST_TEMP_LOW: -3.3, ATTR_FORECAST_WIND_BEARING: 239.6, ATTR_FORECAST_WIND_SPEED: 4.24, }, @@ -109,8 +109,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 10, - ATTR_FORECAST_TEMP_LOW: -3, + ATTR_FORECAST_TEMP: 9.7, + ATTR_FORECAST_TEMP_LOW: -3.2, ATTR_FORECAST_WIND_BEARING: 262.82, ATTR_FORECAST_WIND_SPEED: 3.24, }, @@ -119,8 +119,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_TEMP: 19.4, + ATTR_FORECAST_TEMP_LOW: -0.3, ATTR_FORECAST_WIND_BEARING: 229.3, ATTR_FORECAST_WIND_SPEED: 3.15, }, @@ -129,8 +129,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18, - ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_TEMP: 18.5, + ATTR_FORECAST_TEMP_LOW: 3.0, ATTR_FORECAST_WIND_BEARING: 149.91, ATTR_FORECAST_WIND_SPEED: 4.76, }, @@ -139,8 +139,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 9, + ATTR_FORECAST_TEMP: 19.0, + ATTR_FORECAST_TEMP_LOW: 9.0, ATTR_FORECAST_WIND_BEARING: 210.45, ATTR_FORECAST_WIND_SPEED: 7.01, }, @@ -149,8 +149,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0.12, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 12, + ATTR_FORECAST_TEMP: 19.9, + ATTR_FORECAST_TEMP_LOW: 12.1, ATTR_FORECAST_WIND_BEARING: 217.98, ATTR_FORECAST_WIND_SPEED: 5.5, }, @@ -159,8 +159,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 6, + ATTR_FORECAST_TEMP: 12.5, + ATTR_FORECAST_TEMP_LOW: 6.1, ATTR_FORECAST_WIND_BEARING: 58.79, ATTR_FORECAST_WIND_SPEED: 4.35, }, @@ -169,8 +169,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 23.96, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_TEMP: 6.1, + ATTR_FORECAST_TEMP_LOW: 0.8, ATTR_FORECAST_WIND_BEARING: 70.25, ATTR_FORECAST_WIND_SPEED: 7.26, }, @@ -179,8 +179,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 1.46, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -1, + ATTR_FORECAST_TEMP: 6.5, + ATTR_FORECAST_TEMP_LOW: -1.5, ATTR_FORECAST_WIND_BEARING: 84.47, ATTR_FORECAST_WIND_SPEED: 7.1, }, @@ -189,8 +189,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -2, + ATTR_FORECAST_TEMP: 6.1, + ATTR_FORECAST_TEMP_LOW: -1.6, ATTR_FORECAST_WIND_BEARING: 103.85, ATTR_FORECAST_WIND_SPEED: 3.0, }, @@ -199,8 +199,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11, - ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_TEMP: 11.3, + ATTR_FORECAST_TEMP_LOW: 1.3, ATTR_FORECAST_WIND_BEARING: 145.41, ATTR_FORECAST_WIND_SPEED: 3.25, }, @@ -209,8 +209,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 5, + ATTR_FORECAST_TEMP: 12.3, + ATTR_FORECAST_TEMP_LOW: 5.2, ATTR_FORECAST_WIND_BEARING: 62.99, ATTR_FORECAST_WIND_SPEED: 2.94, }, @@ -219,8 +219,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 2.93, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 9, - ATTR_FORECAST_TEMP_LOW: 4, + ATTR_FORECAST_TEMP: 9.4, + ATTR_FORECAST_TEMP_LOW: 4.1, ATTR_FORECAST_WIND_BEARING: 68.54, ATTR_FORECAST_WIND_SPEED: 6.22, }, @@ -229,8 +229,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00", ATTR_FORECAST_PRECIPITATION: 1.22, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3, - ATTR_FORECAST_TEMP: 5, - ATTR_FORECAST_TEMP_LOW: 2, + ATTR_FORECAST_TEMP: 4.5, + ATTR_FORECAST_TEMP_LOW: 1.7, ATTR_FORECAST_WIND_BEARING: 56.98, ATTR_FORECAST_WIND_SPEED: 7.76, }, @@ -239,7 +239,7 @@ async def test_v4_weather(hass: HomeAssistant) -> None: assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 102776.91 - assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.7 assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.12 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 4.17 From 423ecfa69d45b322e197530b61a6147d67e5704d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Mar 2022 13:50:13 -0700 Subject: [PATCH 0003/1224] Mark all input integrations as helpers (#68922) --- homeassistant/components/input_boolean/manifest.json | 1 + homeassistant/components/input_button/manifest.json | 1 + homeassistant/components/input_datetime/manifest.json | 1 + homeassistant/components/input_number/manifest.json | 1 + homeassistant/components/input_select/manifest.json | 1 + homeassistant/components/input_text/manifest.json | 1 + 6 files changed, 6 insertions(+) diff --git a/homeassistant/components/input_boolean/manifest.json b/homeassistant/components/input_boolean/manifest.json index 7a27d475e6e..589cf536253 100644 --- a/homeassistant/components/input_boolean/manifest.json +++ b/homeassistant/components/input_boolean/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_boolean", + "integration_type": "helper", "name": "Input Boolean", "documentation": "https://www.home-assistant.io/integrations/input_boolean", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_button/manifest.json b/homeassistant/components/input_button/manifest.json index 76133500d36..7e31df775c3 100644 --- a/homeassistant/components/input_button/manifest.json +++ b/homeassistant/components/input_button/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_button", + "integration_type": "helper", "name": "Input Button", "documentation": "https://www.home-assistant.io/integrations/input_button", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_datetime/manifest.json b/homeassistant/components/input_datetime/manifest.json index a394b77b72e..4d1e680c12a 100644 --- a/homeassistant/components/input_datetime/manifest.json +++ b/homeassistant/components/input_datetime/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_datetime", + "integration_type": "helper", "name": "Input Datetime", "documentation": "https://www.home-assistant.io/integrations/input_datetime", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_number/manifest.json b/homeassistant/components/input_number/manifest.json index 93081a7ed49..46cae513fd2 100644 --- a/homeassistant/components/input_number/manifest.json +++ b/homeassistant/components/input_number/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_number", + "integration_type": "helper", "name": "Input Number", "documentation": "https://www.home-assistant.io/integrations/input_number", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_select/manifest.json b/homeassistant/components/input_select/manifest.json index 614ee18390d..1c3dc880d20 100644 --- a/homeassistant/components/input_select/manifest.json +++ b/homeassistant/components/input_select/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_select", + "integration_type": "helper", "name": "Input Select", "documentation": "https://www.home-assistant.io/integrations/input_select", "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/input_text/manifest.json b/homeassistant/components/input_text/manifest.json index 3ca9a0b961a..9cc48f745cf 100644 --- a/homeassistant/components/input_text/manifest.json +++ b/homeassistant/components/input_text/manifest.json @@ -1,5 +1,6 @@ { "domain": "input_text", + "integration_type": "helper", "name": "Input Text", "documentation": "https://www.home-assistant.io/integrations/input_text", "codeowners": ["@home-assistant/core"], From 217d98e008814c88771c73053e4672af2a9e1b79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Mar 2022 23:02:19 +0200 Subject: [PATCH 0004/1224] Bump version to 2022.5.0dev0 (#68923) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f447a83243e..a2dd5788498 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 9 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2022.4 + HA_SHORT_VERSION: 2022.5 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index fabdac85736..c8e8cd998fa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 4 +MINOR_VERSION: Final = 5 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/setup.cfg b/setup.cfg index 9bc8bd45748..1aef468f0ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.4.0.dev0 +version = 2022.5.0.dev0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 89daf4f96b1168fcb3ff8b0ffec3bb181f977cea Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Mar 2022 23:35:12 +0200 Subject: [PATCH 0005/1224] Handle config entries of integrations that are removed (#68928) --- .../components/config/config_entries.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index f3a239b5822..64151c7d90d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -18,7 +18,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) -from homeassistant.loader import async_get_config_flows +from homeassistant.loader import Integration, async_get_config_flows async def async_setup(hass): @@ -63,19 +63,33 @@ class ConfigManagerEntryIndexView(HomeAssistantView): integrations = {} type_filter = request.query["type"] + async def load_integration( + hass: HomeAssistant, domain: str + ) -> Integration | None: + """Load integration.""" + try: + return await loader.async_get_integration(hass, domain) + except loader.IntegrationNotFound: + return None + # Fetch all the integrations so we can check their type for integration in await asyncio.gather( *( - loader.async_get_integration(hass, domain) + load_integration(hass, domain) for domain in {entry.domain for entry in entries} ) ): - integrations[integration.domain] = integration + if integration: + integrations[integration.domain] = integration entries = [ entry for entry in entries - if integrations[entry.domain].integration_type == type_filter + if (type_filter != "helper" and entry.domain not in integrations) + or ( + entry.domain in integrations + and integrations[entry.domain].integration_type == type_filter + ) ] return self.json([entry_json(entry) for entry in entries]) From f9f360c64ef1448827560472aa675fccbde04f4f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Mar 2022 23:36:47 +0200 Subject: [PATCH 0006/1224] Rename helper_config_entry_flow to schema_config_entry_flow (#68924) --- .../components/derivative/config_flow.py | 18 ++--- homeassistant/components/group/config_flow.py | 56 +++++++-------- .../components/integration/config_flow.py | 18 ++--- .../components/min_max/config_flow.py | 18 ++--- .../components/switch_as_x/config_flow.py | 14 ++-- .../components/threshold/config_flow.py | 22 +++--- homeassistant/components/tod/config_flow.py | 18 ++--- .../components/utility_meter/config_flow.py | 22 +++--- ...ry_flow.py => schema_config_entry_flow.py} | 68 +++++++++---------- .../integration/config_flow.py | 18 ++--- .../helpers/test_helper_config_entry_flow.py | 22 +++--- 11 files changed, 147 insertions(+), 147 deletions(-) rename homeassistant/helpers/{helper_config_entry_flow.py => schema_config_entry_flow.py} (86%) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index 348158ce4e0..fe6b99c3eca 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -16,10 +16,10 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import ( @@ -78,16 +78,16 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 0ba5885a3fd..8ddee492834 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -10,11 +10,11 @@ import voluptuous as vol from homeassistant.const import CONF_ENTITIES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, - HelperOptionsFlowHandler, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, + SchemaOptionsFlowHandler, entity_selector_without_own_entities, ) @@ -25,11 +25,11 @@ from .const import CONF_HIDE_MEMBERS def basic_group_options_schema( domain: str, - handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, options: dict[str, Any], ) -> vol.Schema: """Generate options schema.""" - handler = cast(HelperOptionsFlowHandler, handler) + handler = cast(SchemaOptionsFlowHandler, handler) return vol.Schema( { vol.Required(CONF_ENTITIES): entity_selector_without_own_entities( @@ -58,7 +58,7 @@ def basic_group_config_schema(domain: str) -> vol.Schema: def binary_sensor_options_schema( - handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, options: dict[str, Any], ) -> vol.Schema: """Generate options schema.""" @@ -78,7 +78,7 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend( def light_switch_options_schema( domain: str, - handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, options: dict[str, Any], ) -> vol.Schema: """Generate options schema.""" @@ -119,45 +119,45 @@ def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any] return _set_group_type -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowMenuStep(GROUP_TYPES), - "binary_sensor": HelperFlowFormStep( +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowMenuStep(GROUP_TYPES), + "binary_sensor": SchemaFlowFormStep( BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor") ), - "cover": HelperFlowFormStep( + "cover": SchemaFlowFormStep( basic_group_config_schema("cover"), set_group_type("cover") ), - "fan": HelperFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")), - "light": HelperFlowFormStep( + "fan": SchemaFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")), + "light": SchemaFlowFormStep( basic_group_config_schema("light"), set_group_type("light") ), - "lock": HelperFlowFormStep( + "lock": SchemaFlowFormStep( basic_group_config_schema("lock"), set_group_type("lock") ), - "media_player": HelperFlowFormStep( + "media_player": SchemaFlowFormStep( basic_group_config_schema("media_player"), set_group_type("media_player") ), - "switch": HelperFlowFormStep( + "switch": SchemaFlowFormStep( basic_group_config_schema("switch"), set_group_type("switch") ), } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(None, next_step=choose_options_step), - "binary_sensor": HelperFlowFormStep(binary_sensor_options_schema), - "cover": HelperFlowFormStep(partial(basic_group_options_schema, "cover")), - "fan": HelperFlowFormStep(partial(basic_group_options_schema, "fan")), - "light": HelperFlowFormStep(partial(light_switch_options_schema, "light")), - "lock": HelperFlowFormStep(partial(basic_group_options_schema, "lock")), - "media_player": HelperFlowFormStep( +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(None, next_step=choose_options_step), + "binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema), + "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), + "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), + "light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), + "lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")), + "media_player": SchemaFlowFormStep( partial(basic_group_options_schema, "media_player") ), - "switch": HelperFlowFormStep(partial(light_switch_options_schema, "switch")), + "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), } -class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for groups.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index c9e51fd4f9a..c220327e983 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -16,10 +16,10 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import ( @@ -88,16 +88,16 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Integration.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index 353a90cbf6c..64d982dec92 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -8,10 +8,10 @@ import voluptuous as vol from homeassistant.const import CONF_TYPE from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN @@ -38,16 +38,16 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Min/Max.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 4cf7a001679..a70e0a371e8 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -8,17 +8,17 @@ import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.helpers import entity_registry as er, selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, wrapped_entity_config_entry_title, ) from .const import CONF_TARGET_DOMAIN, DOMAIN -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep( +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep( vol.Schema( { vol.Required(CONF_ENTITY_ID): selector.selector( @@ -43,7 +43,7 @@ CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { } -class SwitchAsXConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config flow for Switch as X.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 35a32604334..c77d4b57115 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -8,11 +8,11 @@ import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID, CONF_NAME from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowError, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN @@ -21,7 +21,7 @@ from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, def _validate_mode(data: Any) -> Any: """Validate the threshold mode, and set limits to None if not set.""" if CONF_LOWER not in data and CONF_UPPER not in data: - raise HelperFlowError("need_lower_upper") + raise SchemaFlowError("need_lower_upper") return {CONF_LOWER: None, CONF_UPPER: None, **data} @@ -44,16 +44,16 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Threshold.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py index e4927096b69..bd1712d1db5 100644 --- a/homeassistant/components/tod/config_flow.py +++ b/homeassistant/components/tod/config_flow.py @@ -8,10 +8,10 @@ import voluptuous as vol from homeassistant.const import CONF_NAME from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import CONF_AFTER_TIME, CONF_BEFORE_TIME, DOMAIN @@ -29,16 +29,16 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Times of the Day.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index caf7d3c8d00..555ae7eb46d 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -8,11 +8,11 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowError, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import ( @@ -56,7 +56,7 @@ def _validate_config(data: Any) -> Any: try: vol.Unique()(tariffs) except vol.Invalid as exc: - raise HelperFlowError("tariffs_not_unique") from exc + raise SchemaFlowError("tariffs_not_unique") from exc return data @@ -98,16 +98,16 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_config) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_config) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Utility Meter.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py similarity index 86% rename from homeassistant/helpers/helper_config_entry_flow.py rename to homeassistant/helpers/schema_config_entry_flow.py index 62c9da48547..341ae605025 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -1,4 +1,4 @@ -"""Helpers for data entry flows for helper config entries.""" +"""Helpers for creating schema based data entry flows.""" from __future__ import annotations from abc import abstractmethod @@ -17,25 +17,25 @@ from homeassistant.data_entry_flow import FlowResult, UnknownHandler from . import entity_registry as er, selector -class HelperFlowError(Exception): +class SchemaFlowError(Exception): """Validation failed.""" @dataclass -class HelperFlowFormStep: - """Define a helper config or options flow step.""" +class SchemaFlowFormStep: + """Define a config or options flow step.""" # Optional schema for requesting and validating user input. If schema validation # fails, the step will be retried. If the schema is None, no user input is requested. schema: vol.Schema | Callable[ - [HelperConfigFlowHandler | HelperOptionsFlowHandler, dict[str, Any]], + [SchemaConfigFlowHandler | SchemaOptionsFlowHandler, dict[str, Any]], vol.Schema | None, ] | None # Optional function to validate user input. # The validate_user_input function is called if the schema validates successfully. # The validate_user_input function is passed the user input from the current step. - # The validate_user_input should raise HelperFlowError is user input is invalid. + # The validate_user_input should raise SchemaFlowError is user input is invalid. validate_user_input: Callable[[dict[str, Any]], dict[str, Any]] = lambda x: x # Optional function to identify next step. @@ -48,11 +48,11 @@ class HelperFlowFormStep: # Optional function to allow amending a form schema. # The update_form_schema function is called before async_show_form is called. The # update_form_schema function is passed the handler, which is either an instance of - # HelperConfigFlowHandler or HelperOptionsFlowHandler, the schema, and the union of + # SchemaConfigFlowHandler or SchemaOptionsFlowHandler, the schema, and the union of # config entry options and user input from previous steps. update_form_schema: Callable[ [ - HelperConfigFlowHandler | HelperOptionsFlowHandler, + SchemaConfigFlowHandler | SchemaOptionsFlowHandler, vol.Schema, dict[str, Any], ], @@ -61,20 +61,20 @@ class HelperFlowFormStep: @dataclass -class HelperFlowMenuStep: - """Define a helper config or options flow menu step.""" +class SchemaFlowMenuStep: + """Define a config or options flow menu step.""" # Menu options options: list[str] | dict[str, str] -class HelperCommonFlowHandler: - """Handle a config or options flow for helper.""" +class SchemaCommonFlowHandler: + """Handle a schema based config or options flow.""" def __init__( self, - handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, - flow: dict[str, HelperFlowFormStep | HelperFlowMenuStep], + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, + flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep], config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" @@ -86,12 +86,12 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a step.""" - if isinstance(self._flow[step_id], HelperFlowFormStep): + if isinstance(self._flow[step_id], SchemaFlowFormStep): return await self._async_form_step(step_id, user_input) return await self._async_menu_step(step_id, user_input) def _get_schema( - self, form_step: HelperFlowFormStep, options: dict[str, Any] + self, form_step: SchemaFlowFormStep, options: dict[str, Any] ) -> vol.Schema | None: if form_step.schema is None: return None @@ -103,7 +103,7 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a form step.""" - form_step: HelperFlowFormStep = cast(HelperFlowFormStep, self._flow[step_id]) + form_step: SchemaFlowFormStep = cast(SchemaFlowFormStep, self._flow[step_id]) if ( user_input is not None @@ -126,7 +126,7 @@ class HelperCommonFlowHandler: # Do extra validation of user input try: user_input = form_step.validate_user_input(user_input) - except HelperFlowError as exc: + except SchemaFlowError as exc: return self._show_next_step(step_id, exc, user_input) if user_input is not None: @@ -148,12 +148,12 @@ class HelperCommonFlowHandler: def _show_next_step( self, next_step_id: str, - error: HelperFlowError | None = None, + error: SchemaFlowError | None = None, user_input: dict[str, Any] | None = None, ) -> FlowResult: """Show form for next step.""" - form_step: HelperFlowFormStep = cast( - HelperFlowFormStep, self._flow[next_step_id] + form_step: SchemaFlowFormStep = cast( + SchemaFlowFormStep, self._flow[next_step_id] ) options = dict(self._options) @@ -195,18 +195,18 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a menu step.""" - form_step: HelperFlowMenuStep = cast(HelperFlowMenuStep, self._flow[step_id]) + form_step: SchemaFlowMenuStep = cast(SchemaFlowMenuStep, self._flow[step_id]) return self._handler.async_show_menu( step_id=step_id, menu_options=form_step.options, ) -class HelperConfigFlowHandler(config_entries.ConfigFlow): - """Handle a config flow for helper integrations.""" +class SchemaConfigFlowHandler(config_entries.ConfigFlow): + """Handle a schema based config flow.""" - config_flow: dict[str, HelperFlowFormStep | HelperFlowMenuStep] - options_flow: dict[str, HelperFlowFormStep | HelperFlowMenuStep] | None = None + config_flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] + options_flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] | None = None VERSION = 1 @@ -222,7 +222,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): if cls.options_flow is None: raise UnknownHandler - return HelperOptionsFlowHandler( + return SchemaOptionsFlowHandler( config_entry, cls.options_flow, cls.async_options_flow_finished ) @@ -235,7 +235,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): def __init__(self) -> None: """Initialize config flow.""" - self._common_handler = HelperCommonFlowHandler(self, self.config_flow, None) + self._common_handler = SchemaCommonFlowHandler(self, self.config_flow, None) @classmethod @callback @@ -250,7 +250,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Generate a step handler.""" async def _async_step( - self: HelperConfigFlowHandler, user_input: dict[str, Any] | None = None + self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a config flow step.""" # pylint: disable-next=protected-access @@ -300,8 +300,8 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): ) -class HelperOptionsFlowHandler(config_entries.OptionsFlow): - """Handle an options flow for helper integrations.""" +class SchemaOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a schema based options flow.""" def __init__( self, @@ -310,7 +310,7 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None], ) -> None: """Initialize options flow.""" - self._common_handler = HelperCommonFlowHandler(self, options_flow, config_entry) + self._common_handler = SchemaCommonFlowHandler(self, options_flow, config_entry) self.config_entry = config_entry self._async_options_flow_finished = async_options_flow_finished @@ -326,7 +326,7 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): """Generate a step handler.""" async def _async_step( - self: HelperConfigFlowHandler, user_input: dict[str, Any] | None = None + self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle an options flow step.""" # pylint: disable-next=protected-access @@ -370,7 +370,7 @@ def wrapped_entity_config_entry_title( @callback def entity_selector_without_own_entities( - handler: HelperOptionsFlowHandler, + handler: SchemaOptionsFlowHandler, entity_selector_config: dict[str, Any], ) -> vol.Schema: """Return an entity selector which excludes own entities.""" diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index cc81d0a22b1..b8a048e9dba 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -8,10 +8,10 @@ import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, ) from .const import DOMAIN @@ -30,16 +30,16 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "user": HelperFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } -class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): +class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for NEW_NAME.""" config_flow = CONFIG_FLOW diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py index dece7ace37c..46e8998c738 100644 --- a/tests/helpers/test_helper_config_entry_flow.py +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -1,14 +1,14 @@ -"""Test helper_config_entry_flow.""" +"""Test schema_config_entry_flow.""" import pytest import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.helper_config_entry_flow import ( - HelperConfigFlowHandler, - HelperFlowFormStep, - HelperFlowMenuStep, +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, wrapped_entity_config_entry_title, ) from homeassistant.util.decorator import Registry @@ -100,12 +100,12 @@ async def test_config_flow_advanced_option( } ) - CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(CONFIG_SCHEMA) + CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(CONFIG_SCHEMA) } @manager.mock_reg_handler("test") - class TestFlow(HelperConfigFlowHandler): + class TestFlow(SchemaConfigFlowHandler): config_flow = CONFIG_FLOW # Start flow in basic mode @@ -195,11 +195,11 @@ async def test_options_flow_advanced_option( } ) - OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { - "init": HelperFlowFormStep(OPTIONS_SCHEMA) + OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } - class TestFlow(HelperConfigFlowHandler, domain="test"): + class TestFlow(SchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW From d25f7e1376e5df05c8fc4525dc4e203413570a9a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 31 Mar 2022 00:14:22 +0200 Subject: [PATCH 0007/1224] Motion Blinds integration add Brel dhcp discovery (#68938) --- homeassistant/components/motion_blinds/manifest.json | 3 +++ homeassistant/generated/dhcp.py | 1 + 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 4f4575ae6dd..b1def929f67 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -9,6 +9,9 @@ { "registered_devices": true }, { "hostname": "motion_*" + }, + { + "hostname": "brel_*" } ], "codeowners": ["@starkillerOG"], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 3780b7914c4..9afd63d1bd6 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -57,6 +57,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, {'domain': 'motion_blinds', 'registered_devices': True}, {'domain': 'motion_blinds', 'hostname': 'motion_*'}, + {'domain': 'motion_blinds', 'hostname': 'brel_*'}, {'domain': 'myq', 'macaddress': '645299*'}, {'domain': 'nest', 'macaddress': '18B430*'}, {'domain': 'nest', 'macaddress': '641666*'}, From 3244980a35aa91b54af24df1a7c7955fed11b01d Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 30 Mar 2022 23:45:55 +0100 Subject: [PATCH 0008/1224] Generic camera: Allow svg detect to accept leading whitespace (#68932) --- homeassistant/components/generic/config_flow.py | 2 +- tests/components/generic/test_config_flow.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 1b484821788..5c61966808d 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -114,7 +114,7 @@ def get_image_type(image): if fmt is None: # if imghdr can't figure it out, could be svg. with contextlib.suppress(UnicodeDecodeError): - if image.decode("utf-8").startswith(" Date: Thu, 31 Mar 2022 00:47:15 +0200 Subject: [PATCH 0009/1224] Improve utility_meter services.yaml (#68930) --- homeassistant/components/utility_meter/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index 800e001f6ff..777af78257c 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -6,6 +6,7 @@ reset: target: entity: domain: select + integration: utility_meter next_tariff: name: Next Tariff From e1c4245ff0626140524e212d762de51558823e2c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 30 Mar 2022 17:01:43 -0600 Subject: [PATCH 0010/1224] Bump pylitterbot to 2022.3.0 (#68929) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 0e2746017d8..a07f13a47b5 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.12.0"], + "requirements": ["pylitterbot==2022.3.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/requirements_all.txt b/requirements_all.txt index 285db2b5452..af1f1e96261 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1599,7 +1599,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.12.0 +pylitterbot==2022.3.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63308aa044d..1dcd2e5cb61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.12.0 +pylitterbot==2022.3.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 From f9aa0a7cd887456857ae7ed9466f8ded9a7cbbf5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Mar 2022 21:21:38 -0600 Subject: [PATCH 0011/1224] Bump simplisafe-python to 2022.03.2 (#68915) * Bump simplisafe-python to 2022.03.1 * Another bump --- 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 3791a9cace9..0bea1b6b33b 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==2022.03.0"], + "requirements": ["simplisafe-python==2022.03.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index af1f1e96261..63d5920ea52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2138,7 +2138,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.03.0 +simplisafe-python==2022.03.2 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dcd2e5cb61..f2ecacfb2e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1378,7 +1378,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.03.0 +simplisafe-python==2022.03.2 # homeassistant.components.slack slackclient==2.5.0 From 7a5235dc0cef277eab9608c107464a43d861b1e7 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 31 Mar 2022 08:02:43 +0100 Subject: [PATCH 0012/1224] Generic camera: Allow gif image type in still image checker (#68933) --- homeassistant/components/generic/config_flow.py | 2 +- tests/components/generic/conftest.py | 14 ++++++++++++++ tests/components/generic/test_config_flow.py | 14 +++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 5c61966808d..c5c645264d6 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -57,7 +57,7 @@ DEFAULT_DATA = { CONF_VERIFY_SSL: True, } -SUPPORTED_IMAGE_TYPES = ["png", "jpeg", "svg+xml"] +SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"} def build_schema( diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 63f7a87cba0..9daa3574e6e 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -36,12 +36,26 @@ def fakeimgbytes_svg(): ) +@pytest.fixture(scope="package") +def fakeimgbytes_gif(): + """Fake image in RAM for testing.""" + buf = BytesIO() # fake image in ram for testing. + Image.new("RGB", (1, 1)).save(buf, format="gif") + yield bytes(buf.getbuffer()) + + @pytest.fixture def fakeimg_png(fakeimgbytes_png): """Set up respx to respond to test url with fake image bytes.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) +@pytest.fixture +def fakeimg_gif(fakeimgbytes_gif): + """Set up respx to respond to test url with fake image bytes.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_gif) + + @pytest.fixture(scope="package") def mock_av_open(): """Fake container object with .streams.video[0] != None.""" diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index b3811f61acc..7849e54c747 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -110,12 +110,24 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): assert respx.calls.call_count == 1 +@respx.mock +async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): + """Test we complete ok if the user wants a gif.""" + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" + + @respx.mock async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): """Test we complete ok if svg starts with whitespace, issue #68889.""" fakeimgbytes_wspace_svg = bytes(" \n ", encoding="utf-8") + fakeimgbytes_svg respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_wspace_svg) - data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) result2 = await hass.config_entries.flow.async_configure( From 01a029be2d4f759fb8190deba1523e4f137f7d83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Mar 2022 09:03:33 +0200 Subject: [PATCH 0013/1224] Bump actions/cache from 3.0.0 to 3.0.1 (#68958) Bumps [actions/cache](https://github.com/actions/cache) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a2dd5788498..31a22f8f8d2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: >- @@ -189,7 +189,7 @@ jobs: # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PIP_CACHE }} key: >- @@ -212,7 +212,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -241,7 +241,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -253,7 +253,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -291,7 +291,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -303,7 +303,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -342,7 +342,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -354,7 +354,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -384,7 +384,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -396,7 +396,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -494,7 +494,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -523,7 +523,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -565,7 +565,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: >- @@ -582,7 +582,7 @@ jobs: # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: ${{ env.PIP_CACHE }} key: >- @@ -621,7 +621,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -663,7 +663,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -707,7 +707,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -750,7 +750,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.0 + uses: actions/cache@v3.0.1 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From 9432ab07c2ae73baa0c51198390cfa65bb83d836 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 02:06:07 -0700 Subject: [PATCH 0014/1224] Change privacy mode to config (#68954) --- homeassistant/components/unifiprotect/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 086bd852049..9271e87db50 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -87,7 +87,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( key=_KEY_PRIVACY_MODE, name="Privacy Mode", icon="mdi:eye-settings", - entity_category=None, + entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_privacy_mask", ufp_value="is_privacy_on", ), From 6b2fe6cba9b97c1fa3360c315e1f282cfd51cca1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 31 Mar 2022 05:28:49 -0400 Subject: [PATCH 0015/1224] Add support for new select selector properties (#68952) * Add support for new select selector properties * fix mode option * Apply suggestions from code review * Correct validation for empty options, update tests Co-authored-by: Erik Montnemery --- homeassistant/helpers/selector.py | 30 ++++++++++++++++++-------- tests/helpers/test_selector.py | 36 +++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 9577381d92b..b4d01ef52e0 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -471,25 +471,37 @@ select_option = vol.All( @SELECTORS.register("select") class SelectSelector(Selector): - """Selector for an single-choice input select.""" + """Selector for an single or multi-choice input select.""" selector_type = "select" CONFIG_SCHEMA = vol.Schema( { - vol.Required("options"): vol.All( - vol.Any([str], [select_option]), vol.Length(min=1) - ) + vol.Required("options"): vol.All(vol.Any([str], [select_option])), + vol.Optional("multiple", default=False): cv.boolean, + vol.Optional("custom_value", default=False): cv.boolean, + vol.Optional("mode"): vol.In(("list", "dropdown")), } ) def __call__(self, data: Any) -> Any: """Validate the passed selection.""" - if isinstance(self.config["options"][0], str): - options = self.config["options"] - else: - options = [option["value"] for option in self.config["options"]] - return vol.In(options)(vol.Schema(str)(data)) + options = [] + if self.config["options"]: + if isinstance(self.config["options"][0], str): + options = self.config["options"] + else: + options = [option["value"] for option in self.config["options"]] + + parent_schema = vol.In(options) + if self.config["custom_value"]: + parent_schema = vol.Any(parent_schema, str) + + if not self.config["multiple"]: + return parent_schema(vol.Schema(str)(data)) + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [parent_schema(vol.Schema(str)(val)) for val in data] @SELECTORS.register("text") diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 9e68af05487..bef95b056b4 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -246,7 +246,7 @@ def test_number_selector_schema(schema, valid_selections, invalid_selections): ), ) def test_number_selector_schema_error(schema): - """Test select selector.""" + """Test number selector.""" with pytest.raises(vol.Invalid): selector.validate_selector({"number": schema}) @@ -349,7 +349,7 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): ( {"options": ["red", "green", "blue"]}, ("red", "green", "blue"), - ("cat", 0, None), + ("cat", 0, None, ["red"]), ), ( { @@ -359,7 +359,36 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): ] }, ("red", "green"), - ("cat", 0, None), + ("cat", 0, None, ["red"]), + ), + ( + {"options": ["red", "green", "blue"], "multiple": True}, + (["red"], ["green", "blue"], []), + ("cat", 0, None, "red"), + ), + ( + { + "options": ["red", "green", "blue"], + "multiple": True, + "custom_value": True, + }, + (["red"], ["green", "blue"], ["red", "cat"], []), + ("cat", 0, None, "red"), + ), + ( + {"options": ["red", "green", "blue"], "custom_value": True}, + ("red", "green", "blue", "cat"), + (0, None, ["red"]), + ), + ( + {"options": [], "custom_value": True}, + ("red", "cat"), + (0, None, ["red"]), + ), + ( + {"options": [], "custom_value": True, "multiple": True}, + (["red"], ["green", "blue"], []), + (0, None, "red"), ), ), ) @@ -373,7 +402,6 @@ def test_select_selector_schema(schema, valid_selections, invalid_selections): ( {}, # Must have options {"options": {"hello": "World"}}, # Options must be a list - {"options": []}, # Must have at least option # Options must be strings or value / label pairs {"options": [{"hello": "World"}]}, # Options must all be of the same type From 4327d3aef93b63075cc79dbe5cc6792de4ddaaee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Mar 2022 11:32:11 +0200 Subject: [PATCH 0016/1224] Improve utility_meter services.yaml (#68960) --- homeassistant/components/utility_meter/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index 777af78257c..32a6069d3bb 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -36,6 +36,7 @@ calibrate: target: entity: domain: sensor + integration: utility_meter fields: value: name: Value From 3c478c312aa939f993838d27152f1aa0dac0448f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 31 Mar 2022 04:22:33 -0700 Subject: [PATCH 0017/1224] Fix google calendar blocking call, running outside of executor (#68948) --- homeassistant/components/google/api.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 10b4a35e25f..ea3d23dcb01 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -183,9 +183,13 @@ class GoogleCalendarService: """Get the calendar service with valid credetnails.""" await self._session.async_ensure_token_valid() creds = _async_google_creds(self._hass, self._session.token) - return google_discovery.build( - "calendar", "v3", credentials=creds, cache_discovery=False - ) + + def _build() -> google_discovery.Resource: + return google_discovery.build( + "calendar", "v3", credentials=creds, cache_discovery=False + ) + + return await self._hass.async_add_executor_job(_build) async def async_list_calendars( self, From 400943ce998f81ab4abb198429c1654b998ce173 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Mar 2022 13:57:26 +0200 Subject: [PATCH 0018/1224] Make utility_meter tariffs a list (#68967) --- .../components/utility_meter/config_flow.py | 11 ++++------- homeassistant/components/utility_meter/select.py | 7 +------ homeassistant/components/utility_meter/sensor.py | 7 +------ .../components/utility_meter/test_config_flow.py | 14 +++++++------- tests/components/utility_meter/test_init.py | 8 ++++---- tests/components/utility_meter/test_sensor.py | 16 ++++++++-------- 6 files changed, 25 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index 555ae7eb46d..ed12b3038b6 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -48,13 +48,8 @@ METER_TYPES = [ def _validate_config(data: Any) -> Any: """Validate config.""" - tariffs: list[str] - if not data[CONF_TARIFFS]: - tariffs = [] - else: - tariffs = data[CONF_TARIFFS].split(",") try: - vol.Unique()(tariffs) + vol.Unique()(data[CONF_TARIFFS]) except vol.Invalid as exc: raise SchemaFlowError("tariffs_not_unique") from exc @@ -88,7 +83,9 @@ CONFIG_SCHEMA = vol.Schema( } } ), - vol.Optional(CONF_TARIFFS): selector.selector({"text": {}}), + vol.Required(CONF_TARIFFS, default=[]): selector.selector( + {"select": {"options": [], "custom_value": True, "multiple": True}} + ), vol.Required(CONF_METER_NET_CONSUMPTION, default=False): selector.selector( {"boolean": {}} ), diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index e47f0626f6e..1f39b7f7c16 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -42,12 +42,7 @@ async def async_setup_entry( ) -> None: """Initialize Utility Meter config entry.""" name = config_entry.title - - # Remove when frontend list selector is available - if not config_entry.options.get(CONF_TARIFFS): - tariffs = [] - else: - tariffs = config_entry.options[CONF_TARIFFS].split(",") + tariffs = config_entry.options[CONF_TARIFFS] legacy_add_entities = None unique_id = config_entry.entry_id diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 7800c5035fb..c3d2be63a4b 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -119,12 +119,7 @@ async def async_setup_entry( tariff_entity = hass.data[DATA_UTILITY][entry_id][CONF_TARIFF_ENTITY] meters = [] - - # Remove when frontend list selector is available - if not config_entry.options.get(CONF_TARIFFS): - tariffs = [] - else: - tariffs = config_entry.options[CONF_TARIFFS].split(",") + tariffs = config_entry.options[CONF_TARIFFS] if not tariffs: # Add single sensor, not gated by a tariff selector diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index dd2d99617c6..53f9d814f2f 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: "name": "Electricity meter", "offset": 0, "source": input_sensor_entity_id, - "tariffs": "", + "tariffs": [], }, ) await hass.async_block_till_done() @@ -48,7 +48,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: "net_consumption": False, "offset": 0, "source": input_sensor_entity_id, - "tariffs": "", + "tariffs": [], } assert len(mock_setup_entry.mock_calls) == 1 @@ -61,7 +61,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: "net_consumption": False, "offset": 0, "source": input_sensor_entity_id, - "tariffs": "", + "tariffs": [], } assert config_entry.title == "Electricity meter" @@ -83,7 +83,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: "name": "Electricity meter", "offset": 0, "source": input_sensor_entity_id, - "tariffs": "cat,dog,horse,cow", + "tariffs": ["cat", "dog", "horse", "cow"], }, ) await hass.async_block_till_done() @@ -98,7 +98,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: "net_consumption": False, "offset": 0, "source": input_sensor_entity_id, - "tariffs": "cat,dog,horse,cow", + "tariffs": ["cat", "dog", "horse", "cow"], } config_entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -110,7 +110,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: "net_consumption": False, "offset": 0, "source": input_sensor_entity_id, - "tariffs": "cat,dog,horse,cow", + "tariffs": ["cat", "dog", "horse", "cow"], } assert config_entry.title == "Electricity meter" @@ -127,7 +127,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: "name": "Electricity meter", "offset": 0, "source": input_sensor_entity_id, - "tariffs": "cat,cat,cat,cat", + "tariffs": ["cat", "cat", "cat", "cat"], }, ) await hass.async_block_till_done() diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 925ff00f323..853fd827f0a 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -186,7 +186,7 @@ async def test_services_config_entry(hass): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "peak,offpeak", + "tariffs": ["peak", "offpeak"], }, title="Energy bill", ) @@ -202,7 +202,7 @@ async def test_services_config_entry(hass): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "peak,offpeak", + "tariffs": ["peak", "offpeak"], }, title="Energy bill2", ) @@ -469,11 +469,11 @@ async def test_legacy_support(hass): "tariffs,expected_entities", ( ( - "", + [], ["sensor.electricity_meter"], ), ( - "high,low", + ["high", "low"], [ "sensor.electricity_meter_low", "sensor.electricity_meter_high", diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 04610f6c2f4..1b8328f5a62 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -77,7 +77,7 @@ def alter_time(retval): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "onpeak,midpeak,offpeak", + "tariffs": ["onpeak", "midpeak", "offpeak"], }, ), ), @@ -249,7 +249,7 @@ async def test_state(hass, yaml_config, config_entry_config): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "onpeak,midpeak,offpeak", + "tariffs": ["onpeak", "midpeak", "offpeak"], }, ), ), @@ -327,7 +327,7 @@ async def test_init(hass, yaml_config, config_entry_config): "net_consumption": True, "offset": 0, "source": "sensor.energy", - "tariffs": "", + "tariffs": [], }, { "cycle": "none", @@ -336,7 +336,7 @@ async def test_init(hass, yaml_config, config_entry_config): "net_consumption": False, "offset": 0, "source": "sensor.gas", - "tariffs": "", + "tariffs": [], }, ], ), @@ -411,7 +411,7 @@ async def test_device_class(hass, yaml_config, config_entry_configs): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "onpeak,midpeak,offpeak", + "tariffs": ["onpeak", "midpeak", "offpeak"], }, ), ), @@ -514,7 +514,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): "net_consumption": True, "offset": 0, "source": "sensor.energy", - "tariffs": "", + "tariffs": [], }, ), ), @@ -582,7 +582,7 @@ async def test_net_consumption(hass, yaml_config, config_entry_config): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "", + "tariffs": [], }, ), ), @@ -650,7 +650,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config): "net_consumption": False, "offset": 0, "source": "sensor.energy", - "tariffs": "", + "tariffs": [], }, ), ), From f7c936e842e668115173cc3a8e53b3e6bb7e036f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 31 Mar 2022 14:18:45 +0200 Subject: [PATCH 0019/1224] Add scaffold template for backup (#68961) --- script/scaffold/docs.py | 8 ++++++-- .../templates/backup/integration/backup.py | 10 ++++++++++ .../templates/backup/tests/test_backup.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 script/scaffold/templates/backup/integration/backup.py create mode 100644 script/scaffold/templates/backup/tests/test_backup.py diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 6e31f15e6d4..3ce86b0e138 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -2,6 +2,10 @@ from .model import Info DATA = { + "backup": { + "title": "Backup", + "docs": "https://developers.home-assistant.io/docs/core/platform/backup", + }, "config_flow": { "title": "Config Flow", "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html", @@ -36,12 +40,12 @@ DATA = { }, "reproduce_state": { "title": "Reproduce State", - "docs": "https://developers.home-assistant.io/docs/en/reproduce_state_index.html", + "docs": "https://developers.home-assistant.io/docs/core/platform/reproduce_state", "extra": "You will now need to update the code to make sure that every attribute that can occur in the state will cause the right service to be called.", }, "significant_change": { "title": "Significant Change", - "docs": "https://developers.home-assistant.io/docs/en/significant_change_index.html", + "docs": "https://developers.home-assistant.io/docs/core/platform/significant_change", "extra": "You will now need to update the code to make sure that entities with different device classes are correctly considered.", }, } diff --git a/script/scaffold/templates/backup/integration/backup.py b/script/scaffold/templates/backup/integration/backup.py new file mode 100644 index 00000000000..88df5ead221 --- /dev/null +++ b/script/scaffold/templates/backup/integration/backup.py @@ -0,0 +1,10 @@ +"""Backup platform for the NEW_NAME integration.""" +from homeassistant.core import HomeAssistant + + +async def async_pre_backup(hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + + +async def async_post_backup(hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" diff --git a/script/scaffold/templates/backup/tests/test_backup.py b/script/scaffold/templates/backup/tests/test_backup.py new file mode 100644 index 00000000000..43d23ac7a90 --- /dev/null +++ b/script/scaffold/templates/backup/tests/test_backup.py @@ -0,0 +1,18 @@ +"""Test the NEW_NAME backup platform.""" +from homeassistant.components.NEW_DOMAIN.backup import ( + async_post_backup, + async_pre_backup, +) +from homeassistant.core import HomeAssistant + + +async def test_async_post_backup(hass: HomeAssistant) -> None: + """Verify async_post_backup.""" + # TODO: verify that the async_post_backup function executes as expected + assert await async_post_backup(hass) + + +async def test_async_pre_backup(hass: HomeAssistant) -> None: + """Verify async_pre_backup.""" + # TODO: verify that the async_pre_backup function executes as expected + assert await async_pre_backup(hass) From 185aa025acb222f453dc77c837fbe5af69240a52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Mar 2022 02:28:19 -1000 Subject: [PATCH 0020/1224] Exclude large and chatty attributes from being recorded for update entities (#68940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/update/recorder.py | 12 +++++ tests/components/update/test_recorder.py | 55 +++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 homeassistant/components/update/recorder.py create mode 100644 tests/components/update/test_recorder.py diff --git a/homeassistant/components/update/recorder.py b/homeassistant/components/update/recorder.py new file mode 100644 index 00000000000..cba4cab8ec2 --- /dev/null +++ b/homeassistant/components/update/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude large and chatty update attributes from being recorded in the database.""" + return {ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY} diff --git a/tests/components/update/test_recorder.py b/tests/components/update/test_recorder.py new file mode 100644 index 00000000000..17ab7445b4b --- /dev/null +++ b/tests/components/update/test_recorder.py @@ -0,0 +1,55 @@ +"""The tests for update recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.components.update.const import ( + ATTR_CURRENT_VERSION, + ATTR_IN_PROGRESS, + ATTR_RELEASE_SUMMARY, + DOMAIN, +) +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import HomeAssistant, State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed, async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + + +async def test_exclude_attributes( + hass: HomeAssistant, enable_custom_integrations: None +): + """Test update attributes to be excluded.""" + await async_init_recorder_component(hass) + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + state = hass.states.get("update.update_already_in_progress") + assert state.attributes[ATTR_IN_PROGRESS] == 50 + await async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_IN_PROGRESS not in state.attributes + assert ATTR_RELEASE_SUMMARY not in state.attributes + assert ATTR_CURRENT_VERSION in state.attributes From fc27f38de17e33198c57bff0c091a8fb0dc4f0f6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 31 Mar 2022 08:47:51 -0400 Subject: [PATCH 0021/1224] Rename google hangouts to google chat (#68941) --- homeassistant/components/hangouts/manifest.json | 2 +- homeassistant/components/hangouts/strings.json | 2 +- homeassistant/components/hangouts/translations/en.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 983dc60414a..44d85405339 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -1,6 +1,6 @@ { "domain": "hangouts", - "name": "Google Hangouts", + "name": "Google Chat", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": ["hangups==0.4.17"], diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json index 0128363a1ab..fcc2da456bb 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -16,7 +16,7 @@ "password": "[%key:common::config_flow::data::password%]", "authorization_code": "Authorization Code (required for manual authentication)" }, - "title": "Google Hangouts Login" + "title": "Google Chat Login" }, "2fa": { "data": { diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json index b2d7076bd75..4829e843c6c 100644 --- a/homeassistant/components/hangouts/translations/en.json +++ b/homeassistant/components/hangouts/translations/en.json @@ -22,7 +22,7 @@ "email": "Email", "password": "Password" }, - "title": "Google Hangouts Login" + "title": "Google Chat Login" } } } From 2c0153a32ed6f5c787bafd99099041d65dd85ae4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 31 Mar 2022 14:53:18 +0200 Subject: [PATCH 0022/1224] Revert "Pin click to fix typer issue" (#68927) --- homeassistant/package_constraints.txt | 4 ---- script/gen_requirements_all.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9e0f7d5ea23..641a78fb4a4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -101,7 +101,3 @@ multidict>=6.0.2 # Required for compatibility with point integration - ensure_active_token # https://github.com/home-assistant/core/pull/68176 authlib<1.0 - -# Required for compatibility with typer, used by pyunifiprotect integration -# https://github.com/tiangolo/typer/pull/375 -click<=8.0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e3545ac0ece..c48a59f90d3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -118,10 +118,6 @@ multidict>=6.0.2 # Required for compatibility with point integration - ensure_active_token # https://github.com/home-assistant/core/pull/68176 authlib<1.0 - -# Required for compatibility with typer, used by pyunifiprotect integration -# https://github.com/tiangolo/typer/pull/375 -click<=8.0.4 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 4e2b6db397b04e68f73dcf29b4de54d3f107e842 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 31 Mar 2022 10:31:17 -0400 Subject: [PATCH 0023/1224] Add comments to zwave_js node metadata WS API (#67210) * Add comments to zwave_js node metadata WS API * Add test dat --- homeassistant/components/zwave_js/api.py | 1 + .../zwave_js/fixtures/wallmote_central_scene_state.json | 6 +++++- tests/components/zwave_js/test_api.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 0e947de982b..9cd79ecb27b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -496,6 +496,7 @@ async def websocket_node_metadata( "wakeup": node.device_config.metadata.wakeup, "reset": node.device_config.metadata.reset, "device_database_url": node.device_database_url, + "comments": node.device_config.metadata.comments, } connection.send_result( msg[ID], diff --git a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json index af5314002fa..e4d9f01341e 100644 --- a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json +++ b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json @@ -68,7 +68,11 @@ "inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.", "exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into \u201cexclusion\u201d mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.", "reset": "Remove cover to triggered tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the \u201cDevice Reset Locally Notification\u201d command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)", - "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf", + "comments": { + "level": "info", + "text": "test" + } }, "isEmbedded": true }, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 1596b099ab1..7ec7d98217b 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -249,6 +249,7 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_ result["device_database_url"] == "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0" ) + assert result["comments"] == [{"level": "info", "text": "test"}] # Test getting non-existent node fails await ws_client.send_json( From 2c66ac62033b2f2ba33015120dc5f5f6bccc0902 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 16:39:57 +0200 Subject: [PATCH 0024/1224] Remove deprecated Updater integration (#68981) * Remove deprecated Updater integration * Remove updater mock --- CODEOWNERS | 2 - homeassistant/components/updater/__init__.py | 141 ------------------ .../components/updater/binary_sensor.py | 56 ------- .../components/updater/manifest.json | 8 - homeassistant/components/updater/strings.json | 1 - .../components/updater/translations/af.json | 3 - .../components/updater/translations/ar.json | 3 - .../components/updater/translations/bg.json | 3 - .../components/updater/translations/bs.json | 3 - .../components/updater/translations/ca.json | 3 - .../components/updater/translations/cs.json | 3 - .../components/updater/translations/cy.json | 3 - .../components/updater/translations/da.json | 3 - .../components/updater/translations/de.json | 3 - .../components/updater/translations/el.json | 3 - .../components/updater/translations/en.json | 3 - .../updater/translations/es-419.json | 3 - .../components/updater/translations/es.json | 3 - .../components/updater/translations/et.json | 3 - .../components/updater/translations/eu.json | 3 - .../components/updater/translations/fa.json | 3 - .../components/updater/translations/fi.json | 3 - .../components/updater/translations/fr.json | 3 - .../components/updater/translations/gsw.json | 3 - .../components/updater/translations/he.json | 3 - .../components/updater/translations/hr.json | 3 - .../components/updater/translations/hu.json | 3 - .../components/updater/translations/hy.json | 3 - .../components/updater/translations/id.json | 3 - .../components/updater/translations/is.json | 3 - .../components/updater/translations/it.json | 3 - .../components/updater/translations/ja.json | 3 - .../components/updater/translations/ko.json | 3 - .../components/updater/translations/lb.json | 3 - .../components/updater/translations/lv.json | 3 - .../components/updater/translations/nb.json | 3 - .../components/updater/translations/nl.json | 3 - .../components/updater/translations/nn.json | 3 - .../components/updater/translations/no.json | 3 - .../components/updater/translations/pl.json | 3 - .../updater/translations/pt-BR.json | 3 - .../components/updater/translations/pt.json | 3 - .../components/updater/translations/ro.json | 3 - .../components/updater/translations/ru.json | 3 - .../components/updater/translations/sk.json | 3 - .../components/updater/translations/sl.json | 3 - .../components/updater/translations/sv.json | 3 - .../components/updater/translations/ta.json | 3 - .../components/updater/translations/te.json | 3 - .../components/updater/translations/th.json | 3 - .../components/updater/translations/tr.json | 3 - .../components/updater/translations/uk.json | 3 - .../components/updater/translations/vi.json | 3 - .../updater/translations/zh-Hans.json | 3 - .../updater/translations/zh-Hant.json | 3 - tests/components/default_config/test_init.py | 7 - tests/components/updater/__init__.py | 1 - tests/components/updater/test_init.py | 130 ---------------- 58 files changed, 496 deletions(-) delete mode 100644 homeassistant/components/updater/__init__.py delete mode 100644 homeassistant/components/updater/binary_sensor.py delete mode 100644 homeassistant/components/updater/manifest.json delete mode 100644 homeassistant/components/updater/strings.json delete mode 100644 homeassistant/components/updater/translations/af.json delete mode 100644 homeassistant/components/updater/translations/ar.json delete mode 100644 homeassistant/components/updater/translations/bg.json delete mode 100644 homeassistant/components/updater/translations/bs.json delete mode 100644 homeassistant/components/updater/translations/ca.json delete mode 100644 homeassistant/components/updater/translations/cs.json delete mode 100644 homeassistant/components/updater/translations/cy.json delete mode 100644 homeassistant/components/updater/translations/da.json delete mode 100644 homeassistant/components/updater/translations/de.json delete mode 100644 homeassistant/components/updater/translations/el.json delete mode 100644 homeassistant/components/updater/translations/en.json delete mode 100644 homeassistant/components/updater/translations/es-419.json delete mode 100644 homeassistant/components/updater/translations/es.json delete mode 100644 homeassistant/components/updater/translations/et.json delete mode 100644 homeassistant/components/updater/translations/eu.json delete mode 100644 homeassistant/components/updater/translations/fa.json delete mode 100644 homeassistant/components/updater/translations/fi.json delete mode 100644 homeassistant/components/updater/translations/fr.json delete mode 100644 homeassistant/components/updater/translations/gsw.json delete mode 100644 homeassistant/components/updater/translations/he.json delete mode 100644 homeassistant/components/updater/translations/hr.json delete mode 100644 homeassistant/components/updater/translations/hu.json delete mode 100644 homeassistant/components/updater/translations/hy.json delete mode 100644 homeassistant/components/updater/translations/id.json delete mode 100644 homeassistant/components/updater/translations/is.json delete mode 100644 homeassistant/components/updater/translations/it.json delete mode 100644 homeassistant/components/updater/translations/ja.json delete mode 100644 homeassistant/components/updater/translations/ko.json delete mode 100644 homeassistant/components/updater/translations/lb.json delete mode 100644 homeassistant/components/updater/translations/lv.json delete mode 100644 homeassistant/components/updater/translations/nb.json delete mode 100644 homeassistant/components/updater/translations/nl.json delete mode 100644 homeassistant/components/updater/translations/nn.json delete mode 100644 homeassistant/components/updater/translations/no.json delete mode 100644 homeassistant/components/updater/translations/pl.json delete mode 100644 homeassistant/components/updater/translations/pt-BR.json delete mode 100644 homeassistant/components/updater/translations/pt.json delete mode 100644 homeassistant/components/updater/translations/ro.json delete mode 100644 homeassistant/components/updater/translations/ru.json delete mode 100644 homeassistant/components/updater/translations/sk.json delete mode 100644 homeassistant/components/updater/translations/sl.json delete mode 100644 homeassistant/components/updater/translations/sv.json delete mode 100644 homeassistant/components/updater/translations/ta.json delete mode 100644 homeassistant/components/updater/translations/te.json delete mode 100644 homeassistant/components/updater/translations/th.json delete mode 100644 homeassistant/components/updater/translations/tr.json delete mode 100644 homeassistant/components/updater/translations/uk.json delete mode 100644 homeassistant/components/updater/translations/vi.json delete mode 100644 homeassistant/components/updater/translations/zh-Hans.json delete mode 100644 homeassistant/components/updater/translations/zh-Hant.json delete mode 100644 tests/components/updater/__init__.py delete mode 100644 tests/components/updater/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index d0f03e31e0e..8f0a672fa51 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1076,8 +1076,6 @@ build.json @home-assistant/supervisor /tests/components/upcloud/ @scop /homeassistant/components/update/ @home-assistant/core /tests/components/update/ @home-assistant/core -/homeassistant/components/updater/ @home-assistant/core -/tests/components/updater/ @home-assistant/core /homeassistant/components/upnp/ @StevenLooman @ehendrix23 /tests/components/upnp/ @StevenLooman @ehendrix23 /homeassistant/components/uptime/ @frenck diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py deleted file mode 100644 index 4f88b5d1369..00000000000 --- a/homeassistant/components/updater/__init__.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Support to check for available updates.""" -import asyncio -from datetime import timedelta -import logging - -import async_timeout -from awesomeversion import AwesomeVersion -import voluptuous as vol - -from homeassistant.components import hassio -from homeassistant.const import Platform, __version__ as current_version -from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery, update_coordinator -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) - -ATTR_RELEASE_NOTES = "release_notes" -ATTR_NEWEST_VERSION = "newest_version" - -CONF_REPORTING = "reporting" -CONF_COMPONENT_REPORTING = "include_used_components" - -DOMAIN = "updater" - -UPDATER_URL = "https://www.home-assistant.io/version.json" - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: { - vol.Optional(CONF_REPORTING): cv.boolean, - vol.Optional(CONF_COMPONENT_REPORTING): cv.boolean, - } - }, - extra=vol.ALLOW_EXTRA, -) - -RESPONSE_SCHEMA = vol.Schema( - {vol.Required("current_version"): cv.string, vol.Required("release_notes"): cv.url}, - extra=vol.REMOVE_EXTRA, -) - - -class Updater: - """Updater class for data exchange.""" - - def __init__( - self, update_available: bool, newest_version: str, release_notes: str - ) -> None: - """Initialize attributes.""" - self.update_available = update_available - self.release_notes = release_notes - self.newest_version = newest_version - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the updater component.""" - _LOGGER.warning( - "The updater integration has been deprecated and will be removed in 2022.5, " - "please remove it from your configuration" - ) - - async def check_new_version() -> Updater: - """Check if a new version is available and report if one is.""" - # Skip on dev - if "dev" in current_version: - return Updater(False, "", "") - - newest, release_notes = await get_newest_version(hass) - - _LOGGER.debug("Fetched version %s: %s", newest, release_notes) - - # Load data from Supervisor - if hassio.is_hassio(hass): - core_info = hassio.get_core_info(hass) - newest = core_info["version_latest"] - - # Validate version - update_available = False - if AwesomeVersion(newest) > AwesomeVersion(current_version): - _LOGGER.debug( - "The latest available version of Home Assistant is %s", newest - ) - update_available = True - elif AwesomeVersion(newest) == AwesomeVersion(current_version): - _LOGGER.debug( - "You are on the latest version (%s) of Home Assistant", newest - ) - elif AwesomeVersion(newest) < AwesomeVersion(current_version): - _LOGGER.debug( - "Local version (%s) is newer than the latest available version (%s)", - current_version, - newest, - ) - - _LOGGER.debug("Update available: %s", update_available) - - return Updater(update_available, newest, release_notes) - - coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator[Updater]( - hass, - _LOGGER, - name="Home Assistant update", - update_method=check_new_version, - update_interval=timedelta(days=1), - ) - - # This can take up to 15s which can delay startup - asyncio.create_task(coordinator.async_refresh()) - - hass.async_create_task( - discovery.async_load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config) - ) - - return True - - -async def get_newest_version(hass): - """Get the newest Home Assistant version.""" - session = async_get_clientsession(hass) - - async with async_timeout.timeout(30): - req = await session.get(UPDATER_URL) - - try: - res = await req.json() - except ValueError as err: - raise update_coordinator.UpdateFailed( - "Received invalid JSON from Home Assistant Update" - ) from err - - try: - res = RESPONSE_SCHEMA(res) - return res["current_version"], res["release_notes"] - except vol.Invalid as err: - raise update_coordinator.UpdateFailed( - f"Got unexpected response: {err}" - ) from err diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py deleted file mode 100644 index 1409f26a2f5..00000000000 --- a/homeassistant/components/updater/binary_sensor.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Support for Home Assistant Updater binary sensors.""" -from __future__ import annotations - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the updater binary sensors.""" - if discovery_info is None: - return - - async_add_entities([UpdaterBinary(hass.data[UPDATER_DOMAIN])]) - - -class UpdaterBinary(CoordinatorEntity, BinarySensorEntity): - """Representation of an updater binary sensor.""" - - _attr_device_class = BinarySensorDeviceClass.UPDATE - _attr_name = "Updater" - _attr_unique_id = "updater" - - @property - def available(self) -> bool: - """Return if entity is available.""" - return True - - @property - def is_on(self) -> bool: - """Return true if there is an update available.""" - return self.coordinator.data and self.coordinator.data.update_available - - @property - def extra_state_attributes(self) -> dict | None: - """Return the optional state attributes.""" - if not self.coordinator.data: - return None - data = {} - if self.coordinator.data.release_notes: - data[ATTR_RELEASE_NOTES] = self.coordinator.data.release_notes - if self.coordinator.data.newest_version: - data[ATTR_NEWEST_VERSION] = self.coordinator.data.newest_version - return data diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json deleted file mode 100644 index db225bbf242..00000000000 --- a/homeassistant/components/updater/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "updater", - "name": "Updater", - "documentation": "https://www.home-assistant.io/integrations/updater", - "codeowners": ["@home-assistant/core"], - "quality_scale": "internal", - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/updater/strings.json b/homeassistant/components/updater/strings.json deleted file mode 100644 index d4fe2079d8f..00000000000 --- a/homeassistant/components/updater/strings.json +++ /dev/null @@ -1 +0,0 @@ -{ "title": "Updater" } diff --git a/homeassistant/components/updater/translations/af.json b/homeassistant/components/updater/translations/af.json deleted file mode 100644 index bf9cb9c98f4..00000000000 --- a/homeassistant/components/updater/translations/af.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Opdateerder" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ar.json b/homeassistant/components/updater/translations/ar.json deleted file mode 100644 index 9aecb4b83dc..00000000000 --- a/homeassistant/components/updater/translations/ar.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u062a\u062d\u062f\u064a\u062b" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/bg.json b/homeassistant/components/updater/translations/bg.json deleted file mode 100644 index ce1bddc104f..00000000000 --- a/homeassistant/components/updater/translations/bg.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u041e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/bs.json b/homeassistant/components/updater/translations/bs.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/bs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ca.json b/homeassistant/components/updater/translations/ca.json deleted file mode 100644 index 419215d32b6..00000000000 --- a/homeassistant/components/updater/translations/ca.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Actualitzador" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/cs.json b/homeassistant/components/updater/translations/cs.json deleted file mode 100644 index 9d25158400b..00000000000 --- a/homeassistant/components/updater/translations/cs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aktualiz\u00e1tor" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/cy.json b/homeassistant/components/updater/translations/cy.json deleted file mode 100644 index b3ef0dcb85f..00000000000 --- a/homeassistant/components/updater/translations/cy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Diweddarwr" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/da.json b/homeassistant/components/updater/translations/da.json deleted file mode 100644 index bc9b108c3ec..00000000000 --- a/homeassistant/components/updater/translations/da.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Opdater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/de.json b/homeassistant/components/updater/translations/de.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/de.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/el.json b/homeassistant/components/updater/translations/el.json deleted file mode 100644 index f44dc928c16..00000000000 --- a/homeassistant/components/updater/translations/el.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03c4\u03ae\u03c2" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/en.json b/homeassistant/components/updater/translations/en.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/en.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/es-419.json b/homeassistant/components/updater/translations/es-419.json deleted file mode 100644 index a822ffbd0a9..00000000000 --- a/homeassistant/components/updater/translations/es-419.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Actualizador" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/es.json b/homeassistant/components/updater/translations/es.json deleted file mode 100644 index a822ffbd0a9..00000000000 --- a/homeassistant/components/updater/translations/es.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Actualizador" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/et.json b/homeassistant/components/updater/translations/et.json deleted file mode 100644 index 8d36316f011..00000000000 --- a/homeassistant/components/updater/translations/et.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Uuendaja" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/eu.json b/homeassistant/components/updater/translations/eu.json deleted file mode 100644 index cec08736bae..00000000000 --- a/homeassistant/components/updater/translations/eu.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Eguneratzailea" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/fa.json b/homeassistant/components/updater/translations/fa.json deleted file mode 100644 index d32b1e212c2..00000000000 --- a/homeassistant/components/updater/translations/fa.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u0628\u0647 \u0631\u0648\u0632 \u0631\u0633\u0627\u0646" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/fi.json b/homeassistant/components/updater/translations/fi.json deleted file mode 100644 index 48f9aa81b72..00000000000 --- a/homeassistant/components/updater/translations/fi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "P\u00e4ivitys" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/fr.json b/homeassistant/components/updater/translations/fr.json deleted file mode 100644 index 228912f95a8..00000000000 --- a/homeassistant/components/updater/translations/fr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Mise \u00e0 jour" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/gsw.json b/homeassistant/components/updater/translations/gsw.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/gsw.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/he.json b/homeassistant/components/updater/translations/he.json deleted file mode 100644 index 38072833421..00000000000 --- a/homeassistant/components/updater/translations/he.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u05de\u05e2\u05d3\u05db\u05df" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/hr.json b/homeassistant/components/updater/translations/hr.json deleted file mode 100644 index 21d0438f9cb..00000000000 --- a/homeassistant/components/updater/translations/hr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "A\u017euriranje" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/hu.json b/homeassistant/components/updater/translations/hu.json deleted file mode 100644 index e862dcb360c..00000000000 --- a/homeassistant/components/updater/translations/hu.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Friss\u00edt\u0151" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/hy.json b/homeassistant/components/updater/translations/hy.json deleted file mode 100644 index 78c67fb8950..00000000000 --- a/homeassistant/components/updater/translations/hy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u0539\u0561\u0580\u0574\u0561\u0581\u0576\u0578\u0572" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/id.json b/homeassistant/components/updater/translations/id.json deleted file mode 100644 index 1ab6aa58946..00000000000 --- a/homeassistant/components/updater/translations/id.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Pembaru" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/is.json b/homeassistant/components/updater/translations/is.json deleted file mode 100644 index e0f7536fd1a..00000000000 --- a/homeassistant/components/updater/translations/is.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Uppf\u00e6rslu\u00e1lfur" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/it.json b/homeassistant/components/updater/translations/it.json deleted file mode 100644 index 539f0bb4294..00000000000 --- a/homeassistant/components/updater/translations/it.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aggiornamento" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ja.json b/homeassistant/components/updater/translations/ja.json deleted file mode 100644 index 2a34917b909..00000000000 --- a/homeassistant/components/updater/translations/ja.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30bf\u30fc" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ko.json b/homeassistant/components/updater/translations/ko.json deleted file mode 100644 index 14137569e1b..00000000000 --- a/homeassistant/components/updater/translations/ko.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\uc5c5\ub370\uc774\ud130" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/lb.json b/homeassistant/components/updater/translations/lb.json deleted file mode 100644 index 375f8fa7bc6..00000000000 --- a/homeassistant/components/updater/translations/lb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aktualis\u00e9ierung" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/lv.json b/homeassistant/components/updater/translations/lv.json deleted file mode 100644 index 15d29e35a06..00000000000 --- a/homeassistant/components/updater/translations/lv.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Atjaunin\u0101t\u0101js" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/nb.json b/homeassistant/components/updater/translations/nb.json deleted file mode 100644 index e98d60ab4fc..00000000000 --- a/homeassistant/components/updater/translations/nb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Oppdater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/nl.json b/homeassistant/components/updater/translations/nl.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/nl.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/nn.json b/homeassistant/components/updater/translations/nn.json deleted file mode 100644 index 7eb98bdd2c1..00000000000 --- a/homeassistant/components/updater/translations/nn.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Oppdateringar" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/no.json b/homeassistant/components/updater/translations/no.json deleted file mode 100644 index c8fafabfe77..00000000000 --- a/homeassistant/components/updater/translations/no.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Oppdaterer" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/pl.json b/homeassistant/components/updater/translations/pl.json deleted file mode 100644 index 21a3703bba9..00000000000 --- a/homeassistant/components/updater/translations/pl.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aktualizator" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/pt-BR.json b/homeassistant/components/updater/translations/pt-BR.json deleted file mode 100644 index cc89a22092a..00000000000 --- a/homeassistant/components/updater/translations/pt-BR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Gerenciador de atualiza\u00e7\u00f5es" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/pt.json b/homeassistant/components/updater/translations/pt.json deleted file mode 100644 index 7d07ec8da09..00000000000 --- a/homeassistant/components/updater/translations/pt.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Atualizador" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ro.json b/homeassistant/components/updater/translations/ro.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/ro.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ru.json b/homeassistant/components/updater/translations/ru.json deleted file mode 100644 index a2ee79efd15..00000000000 --- a/homeassistant/components/updater/translations/ru.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/sk.json b/homeassistant/components/updater/translations/sk.json deleted file mode 100644 index 9d25158400b..00000000000 --- a/homeassistant/components/updater/translations/sk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aktualiz\u00e1tor" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/sl.json b/homeassistant/components/updater/translations/sl.json deleted file mode 100644 index 7972844cb69..00000000000 --- a/homeassistant/components/updater/translations/sl.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Posodabljalnik" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/sv.json b/homeassistant/components/updater/translations/sv.json deleted file mode 100644 index 78ef7d2df20..00000000000 --- a/homeassistant/components/updater/translations/sv.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Uppdaterare" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/ta.json b/homeassistant/components/updater/translations/ta.json deleted file mode 100644 index 74f9398fbcb..00000000000 --- a/homeassistant/components/updater/translations/ta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u0b85\u0baa\u0bcd\u0b9f\u0bc7\u0b9f\u0bcd\u0b9f\u0bb0\u0bcd" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/te.json b/homeassistant/components/updater/translations/te.json deleted file mode 100644 index 43859eedc5a..00000000000 --- a/homeassistant/components/updater/translations/te.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Updater" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/th.json b/homeassistant/components/updater/translations/th.json deleted file mode 100644 index d825a885d68..00000000000 --- a/homeassistant/components/updater/translations/th.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u0e2d\u0e31\u0e1e\u0e40\u0e14\u0e15" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/tr.json b/homeassistant/components/updater/translations/tr.json deleted file mode 100644 index 7034ef0d79e..00000000000 --- a/homeassistant/components/updater/translations/tr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "G\u00fcncelleyici" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/uk.json b/homeassistant/components/updater/translations/uk.json deleted file mode 100644 index e98d67fc206..00000000000 --- a/homeassistant/components/updater/translations/uk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/vi.json b/homeassistant/components/updater/translations/vi.json deleted file mode 100644 index 0e2783d6f21..00000000000 --- a/homeassistant/components/updater/translations/vi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Tr\u00ecnh c\u1eadp nh\u1eadt" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/zh-Hans.json b/homeassistant/components/updater/translations/zh-Hans.json deleted file mode 100644 index 154ab2b812b..00000000000 --- a/homeassistant/components/updater/translations/zh-Hans.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u66f4\u65b0\u63d0\u793a" -} \ No newline at end of file diff --git a/homeassistant/components/updater/translations/zh-Hant.json b/homeassistant/components/updater/translations/zh-Hant.json deleted file mode 100644 index 23c1b069fc1..00000000000 --- a/homeassistant/components/updater/translations/zh-Hant.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u66f4\u65b0\u5668" -} \ No newline at end of file diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 1052eeeb164..7701eb55b90 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -15,13 +15,6 @@ def mock_ssdp(): yield -@pytest.fixture(autouse=True) -def mock_updater(): - """Mock updater.""" - with patch("homeassistant.components.updater.get_newest_version"): - yield - - @pytest.fixture(autouse=True) def recorder_url_mock(): """Mock recorder url.""" diff --git a/tests/components/updater/__init__.py b/tests/components/updater/__init__.py deleted file mode 100644 index 31a19cb3bf7..00000000000 --- a/tests/components/updater/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the updater component.""" diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py deleted file mode 100644 index 2b0f494f5f5..00000000000 --- a/tests/components/updater/test_init.py +++ /dev/null @@ -1,130 +0,0 @@ -"""The tests for the Updater integration.""" -from unittest.mock import patch - -import pytest - -from homeassistant.components import updater -from homeassistant.helpers.update_coordinator import UpdateFailed -from homeassistant.setup import async_setup_component - -from tests.common import mock_component - -NEW_VERSION = "10000.0" -MOCK_VERSION = "10.0" -MOCK_DEV_VERSION = "10.0.dev0" -MOCK_RESPONSE = { - "current_version": "0.15", - "release_notes": "https://home-assistant.io", -} -MOCK_CONFIG = {updater.DOMAIN: {"reporting": True}} -RELEASE_NOTES = "test release notes" - - -@pytest.fixture(autouse=True) -def mock_version(): - """Mock current version.""" - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - yield - - -@pytest.fixture(name="mock_get_newest_version") -def mock_get_newest_version_fixture(): - """Fixture to mock get_newest_version.""" - with patch( - "homeassistant.components.updater.get_newest_version", - return_value=(NEW_VERSION, RELEASE_NOTES), - ) as mock: - yield mock - - -async def test_new_version_shows_entity_true(hass, mock_get_newest_version): - """Test if sensor is true if new version is available.""" - assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - - await hass.async_block_till_done() - assert hass.states.is_state("binary_sensor.updater", "on") - assert ( - hass.states.get("binary_sensor.updater").attributes["newest_version"] - == NEW_VERSION - ) - assert ( - hass.states.get("binary_sensor.updater").attributes["release_notes"] - == RELEASE_NOTES - ) - - -async def test_same_version_shows_entity_false(hass, mock_get_newest_version): - """Test if sensor is false if no new version is available.""" - mock_get_newest_version.return_value = (MOCK_VERSION, "") - - assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - - await hass.async_block_till_done() - - assert hass.states.is_state("binary_sensor.updater", "off") - assert ( - hass.states.get("binary_sensor.updater").attributes["newest_version"] - == MOCK_VERSION - ) - assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes - - -async def test_deprecated_reporting(hass, mock_get_newest_version, caplog): - """Test we do not gather analytics when disable reporting is active.""" - mock_get_newest_version.return_value = (MOCK_VERSION, "") - - assert await async_setup_component( - hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": True}} - ) - await hass.async_block_till_done() - - assert "deprecated" in caplog.text - - -async def test_error_fetching_new_version_bad_json(hass, aioclient_mock): - """Test we handle json error while fetching new version.""" - aioclient_mock.get(updater.UPDATER_URL, text="not json") - - with patch( - "homeassistant.helpers.system_info.async_get_system_info", - return_value={"fake": "bla"}, - ), pytest.raises(UpdateFailed): - await updater.get_newest_version(hass) - - -async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): - """Test we handle response error while fetching new version.""" - aioclient_mock.get( - updater.UPDATER_URL, - json={ - "version": "0.15" - # 'release-notes' is missing - }, - ) - - with patch( - "homeassistant.helpers.system_info.async_get_system_info", - return_value={"fake": "bla"}, - ), pytest.raises(UpdateFailed): - await updater.get_newest_version(hass) - - -async def test_new_version_shows_entity_after_hour_hassio( - hass, mock_get_newest_version -): - """Test if binary sensor gets updated if new version is available / Hass.io.""" - mock_component(hass, "hassio") - hass.data["hassio_core_info"] = {"version_latest": "999.0"} - - assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) - - await hass.async_block_till_done() - - assert hass.states.is_state("binary_sensor.updater", "on") - assert ( - hass.states.get("binary_sensor.updater").attributes["newest_version"] == "999.0" - ) - assert ( - hass.states.get("binary_sensor.updater").attributes["release_notes"] - == RELEASE_NOTES - ) From 398db353349cd04df697ea5aaf34bb4a4f55302e Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 31 Mar 2022 16:57:52 +0200 Subject: [PATCH 0025/1224] Prevent issues with setting up "Timer" integration (unknown "restore" key) (#68936) * Prevent issues with setting up "Timer" for existing entities * Use default constant * Update homeassistant/components/timer/__init__.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/timer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 63ab766567d..215aa8577f7 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -207,7 +207,7 @@ class Timer(RestoreEntity): self._remaining: timedelta | None = None self._end: datetime | None = None self._listener: Callable[[], None] | None = None - self._restore: bool = self._config[CONF_RESTORE] + self._restore: bool = self._config.get(CONF_RESTORE, DEFAULT_RESTORE) self._attr_should_poll = False self._attr_force_update = True From 0f6296e4b520ec8daf0f12e7b6db3c863c811ae8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 31 Mar 2022 11:26:27 -0400 Subject: [PATCH 0026/1224] Bump zigpy to 0.44.1 and zha-quirks to 0.0.69 (#68921) * Make unit tests pass * Flip response type check to not rely on it being a list https://github.com/zigpy/zigpy/pull/716#issuecomment-1025236190 * Bump zigpy and quirks versions to ZCLR8 releases * Fix renamed zigpy cluster attributes * Handle the default response for ZLL `get_group_identifiers` * Add more error context to `stage failed` errors * Fix unit test returning lists as ZCL request responses * Always load quirks when testing ZHA * Bump zha-quirks to 0.0.69 --- homeassistant/components/zha/api.py | 6 +-- homeassistant/components/zha/climate.py | 6 +-- .../components/zha/core/channels/__init__.py | 4 +- .../components/zha/core/channels/base.py | 34 +++++++----- .../components/zha/core/channels/closures.py | 7 +-- .../components/zha/core/channels/general.py | 14 +++-- .../components/zha/core/channels/hvac.py | 18 +++---- .../components/zha/core/channels/lightlink.py | 8 ++- .../components/zha/core/channels/security.py | 6 +-- .../zha/core/channels/smartenergy.py | 4 +- homeassistant/components/zha/core/device.py | 4 +- homeassistant/components/zha/core/group.py | 8 +-- homeassistant/components/zha/core/helpers.py | 10 ++-- homeassistant/components/zha/cover.py | 16 +++--- homeassistant/components/zha/entity.py | 4 +- homeassistant/components/zha/light.py | 10 ++-- homeassistant/components/zha/lock.py | 4 +- homeassistant/components/zha/manifest.json | 4 +- homeassistant/components/zha/switch.py | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/zha/common.py | 53 +++++++++++++------ tests/components/zha/conftest.py | 14 +++++ tests/components/zha/test_api.py | 2 +- tests/components/zha/test_channels.py | 26 +++++++-- tests/components/zha/test_climate.py | 11 ++-- tests/components/zha/test_cover.py | 8 +-- tests/components/zha/test_discover.py | 2 +- tests/components/zha/test_light.py | 39 +++++++++++--- tests/components/zha/test_sensor.py | 4 +- tests/components/zha/test_switch.py | 32 +++++++++-- 31 files changed, 248 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 552be260e8b..c42384682da 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -650,7 +650,7 @@ async def websocket_device_cluster_attributes( ) if attributes is not None: for attr_id, attr in attributes.items(): - cluster_attributes.append({ID: attr_id, ATTR_NAME: attr[0]}) + cluster_attributes.append({ID: attr_id, ATTR_NAME: attr.name}) _LOGGER.debug( "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", ATTR_CLUSTER_ID, @@ -700,7 +700,7 @@ async def websocket_device_cluster_commands( { TYPE: CLIENT, ID: cmd_id, - ATTR_NAME: cmd[0], + ATTR_NAME: cmd.name, } ) for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items(): @@ -708,7 +708,7 @@ async def websocket_device_cluster_commands( { TYPE: CLUSTER_COMMAND_SERVER, ID: cmd_id, - ATTR_NAME: cmd[0], + ATTR_NAME: cmd.name, } ) _LOGGER.debug( diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 8544c46e92e..06b1e8a47d8 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -161,9 +161,9 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - if self._thrm.local_temp is None: + if self._thrm.local_temperature is None: return None - return self._thrm.local_temp / ZCL_TEMP + return self._thrm.local_temperature / ZCL_TEMP @property def extra_state_attributes(self): @@ -272,7 +272,7 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def hvac_modes(self) -> tuple[str, ...]: """Return the list of available HVAC operation modes.""" - return SEQ_OF_OPERATION.get(self._thrm.ctrl_seqe_of_oper, (HVAC_MODE_OFF,)) + return SEQ_OF_OPERATION.get(self._thrm.ctrl_sequence_of_oper, (HVAC_MODE_OFF,)) @property def precision(self): diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index b63c20e14eb..2011f92a63b 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -346,7 +346,9 @@ class ChannelPool: results = await asyncio.gather(*tasks, return_exceptions=True) for channel, outcome in zip(channels, results): if isinstance(outcome, Exception): - channel.warning("'%s' stage failed: %s", func_name, str(outcome)) + channel.warning( + "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome + ) continue channel.debug("'%s' stage succeeded", func_name) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index f79000d0646..0dd6169373b 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -8,7 +8,7 @@ import logging from typing import Any import zigpy.exceptions -from zigpy.zcl.foundation import Status +from zigpy.zcl.foundation import ConfigureReportingResponseRecord, Status from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback @@ -111,7 +111,7 @@ class ZigbeeChannel(LogMixin): if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: attr = self.REPORT_CONFIG[0].get("attr") if isinstance(attr, str): - self.value_attribute = self.cluster.attridx.get(attr) + self.value_attribute = self.cluster.attributes_by_name.get(attr) else: self.value_attribute = attr self._status = ChannelStatus.CREATED @@ -260,7 +260,7 @@ class ZigbeeChannel(LogMixin): self, attrs: dict[int | str, tuple], res: list | tuple ) -> None: """Parse configure reporting result.""" - if not isinstance(res, list): + if isinstance(res, (Exception, ConfigureReportingResponseRecord)): # assume default response self.debug( "attr reporting for '%s' on '%s': %s", @@ -345,7 +345,7 @@ class ZigbeeChannel(LogMixin): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, - self.cluster.attributes.get(attrid, [attrid])[0], + self._get_attribute_name(attrid), value, ) @@ -368,6 +368,12 @@ class ZigbeeChannel(LogMixin): async def async_update(self): """Retrieve latest state from cluster.""" + def _get_attribute_name(self, attrid: int) -> str | int: + if attrid not in self.cluster.attributes: + return attrid + + return self.cluster.attributes[attrid].name + async def get_attribute_value(self, attribute, from_cache=True): """Get the value for an attribute.""" manufacturer = None @@ -421,11 +427,11 @@ class ZigbeeChannel(LogMixin): get_attributes = partialmethod(_get_attributes, False) - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:%s]: {msg}" args = (self._ch_pool.nwk, self._id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) def __getattr__(self, name): """Get attribute or a decorated cluster command.""" @@ -479,11 +485,11 @@ class ZDOChannel(LogMixin): """Configure channel.""" self._status = ChannelStatus.CONFIGURED - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:ZDO](%s): {msg}" args = (self._zha_device.nwk, self._zha_device.model) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ClientChannel(ZigbeeChannel): @@ -492,13 +498,17 @@ class ClientChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle an attribute updated on this cluster.""" + + try: + attr_name = self._cluster.attributes[attrid].name + except KeyError: + attr_name = "Unknown" + self.zha_send_event( SIGNAL_ATTR_UPDATED, { ATTR_ATTRIBUTE_ID: attrid, - ATTR_ATTRIBUTE_NAME: self._cluster.attributes.get(attrid, ["Unknown"])[ - 0 - ], + ATTR_ATTRIBUTE_NAME: attr_name, ATTR_VALUE: value, }, ) @@ -510,4 +520,4 @@ class ClientChannel(ZigbeeChannel): self._cluster.server_commands is not None and self._cluster.server_commands.get(command_id) is not None ): - self.zha_send_event(self._cluster.server_commands.get(command_id)[0], args) + self.zha_send_event(self._cluster.server_commands[command_id].name, args) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index c63d069767d..bf50c8fc4ba 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -33,7 +33,8 @@ class DoorLockChannel(ZigbeeChannel): ): return - command_name = self._cluster.client_commands.get(command_id, [command_id])[0] + command_name = self._cluster.client_commands[command_id].name + if command_name == "operation_event_notification": self.zha_send_event( command_name, @@ -47,7 +48,7 @@ class DoorLockChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update from lock cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -140,7 +141,7 @@ class WindowCovering(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update from window_covering cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 89d750465b8..09a1fd80f17 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -103,7 +103,7 @@ class AnalogOutput(ZigbeeChannel): except zigpy.exceptions.ZigbeeException as ex: self.error("Could not set value: %s", ex) return False - if isinstance(res, list) and all( + if not isinstance(res, Exception) and all( record.status == Status.SUCCESS for record in res[0] ): return True @@ -380,7 +380,11 @@ class Ota(ZigbeeChannel): self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle OTA commands.""" - cmd_name = self.cluster.server_commands.get(command_id, [command_id])[0] + if command_id in self.cluster.server_commands: + cmd_name = self.cluster.server_commands[command_id].name + else: + cmd_name = command_id + signal_id = self._ch_pool.unique_id.split("-")[0] if cmd_name == "query_next_image": self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) @@ -418,7 +422,11 @@ class PollControl(ZigbeeChannel): self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle commands received to this cluster.""" - cmd_name = self.cluster.client_commands.get(command_id, [command_id])[0] + if command_id in self.cluster.client_commands: + cmd_name = self.cluster.client_commands[command_id].name + else: + cmd_name = command_id + self.debug("Received %s tsn command '%s': %s", tsn, cmd_name, args) self.zha_send_event(cmd_name, args) if cmd_name == "checkin": diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 726d9f15376..5b102d062cb 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -70,7 +70,7 @@ class FanChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid: int, value: Any) -> None: """Handle attribute update from fan cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -90,7 +90,7 @@ class ThermostatChannel(ZigbeeChannel): """Thermostat channel.""" REPORT_CONFIG = ( - {"attr": "local_temp", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "local_temperature", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "unoccupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, @@ -107,7 +107,7 @@ class ThermostatChannel(ZigbeeChannel): "abs_max_heat_setpoint_limit": True, "abs_min_cool_setpoint_limit": True, "abs_max_cool_setpoint_limit": True, - "ctrl_seqe_of_oper": False, + "ctrl_sequence_of_oper": False, "max_cool_setpoint_limit": True, "max_heat_setpoint_limit": True, "min_cool_setpoint_limit": True, @@ -135,9 +135,9 @@ class ThermostatChannel(ZigbeeChannel): return self.cluster.get("abs_min_heat_setpoint_limit", 700) @property - def ctrl_seqe_of_oper(self) -> int: + def ctrl_sequence_of_oper(self) -> int: """Control Sequence of operations attribute.""" - return self.cluster.get("ctrl_seqe_of_oper", 0xFF) + return self.cluster.get("ctrl_sequence_of_oper", 0xFF) @property def max_cool_setpoint_limit(self) -> int: @@ -172,9 +172,9 @@ class ThermostatChannel(ZigbeeChannel): return sp_limit @property - def local_temp(self) -> int | None: + def local_temperature(self) -> int | None: """Thermostat temperature.""" - return self.cluster.get("local_temp") + return self.cluster.get("local_temperature") @property def occupancy(self) -> int | None: @@ -229,7 +229,7 @@ class ThermostatChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -300,7 +300,7 @@ class ThermostatChannel(ZigbeeChannel): @staticmethod def check_result(res: list) -> bool: """Normalize the result.""" - if not isinstance(res, list): + if isinstance(res, Exception): return False return all(record.status == Status.SUCCESS for record in res[0]) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 46c40fdaff0..a29d9020a75 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -3,6 +3,7 @@ import asyncio import zigpy.exceptions from zigpy.zcl.clusters import lightlink +from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand from .. import registries from .base import ChannelStatus, ZigbeeChannel @@ -30,11 +31,16 @@ class LightLink(ZigbeeChannel): return try: - _, _, groups = await self.cluster.get_group_identifiers(0) + rsp = await self.cluster.get_group_identifiers(0) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc: self.warning("Couldn't get list of groups: %s", str(exc)) return + if isinstance(rsp, GENERAL_COMMANDS[GeneralCommand.Default_Response].schema): + groups = [] + else: + groups = rsp.group_info_records + if groups: for group in groups: self.debug("Adding coordinator to 0x%04x group id", group.group_id) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 8aa5c620656..19be861178f 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -85,7 +85,7 @@ class IasAce(ZigbeeChannel): def cluster_command(self, tsn, command_id, args) -> None: """Handle commands received to this cluster.""" self.warning( - "received command %s", self._cluster.server_commands.get(command_id)[NAME] + "received command %s", self._cluster.server_commands[command_id].name ) self.command_map[command_id](*args) @@ -94,7 +94,7 @@ class IasAce(ZigbeeChannel): mode = AceCluster.ArmMode(arm_mode) self.zha_send_event( - self._cluster.server_commands.get(IAS_ACE_ARM)[NAME], + self._cluster.server_commands[IAS_ACE_ARM].name, { "arm_mode": mode.value, "arm_mode_description": mode.name, @@ -190,7 +190,7 @@ class IasAce(ZigbeeChannel): def _bypass(self, zone_list, code) -> None: """Handle the IAS ACE bypass command.""" self.zha_send_event( - self._cluster.server_commands.get(IAS_ACE_BYPASS)[NAME], + self._cluster.server_commands[IAS_ACE_BYPASS].name, {"zone_list": zone_list, "code": code}, ) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 5877dad14fa..b153372a322 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -65,7 +65,7 @@ class Metering(ZigbeeChannel): "divisor": True, "metering_device_type": True, "multiplier": True, - "summa_formatting": True, + "summation_formatting": True, "unit_of_measure": True, } @@ -159,7 +159,7 @@ class Metering(ZigbeeChannel): self._format_spec = self.get_formatting(fmting) fmting = self.cluster.get( - "summa_formatting", 0xF9 + "summation_formatting", 0xF9 ) # 1 digit to the right, 15 digits to the left self._summa_format = self.get_formatting(fmting) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 79cc54c4829..e80a0725cc1 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -783,8 +783,8 @@ class ZHADevice(LogMixin): fmt = f"{log_msg[1]} completed: %s" zdo.debug(fmt, *(log_msg[2] + (outcome,))) - def log(self, level: int, msg: str, *args: Any) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs: dict) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 93e96c7565b..af17f28e622 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -108,11 +108,11 @@ class ZHAGroupMember(LogMixin): str(ex), ) - def log(self, level: int, msg: str, *args: Any) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ZHAGroup(LogMixin): @@ -224,8 +224,8 @@ class ZHAGroup(LogMixin): group_info["members"] = [member.member_info for member in self.members] return group_info - def log(self, level: int, msg: str, *args: Any) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.name, self.group_id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 5e98799e387..fcd29c1619f 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -210,23 +210,23 @@ def reduce_attribute( class LogMixin: """Log helper.""" - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log with level.""" raise NotImplementedError - def debug(self, msg, *args): + def debug(self, msg, *args, **kwargs): """Debug level log.""" return self.log(logging.DEBUG, msg, *args) - def info(self, msg, *args): + def info(self, msg, *args, **kwargs): """Info level log.""" return self.log(logging.INFO, msg, *args) - def warning(self, msg, *args): + def warning(self, msg, *args, **kwargs): """Warning method log.""" return self.log(logging.WARNING, msg, *args) - def error(self, msg, *args): + def error(self, msg, *args, **kwargs): """Error level log.""" return self.log(logging.ERROR, msg, *args) diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 9f62d4b9c02..0fdb4daeaa5 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -133,20 +133,20 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the window cover.""" res = await self._cover_channel.up_open() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_OPENING) async def async_close_cover(self, **kwargs): """Close the window cover.""" res = await self._cover_channel.down_close() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_CLOSING) async def async_set_cover_position(self, **kwargs): """Move the roller shutter to a specific position.""" new_pos = kwargs[ATTR_POSITION] res = await self._cover_channel.go_to_lift_percentage(100 - new_pos) - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state( STATE_CLOSING if new_pos < self._current_position else STATE_OPENING ) @@ -154,7 +154,7 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the window cover.""" res = await self._cover_channel.stop() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED self.async_write_ha_state() @@ -250,7 +250,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the window cover.""" res = await self._on_off_channel.on() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't open cover: %s", res) return @@ -260,7 +260,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Close the window cover.""" res = await self._on_off_channel.off() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't open cover: %s", res) return @@ -274,7 +274,7 @@ class Shade(ZhaEntity, CoverEntity): new_pos * 255 / 100, 1 ) - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't set cover's position: %s", res) return @@ -284,7 +284,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" res = await self._level_channel.stop() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't stop cover: %s", res) return diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 4a9b0f7577c..13e43aa9ff0 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -139,11 +139,11 @@ class BaseZhaEntity(LogMixin, entity.Entity): ) self._unsubs.append(unsub) - def log(self, level: int, msg: str, *args): + def log(self, level: int, msg: str, *args, **kwargs): """Log a message.""" msg = f"%s: {msg}" args = (self.entity_id,) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ZhaEntity(BaseZhaEntity, RestoreEntity): diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6855db22572..b6d344a57e7 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -243,7 +243,7 @@ class BaseLight(LogMixin, light.LightEntity): level, duration ) t_log["move_to_level_with_on_off"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = bool(level) @@ -255,7 +255,7 @@ class BaseLight(LogMixin, light.LightEntity): # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True @@ -266,7 +266,7 @@ class BaseLight(LogMixin, light.LightEntity): temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp(temperature, duration) t_log["move_to_color_temp"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._color_temp = temperature @@ -282,7 +282,7 @@ class BaseLight(LogMixin, light.LightEntity): int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration ) t_log["move_to_color"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._hs_color = hs_color @@ -340,7 +340,7 @@ class BaseLight(LogMixin, light.LightEntity): else: result = await self._on_off_channel.off() self.debug("turned off: %s", result) - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 341cfcebf68..1ebb10cacb6 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -122,7 +122,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_lock(self, **kwargs): """Lock the lock.""" result = await self._doorlock_channel.lock_door() - if not isinstance(result, list) or result[0] is not Status.SUCCESS: + if isinstance(result, Exception) or result[0] is not Status.SUCCESS: self.error("Error with lock_door: %s", result) return self.async_write_ha_state() @@ -130,7 +130,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_unlock(self, **kwargs): """Unlock the lock.""" result = await self._doorlock_channel.unlock_door() - if not isinstance(result, list) or result[0] is not Status.SUCCESS: + if isinstance(result, Exception) or result[0] is not Status.SUCCESS: self.error("Error with unlock_door: %s", result) return self.async_write_ha_state() diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e542c77516e..6d47535b765 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,9 +7,9 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.67", + "zha-quirks==0.0.69", "zigpy-deconz==0.14.0", - "zigpy==0.43.0", + "zigpy==0.44.1", "zigpy-xbee==0.14.0", "zigpy-zigate==0.8.0", "zigpy-znp==0.7.0" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 29fb08b9bc0..87d2407c2dc 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -65,7 +65,7 @@ class BaseSwitch(SwitchEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" result = await self._on_off_channel.on() - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = True self.async_write_ha_state() @@ -73,7 +73,7 @@ class BaseSwitch(SwitchEntity): async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" result = await self._on_off_channel.off() - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 63d5920ea52..445dc6aa49f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2469,7 +2469,7 @@ zengge==0.2 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.67 +zha-quirks==0.0.69 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2490,7 +2490,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.43.0 +zigpy==0.44.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2ecacfb2e4..5d132c38fc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1595,7 +1595,7 @@ youless-api==0.16 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.67 +zha-quirks==0.0.69 # homeassistant.components.zha zigpy-deconz==0.14.0 @@ -1610,7 +1610,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.43.0 +zigpy==0.44.1 # homeassistant.components.zwave_js zwave-js-server-python==0.35.2 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 48772d31fb6..757587071fd 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -20,8 +20,10 @@ def patch_cluster(cluster): value = cluster.PLUGGED_ATTR_READS.get(attr_id) if value is None: # try converting attr_id to attr_name and lookup the plugs again - attr_name = cluster.attributes.get(attr_id) - value = attr_name and cluster.PLUGGED_ATTR_READS.get(attr_name[0]) + attr = cluster.attributes.get(attr_id) + + if attr is not None: + value = cluster.PLUGGED_ATTR_READS.get(attr.name) if value is not None: result.append( zcl_f.ReadAttributeRecord( @@ -58,14 +60,23 @@ def patch_cluster(cluster): def update_attribute_cache(cluster): """Update attribute cache based on plugged attributes.""" - if cluster.PLUGGED_ATTR_READS: - attrs = [ - make_attribute(cluster.attridx.get(attr, attr), value) - for attr, value in cluster.PLUGGED_ATTR_READS.items() - ] - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - hdr.frame_control.disable_default_response = True - cluster.handle_message(hdr, [attrs]) + if not cluster.PLUGGED_ATTR_READS: + return + + attrs = [] + for attrid, value in cluster.PLUGGED_ATTR_READS.items(): + if isinstance(attrid, str): + attrid = cluster.attributes_by_name[attrid].id + else: + attrid = zigpy.types.uint16_t(attrid) + attrs.append(make_attribute(attrid, value)) + + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + hdr.frame_control.disable_default_response = True + msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( + attribute_reports=attrs + ) + cluster.handle_message(hdr, msg) def get_zha_gateway(hass): @@ -96,13 +107,23 @@ async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: d This is to simulate the normal device communication that happens when a device is paired to the zigbee network. """ - attrs = [ - make_attribute(cluster.attridx.get(attr, attr), value) - for attr, value in attributes.items() - ] - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + attrs = [] + + for attrid, value in attributes.items(): + if isinstance(attrid, str): + attrid = cluster.attributes_by_name[attrid].id + else: + attrid = zigpy.types.uint16_t(attrid) + + attrs.append(make_attribute(attrid, value)) + + msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( + attribute_reports=attrs + ) + + hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes) hdr.frame_control.disable_default_response = True - cluster.handle_message(hdr, [attrs]) + cluster.handle_message(hdr, msg) await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index fd138567367..0e969b1b0f3 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -27,6 +27,20 @@ FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" +@pytest.fixture(scope="session", autouse=True) +def globally_load_quirks(): + """Load quirks automatically so that ZHA tests run deterministically in isolation. + + If portions of the ZHA test suite that do not happen to load quirks are run + independently, bugs can emerge that will show up only when more of the test suite is + run. + """ + + import zhaquirks + + zhaquirks.setup() + + @pytest.fixture def zigpy_app_controller(): """Zigpy ApplicationController fixture.""" diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 4e97f35bf1d..dac9855148a 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -145,7 +145,7 @@ async def test_device_cluster_attributes(zha_client): msg = await zha_client.receive_json() attributes = msg["result"] - assert len(attributes) == 5 + assert len(attributes) == 7 for attribute in attributes: assert attribute[ID] is not None diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 8eafdc451cc..79b8dbc6a71 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -130,7 +130,7 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): 0x0201, 1, { - "local_temp", + "local_temperature", "occupied_cooling_setpoint", "occupied_heating_setpoint", "unoccupied_cooling_setpoint", @@ -586,13 +586,23 @@ async def test_zll_device_groups( cluster = zigpy_zll_device.endpoints[1].lightlink channel = zha_channels.lightlink.LightLink(cluster, channel_pool) + get_group_identifiers_rsp = zigpy.zcl.clusters.lightlink.LightLink.commands_by_name[ + "get_group_identifiers_rsp" + ].schema + with patch.object( - cluster, "command", AsyncMock(return_value=[1, 0, []]) + cluster, + "command", + AsyncMock( + return_value=get_group_identifiers_rsp( + total=0, start_index=0, group_info_records=[] + ) + ), ) as cmd_mock: await channel.async_configure() assert cmd_mock.await_count == 1 assert ( - cluster.server_commands[cmd_mock.await_args[0][0]][0] + cluster.server_commands[cmd_mock.await_args[0][0]].name == "get_group_identifiers" ) assert cluster.bind.call_count == 0 @@ -603,12 +613,18 @@ async def test_zll_device_groups( group_1 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xABCD, 0x00) group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00) with patch.object( - cluster, "command", AsyncMock(return_value=[1, 0, [group_1, group_2]]) + cluster, + "command", + AsyncMock( + return_value=get_group_identifiers_rsp( + total=2, start_index=0, group_info_records=[group_1, group_2] + ) + ), ) as cmd_mock: await channel.async_configure() assert cmd_mock.await_count == 1 assert ( - cluster.server_commands[cmd_mock.await_args[0][0]][0] + cluster.server_commands[cmd_mock.await_args[0][0]].name == "get_group_identifiers" ) assert cluster.bind.call_count == 0 diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 9f856ca1df6..fbf18ff9004 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -6,6 +6,7 @@ import pytest import zhaquirks.sinope.thermostat import zhaquirks.tuya.ts0601_trv import zigpy.profiles +import zigpy.types import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat import zigpy.zcl.foundation as zcl_f @@ -162,8 +163,8 @@ ZCL_ATTR_PLUG = { "abs_max_heat_setpoint_limit": 3000, "abs_min_cool_setpoint_limit": 2000, "abs_max_cool_setpoint_limit": 4000, - "ctrl_seqe_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating, - "local_temp": None, + "ctrl_sequence_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating, + "local_temperature": None, "max_cool_setpoint_limit": 3900, "max_heat_setpoint_limit": 2900, "min_cool_setpoint_limit": 2100, @@ -268,7 +269,7 @@ def test_sequence_mappings(): assert Thermostat.SystemMode(HVAC_MODE_2_SYSTEM[hvac_mode]) is not None -async def test_climate_local_temp(hass, device_climate): +async def test_climate_local_temperature(hass, device_climate): """Test local temperature.""" thrm_cluster = device_climate.device.endpoints[1].thermostat @@ -517,7 +518,7 @@ async def test_hvac_modes(hass, device_climate_mock, seq_of_op, modes): """Test HVAC modes from sequence of operations.""" device_climate = await device_climate_mock( - CLIMATE, {"ctrl_seqe_of_oper": seq_of_op} + CLIMATE, {"ctrl_sequence_of_oper": seq_of_op} ) entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) @@ -1119,7 +1120,7 @@ async def test_occupancy_reset(hass, device_climate_sinope): assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY await send_attributes_report( - hass, thrm_cluster, {"occupied_heating_setpoint": 1950} + hass, thrm_cluster, {"occupied_heating_setpoint": zigpy.types.uint16_t(1950)} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 73ab38c27ac..60a4fab25be 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -146,7 +146,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x01 - assert cluster.request.call_args[0][2] == () + assert cluster.request.call_args[0][2].command.name == "down_close" assert cluster.request.call_args[1]["expect_reply"] is True # open from UI @@ -159,7 +159,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x00 - assert cluster.request.call_args[0][2] == () + assert cluster.request.call_args[0][2].command.name == "up_open" assert cluster.request.call_args[1]["expect_reply"] is True # set position UI @@ -175,7 +175,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x05 - assert cluster.request.call_args[0][2] == (zigpy.types.uint8_t,) + assert cluster.request.call_args[0][2].command.name == "go_to_lift_percentage" assert cluster.request.call_args[0][3] == 53 assert cluster.request.call_args[1]["expect_reply"] is True @@ -189,7 +189,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x02 - assert cluster.request.call_args[0][2] == () + assert cluster.request.call_args[0][2].command.name == "stop" assert cluster.request.call_args[1]["expect_reply"] is True # test rejoin diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 9953b6e9d15..93a50c77c90 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -120,7 +120,7 @@ async def test_devices( assert cluster_identify.request.call_args == mock.call( False, 64, - (zigpy.types.uint8_t, zigpy.types.uint8_t), + cluster_identify.commands_by_name["trigger_effect"].schema, 2, 0, expect_reply=True, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 9c35215c889..4ac777f5d8e 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, call, patch, sentinel import pytest import zigpy.profiles.zha as zha -import zigpy.types import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f @@ -336,7 +335,13 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) await async_test_off_from_hass(hass, cluster, entity_id) @@ -353,7 +358,13 @@ async def async_test_off_from_hass(hass, cluster, entity_id): assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + OFF, + cluster.commands_by_name["off"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) @@ -373,7 +384,13 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_count == 0 assert level_cluster.request.await_count == 0 assert on_off_cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + on_off_cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() @@ -389,12 +406,18 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_count == 1 assert level_cluster.request.await_count == 1 assert on_off_cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + on_off_cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) assert level_cluster.request.call_args == call( False, 4, - (zigpy.types.uint8_t, zigpy.types.uint16_t), + level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 254, 100.0, expect_reply=True, @@ -419,7 +442,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (zigpy.types.uint8_t, zigpy.types.uint16_t), + level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, 1, expect_reply=True, @@ -462,7 +485,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): assert cluster.request.call_args == call( False, 64, - (zigpy.types.uint8_t, zigpy.types.uint8_t), + cluster.commands_by_name["trigger_effect"].schema, FLASH_EFFECTS[flash], 0, expect_reply=True, diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index c4e66e98098..03a88c3560e 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -307,7 +307,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): "metering_device_type": 0x00, "multiplier": 1, "status": 0x00, - "summa_formatting": 0b1_0111_010, + "summation_formatting": 0b1_0111_010, "unit_of_measure": 0x01, }, {"instaneneous_demand"}, @@ -814,7 +814,7 @@ async def test_se_summation_uom( "metering_device_type": 0x00, "multiplier": 1, "status": 0x00, - "summa_formatting": 0b1_0111_010, + "summation_formatting": 0b1_0111_010, "unit_of_measure": raw_uom, } await zha_device_joined(zigpy_device) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index c5cdf1a96f1..a624e5f2c73 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -141,7 +141,13 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) # turn off from HA @@ -155,7 +161,13 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + OFF, + cluster.commands_by_name["off"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) # test joining a new switch to the network and HA @@ -224,7 +236,13 @@ async def test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + group_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) assert hass.states.get(entity_id).state == STATE_ON @@ -239,7 +257,13 @@ async def test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + OFF, + group_cluster_on_off.commands_by_name["off"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) assert hass.states.get(entity_id).state == STATE_OFF From b8e4784d4ad4039d38e9242ed0ac76429b3ffc9e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 31 Mar 2022 10:52:07 -0600 Subject: [PATCH 0027/1224] Bump aioguardian to 2022.03.2 (#68916) * Bump aioguardian to 2022.03.0 * Another bump * Another bump Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/guardian/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 9a663dc104e..fe9a453a166 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==2021.11.0"], + "requirements": ["aioguardian==2022.03.2"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 445dc6aa49f..ad1e2d69726 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioftp==0.12.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2021.11.0 +aioguardian==2022.03.2 # homeassistant.components.harmony aioharmony==0.2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d132c38fc5..0d80011811d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioflo==2021.11.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2021.11.0 +aioguardian==2022.03.2 # homeassistant.components.harmony aioharmony==0.2.9 From f0740ce73aa3f0119b05c9349bc21b3a9a7e9b8d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 31 Mar 2022 18:58:49 +0200 Subject: [PATCH 0028/1224] bump pynetgear to 0.9.2 (#68986) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index b2c7ddf6be2..932535f68f6 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.9.1"], + "requirements": ["pynetgear==0.9.2"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index ad1e2d69726..0d481ebfcb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.1 +pynetgear==0.9.2 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d80011811d..b14862d25b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1100,7 +1100,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.1 +pynetgear==0.9.2 # homeassistant.components.nina pynina==0.1.7 From 86bec82c24b9d8ec9ffccaa8dda38131421bfc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 31 Mar 2022 19:58:56 +0200 Subject: [PATCH 0029/1224] Update aioairzone to v0.3.1 (#68975) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_config_flow.py | 8 +++++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 3f8fbc5647b..236d21c81dd 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.2.3"], + "requirements": ["aioairzone==0.3.1"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 0d481ebfcb4..5157f5bc8c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.airzone -aioairzone==0.2.3 +aioairzone==0.3.1 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b14862d25b4..eccf67a9c42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.airzone -aioairzone==0.2.3 +aioairzone==0.3.1 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index a6612d6de9c..08eb35ef52b 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from aiohttp.client_exceptions import ClientConnectorError +from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError from homeassistant import data_entry_flow from homeassistant.components.airzone.const import DOMAIN @@ -23,6 +23,12 @@ async def test_form(hass): ) as mock_setup_entry, patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=ClientResponseError(MagicMock(), MagicMock()), + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=ClientResponseError(MagicMock(), MagicMock()), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} From 70922f9733940b09a99b1d3c33797a168258e3c2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 31 Mar 2022 11:59:40 -0600 Subject: [PATCH 0030/1224] Bump simplisafe-python to 2022.03.3 (#68990) --- 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 0bea1b6b33b..175291d96a6 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==2022.03.2"], + "requirements": ["simplisafe-python==2022.03.3"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5157f5bc8c6..43e1704ac9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2138,7 +2138,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.03.2 +simplisafe-python==2022.03.3 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eccf67a9c42..4009805767b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1378,7 +1378,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.03.2 +simplisafe-python==2022.03.3 # homeassistant.components.slack slackclient==2.5.0 From 3bc2586874051dbca9a703898f7a46a9be6a5a20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:10:16 -0700 Subject: [PATCH 0031/1224] Don't log the stack trace (#69000) --- homeassistant/components/kodi/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 7281057ac7e..a07ee14137a 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -447,7 +447,7 @@ class KodiEntity(MediaPlayerEntity): except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): if not self._connect_error: self._connect_error = True - _LOGGER.error("Unable to connect to Kodi via websocket", exc_info=True) + _LOGGER.error("Unable to connect to Kodi via websocket") await self._clear_connection(False) async def _ping(self): @@ -456,7 +456,7 @@ class KodiEntity(MediaPlayerEntity): except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): if not self._connect_error: self._connect_error = True - _LOGGER.error("Unable to ping Kodi via websocket", exc_info=True) + _LOGGER.error("Unable to ping Kodi via websocket") await self._clear_connection() async def _async_connect_websocket_if_disconnected(self, *_): From 88c9233d5064f751458220024b338b042b285513 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:12:02 -0700 Subject: [PATCH 0032/1224] Remove deprecated Switchbot import (#69002) --- .../components/switchbot/config_flow.py | 13 ---- homeassistant/components/switchbot/switch.py | 62 ++----------------- tests/components/switchbot/__init__.py | 12 +--- .../components/switchbot/test_config_flow.py | 36 ++--------- 4 files changed, 14 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 2d4e61bada5..70e032414a7 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -131,19 +131,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Handle config import from yaml.""" - _LOGGER.debug("import config: %s", import_config) - - import_config[CONF_MAC] = import_config[CONF_MAC].replace("-", ":").lower() - - await self.async_set_unique_id(import_config[CONF_MAC].replace(":", "")) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=import_config[CONF_NAME], data=import_config - ) - class SwitchbotOptionsFlowHandler(OptionsFlow): """Handle Switchbot options.""" diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index de66a437dee..b5507594521 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -5,27 +5,15 @@ import logging from typing import Any from switchbot import Switchbot # pylint: disable=import-error -import voluptuous as vol -from homeassistant.components.switch import ( - PLATFORM_SCHEMA, - SwitchDeviceClass, - SwitchEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_MAC, - CONF_NAME, - CONF_PASSWORD, - CONF_SENSOR_TYPE, - STATE_ON, -) +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ATTR_BOT, CONF_RETRY_COUNT, DATA_COORDINATOR, DEFAULT_NAME, DOMAIN +from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -33,46 +21,6 @@ from .entity import SwitchbotEntity _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import yaml config and initiates config flow for Switchbot devices.""" - _LOGGER.warning( - "Configuration of the Switchbot switch platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - - # Check if entry config exists and skips import if it does. - if hass.config_entries.async_entries(DOMAIN): - return - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_NAME: config[CONF_NAME], - CONF_PASSWORD: config.get(CONF_PASSWORD, None), - CONF_MAC: config[CONF_MAC].replace("-", ":").lower(), - CONF_SENSOR_TYPE: ATTR_BOT, - }, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 5d01a8d0d68..376406ac50c 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1,7 +1,7 @@ """Tests for the switchbot integration.""" from unittest.mock import patch -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -38,15 +38,9 @@ USER_INPUT_INVALID = { CONF_MAC: "invalid-mac", } -YAML_CONFIG = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", - CONF_SENSOR_TYPE: "bot", -} - -def _patch_async_setup_entry(return_value=True): +def patch_async_setup_entry(return_value=True): + """Patch async setup entry to return True.""" return patch( "homeassistant.components.switchbot.async_setup_entry", return_value=return_value, diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index edd35238034..59871681dfe 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.switchbot.const import ( CONF_SCAN_TIMEOUT, CONF_TIME_BETWEEN_UPDATE_COMMAND, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -15,13 +15,7 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import ( - USER_INPUT, - USER_INPUT_CURTAIN, - YAML_CONFIG, - _patch_async_setup_entry, - init_integration, -) +from . import USER_INPUT, USER_INPUT_CURTAIN, init_integration, patch_async_setup_entry DOMAIN = "switchbot" @@ -36,7 +30,7 @@ async def test_user_form_valid_mac(hass): assert result["step_id"] == "user" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -63,7 +57,7 @@ async def test_user_form_valid_mac(hass): assert result["step_id"] == "user" assert result["errors"] == {} - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_CURTAIN, @@ -90,24 +84,6 @@ async def test_user_form_valid_mac(hass): assert result["reason"] == "no_unconfigured_devices" -async def test_async_step_import(hass): - """Test the config import flow.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_MAC: "e7:89:43:99:99:99", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_SENSOR_TYPE: "bot", - } - - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_user_form_exception(hass, switchbot_config_flow): """Test we handle exception on user form.""" @@ -132,7 +108,7 @@ async def test_user_form_exception(hass, switchbot_config_flow): async def test_options_flow(hass): """Test updating options.""" - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) @@ -161,7 +137,7 @@ async def test_options_flow(hass): # Test changing of entry options. - with _patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) From d5f4e512e9448a0ad37da55871754d01d910b29d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:16:55 -0700 Subject: [PATCH 0033/1224] Solax: remove deprecated YAML import (#69003) --- homeassistant/components/solax/config_flow.py | 11 ----- homeassistant/components/solax/sensor.py | 41 +------------------ tests/components/solax/test_config_flow.py | 39 ------------------ 3 files changed, 2 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/solax/config_flow.py b/homeassistant/components/solax/config_flow.py index 5c4ef05da4b..56c6989cc7f 100644 --- a/homeassistant/components/solax/config_flow.py +++ b/homeassistant/components/solax/config_flow.py @@ -63,14 +63,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle import of solax config from YAML.""" - - import_data = { - CONF_IP_ADDRESS: config[CONF_IP_ADDRESS], - CONF_PORT: config[CONF_PORT], - CONF_PASSWORD: DEFAULT_PASSWORD, - } - - return await self.async_step_user(user_input=import_data) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 6f1b5ef6cf3..7f9d81ac9b0 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -3,38 +3,25 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging from solax.inverter import InverterError -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN, MANUFACTURER -_LOGGER = logging.getLogger(__name__) - DEFAULT_PORT = 80 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }, -) SCAN_INTERVAL = timedelta(seconds=30) @@ -81,30 +68,6 @@ async def async_setup_entry( async_add_entities(devices) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Platform setup.""" - - _LOGGER.warning( - "Configuration of the SolaX Power platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - class RealTimeDataEndpoint: """Representation of a Sensor.""" diff --git a/tests/components/solax/test_config_flow.py b/tests/components/solax/test_config_flow.py index 56db8d3f6cb..cb658405860 100644 --- a/tests/components/solax/test_config_flow.py +++ b/tests/components/solax/test_config_flow.py @@ -90,42 +90,3 @@ async def test_form_unknown_error(hass): assert entry_result["type"] == "form" assert entry_result["errors"] == {"base": "unknown"} - - -async def test_import_success(hass): - """Test import success.""" - conf = {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80} - with patch( - "homeassistant.components.solax.config_flow.real_time_api", - return_value=__mock_real_time_api_success(), - ), patch("solax.RealTimeAPI.get_data", return_value=__mock_get_data()), patch( - "homeassistant.components.solax.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - entry_result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf - ) - - assert entry_result["type"] == "create_entry" - assert entry_result["title"] == "ABCDEFGHIJ" - assert entry_result["data"] == { - CONF_IP_ADDRESS: "192.168.1.87", - CONF_PORT: 80, - CONF_PASSWORD: "", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_error(hass): - """Test import success.""" - conf = {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80} - with patch( - "homeassistant.components.solax.config_flow.real_time_api", - side_effect=ConnectionError, - ): - entry_result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf - ) - - assert entry_result["type"] == "form" - assert entry_result["errors"] == {"base": "cannot_connect"} From 513b05c92740c56702c141ca3d6eae26d876cebf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:18:17 -0700 Subject: [PATCH 0034/1224] Nanoleaf: remove deprecated YAML import (#69004) --- .../components/nanoleaf/config_flow.py | 11 ---- homeassistant/components/nanoleaf/light.py | 40 +-------------- tests/components/nanoleaf/test_config_flow.py | 51 +------------------ 3 files changed, 2 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 6ae70b32d8e..ed63754697a 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -184,17 +184,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_setup_finish() - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle Nanoleaf configuration import.""" - self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) - _LOGGER.debug( - "Importing Nanoleaf on %s from your configuration.yaml", config[CONF_HOST] - ) - self.nanoleaf = Nanoleaf( - async_get_clientsession(self.hass), config[CONF_HOST], config[CONF_TOKEN] - ) - return await self.async_setup_finish() - async def async_setup_finish( self, discovery_integration_import: bool = False ) -> FlowResult: diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index ed3476c4576..1cf6bd4d8bf 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,12 +1,10 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations -import logging import math from typing import Any from aionanoleaf import Nanoleaf -import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,7 +12,6 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -22,12 +19,9 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -41,38 +35,6 @@ from .entity import NanoleafEntity RESERVED_EFFECTS = ("*Solid*", "*Static*", "*Dynamic*") DEFAULT_NAME = "Nanoleaf" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Nanoleaf light platform.""" - _LOGGER.warning( - "Configuration of the Nanoleaf integration in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: config[CONF_HOST], CONF_TOKEN: config[CONF_TOKEN]}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 305f88a2e90..0a2a262ce6f 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch -from aionanoleaf import InvalidToken, NanoleafException, Unauthorized, Unavailable +from aionanoleaf import InvalidToken, Unauthorized, Unavailable import pytest from homeassistant import config_entries @@ -302,55 +302,6 @@ async def test_reauth(hass: HomeAssistant) -> None: assert entry.data[CONF_TOKEN] == TEST_TOKEN -async def test_import_config(hass: HomeAssistant) -> None: - """Test configuration import.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf", - return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), - ), patch( - "homeassistant.components.nanoleaf.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, - ) - assert result["type"] == "create_entry" - assert result["title"] == TEST_NAME - assert result["data"] == { - CONF_HOST: TEST_HOST, - CONF_TOKEN: TEST_TOKEN, - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - "error, reason", - [ - (Unavailable, "cannot_connect"), - (InvalidToken, "invalid_token"), - (Exception, "unknown"), - ], -) -async def test_import_config_error( - hass: HomeAssistant, error: NanoleafException, reason: str -) -> None: - """Test configuration import with errors in setup_finish.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", - side_effect=error, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, - ) - assert result["type"] == "abort" - assert result["reason"] == reason - - @pytest.mark.parametrize( "source, type_in_discovery", [ From ce5d20eb8e386c23fce804099f573b8cd5b73a54 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:20:50 -0700 Subject: [PATCH 0035/1224] Update tradfri deprecation message (#69005) --- homeassistant/components/tradfri/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 1971b14b2be..0054b1d7bff 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -141,7 +141,7 @@ async def async_setup_entry( # https://www.home-assistant.io/integrations/tradfri/ _LOGGER.warning( "Importing of Tradfri groups has been deprecated due to stability issues " - "and will be removed in Home Assistant core 2022.4" + "and will be removed in Home Assistant core 2022.5" ) # No need to load groups if the user hasn't requested it groups_commands: Command = await api( From bb322a18bb6ecc7c48a71a35ba6821fc47b8ef7c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:23:52 -0700 Subject: [PATCH 0036/1224] Launch Library: remove deprecated YAML import (#69008) --- .../components/launch_library/config_flow.py | 5 --- .../components/launch_library/sensor.py | 36 +------------------ .../launch_library/test_config_flow.py | 20 +---------- 3 files changed, 2 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/launch_library/config_flow.py b/homeassistant/components/launch_library/config_flow.py index 1023bb84079..d57bc3b7d01 100644 --- a/homeassistant/components/launch_library/config_flow.py +++ b/homeassistant/components/launch_library/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any from homeassistant import config_entries -from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -27,7 +26,3 @@ class LaunchLibraryFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title="Launch Library", data=user_input) return self.async_show_form(step_id="user") - - async def async_step_import(self, conf: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - return await self.async_step_user(user_input={CONF_NAME: conf[CONF_NAME]}) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index fac62c9bb87..85183a2d616 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -4,25 +4,20 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -import logging from typing import Any from pylaunches.objects.event import Event from pylaunches.objects.launch import Launch -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -34,13 +29,6 @@ from .const import DOMAIN DEFAULT_NEXT_LAUNCH_NAME = "Next launch" -_LOGGER = logging.getLogger(__name__) - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NEXT_LAUNCH_NAME): cv.string} -) - @dataclass class LaunchLibrarySensorEntityDescriptionMixin: @@ -137,28 +125,6 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Launch Library configuration from yaml.""" - _LOGGER.warning( - "Configuration of the launch_library platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 1b8f2bde453..5b6cb85cfee 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -3,29 +3,11 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.launch_library.const import DOMAIN -from homeassistant.components.launch_library.sensor import DEFAULT_NEXT_LAUNCH_NAME -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from tests.common import MockConfigEntry -async def test_import(hass): - """Test entry will be imported.""" - - imported_config = {CONF_NAME: DEFAULT_NEXT_LAUNCH_NAME} - - with patch( - "homeassistant.components.launch_library.async_setup_entry", return_value=True - ): - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=imported_config - ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result.get("result").data == imported_config - - async def test_create_entry(hass): """Test we can finish a config flow.""" From 56998f219b92281b0058ae6d0df6665759a73071 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 11:25:01 -0700 Subject: [PATCH 0037/1224] Version: remove deprecated YAML import (#69010) --- homeassistant/components/version/sensor.py | 60 +----------- tests/components/version/test_sensor.py | 101 +-------------------- 2 files changed, 6 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index f0583a19068..82e49155603 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,69 +1,19 @@ """Sensor that can display the current Home Assistant versions.""" from __future__ import annotations -from typing import Any, Final +from typing import Any -import voluptuous as vol -from voluptuous.schema_builder import Schema - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType -from .const import ( - ATTR_SOURCE, - CONF_BETA, - CONF_IMAGE, - CONF_SOURCE, - DEFAULT_BETA, - DEFAULT_IMAGE, - DEFAULT_NAME, - DEFAULT_SOURCE, - DOMAIN, - LOGGER, - VALID_IMAGES, - VALID_SOURCES, -) +from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN from .coordinator import VersionDataUpdateCoordinator from .entity import VersionEntity -PLATFORM_SCHEMA: Final[Schema] = SENSOR_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_BETA, default=DEFAULT_BETA): cv.boolean, - vol.Optional(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(VALID_IMAGES), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SOURCE, default=DEFAULT_SOURCE): vol.In(VALID_SOURCES), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the legacy version sensor platform.""" - LOGGER.warning( - "Configuration of the Version platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={ATTR_SOURCE: SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 72e63820345..94b6f3f4c56 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,54 +1,12 @@ """The test for the version sensor platform.""" from __future__ import annotations -from typing import Any -from unittest.mock import patch - -from pyhaversion import HaVersionChannel, HaVersionSource from pyhaversion.exceptions import HaVersionException import pytest -from homeassistant.components.version.const import ( - CONF_BETA, - CONF_CHANNEL, - CONF_IMAGE, - CONF_VERSION_SOURCE, - DEFAULT_NAME_LATEST, - DOMAIN, - VERSION_SOURCE_DOCKER_HUB, - VERSION_SOURCE_VERSIONS, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from .common import ( - MOCK_VERSION, - MOCK_VERSION_DATA, - TEST_DEFAULT_IMPORT_CONFIG, - mock_get_version_update, - setup_version_integration, -) - - -async def async_setup_sensor_wrapper( - hass: HomeAssistant, config: dict[str, Any] -) -> ConfigEntry: - """Set up the Version sensor platform.""" - with patch( - "pyhaversion.HaVersion.get_version", - return_value=(MOCK_VERSION, MOCK_VERSION_DATA), - ): - assert await async_setup_component( - hass, "sensor", {"sensor": {"platform": DOMAIN, **config}} - ) - await hass.async_block_till_done() - - config_entries = hass.config_entries.async_entries(DOMAIN) - config_entry = config_entries[-1] - assert config_entry.source == "import" - return config_entry +from .common import MOCK_VERSION, mock_get_version_update, setup_version_integration async def test_version_sensor(hass: HomeAssistant): @@ -73,60 +31,3 @@ async def test_update(hass: HomeAssistant, caplog: pytest.LogCaptureFixture): await mock_get_version_update(hass, side_effect=HaVersionException) assert hass.states.get("sensor.local_installation").state == "unavailable" assert "Error fetching version data" in caplog.text - - -@pytest.mark.parametrize( - "yaml,converted", - ( - ( - {}, - TEST_DEFAULT_IMPORT_CONFIG, - ), - ( - {CONF_NAME: "test"}, - {**TEST_DEFAULT_IMPORT_CONFIG, CONF_NAME: "test"}, - ), - ( - {CONF_SOURCE: "hassio", CONF_IMAGE: "odroid-n2"}, - { - **TEST_DEFAULT_IMPORT_CONFIG, - CONF_NAME: DEFAULT_NAME_LATEST, - CONF_SOURCE: HaVersionSource.SUPERVISOR, - CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS, - CONF_IMAGE: "odroid-n2", - }, - ), - ( - {CONF_SOURCE: "docker"}, - { - **TEST_DEFAULT_IMPORT_CONFIG, - CONF_NAME: DEFAULT_NAME_LATEST, - CONF_SOURCE: HaVersionSource.CONTAINER, - CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB, - }, - ), - ( - {CONF_BETA: True}, - { - **TEST_DEFAULT_IMPORT_CONFIG, - CONF_CHANNEL: HaVersionChannel.BETA, - }, - ), - ( - {CONF_SOURCE: "container", CONF_IMAGE: "odroid-n2"}, - { - **TEST_DEFAULT_IMPORT_CONFIG, - CONF_NAME: DEFAULT_NAME_LATEST, - CONF_SOURCE: HaVersionSource.CONTAINER, - CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB, - CONF_IMAGE: "odroid-n2-homeassistant", - }, - ), - ), -) -async def test_config_import( - hass: HomeAssistant, yaml: dict[str, Any], converted: dict[str, Any] -) -> None: - """Test importing YAML configuration.""" - config_entry = await async_setup_sensor_wrapper(hass, yaml) - assert config_entry.data == converted From fef43d4f39245187418d043e6fea3c590568f73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 31 Mar 2022 20:59:26 +0200 Subject: [PATCH 0038/1224] Cleanup Version after removing YAML (#69020) --- .../components/version/config_flow.py | 54 +------------------ homeassistant/components/version/const.py | 7 --- tests/components/version/test_config_flow.py | 40 ++------------ 3 files changed, 4 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/version/config_flow.py b/homeassistant/components/version/config_flow.py index 292b194eea1..2fd670a7342 100644 --- a/homeassistant/components/version/config_flow.py +++ b/homeassistant/components/version/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME, CONF_SOURCE +from homeassistant.const import CONF_SOURCE from homeassistant.data_entry_flow import FlowResult from .const import ( @@ -20,25 +20,16 @@ from .const import ( DEFAULT_CHANNEL, DEFAULT_CONFIGURATION, DEFAULT_IMAGE, - DEFAULT_NAME, DEFAULT_NAME_CURRENT, - DEFAULT_NAME_LATEST, - DEFAULT_SOURCE, DOMAIN, - POSTFIX_CONTAINER_NAME, - SOURCE_DOCKER, - SOURCE_HASSIO, STEP_USER, STEP_VERSION_SOURCE, VALID_BOARDS, VALID_CHANNELS, VALID_CONTAINER_IMAGES, VALID_IMAGES, - VERSION_SOURCE_DOCKER_HUB, VERSION_SOURCE_LOCAL, VERSION_SOURCE_MAP, - VERSION_SOURCE_MAP_INVERTED, - VERSION_SOURCE_VERSIONS, ) @@ -137,16 +128,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._config_entry_name, data=self._entry_data ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - self._entry_data = _convert_imported_configuration(import_config) - - self._async_abort_entries_match({**DEFAULT_CONFIGURATION, **self._entry_data}) - - return self.async_create_entry( - title=self._config_entry_name, data=self._entry_data - ) - @property def _config_entry_name(self) -> str: """Return the name of the config entry.""" @@ -159,36 +140,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return f"{name} {channel.title()}" return name - - -def _convert_imported_configuration(config: dict[str, Any]) -> Any: - """Convert a key from the imported configuration.""" - data = DEFAULT_CONFIGURATION.copy() - if config.get(CONF_BETA): - data[CONF_CHANNEL] = "beta" - - if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE: - if source == SOURCE_HASSIO: - data[CONF_SOURCE] = "supervisor" - data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS - elif source == SOURCE_DOCKER: - data[CONF_SOURCE] = "container" - data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB - else: - data[CONF_SOURCE] = source - data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source] - - if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE: - if data[CONF_SOURCE] == "container": - data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}" - else: - data[CONF_IMAGE] = config[CONF_IMAGE] - - if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME: - data[CONF_NAME] = config[CONF_NAME] - else: - if data[CONF_SOURCE] == "local": - data[CONF_NAME] = DEFAULT_NAME_CURRENT - else: - data[CONF_NAME] = DEFAULT_NAME_LATEST - return data diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 9f480c25cc5..419e49d7240 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -31,7 +31,6 @@ ATTR_VERSION_SOURCE: Final = CONF_VERSION_SOURCE ATTR_SOURCE: Final = CONF_SOURCE SOURCE_DOCKER: Final = "docker" # Kept to not break existing configurations -SOURCE_HASSIO: Final = "hassio" # Kept to not break existing configurations VERSION_SOURCE_DOCKER_HUB: Final = "Docker Hub" VERSION_SOURCE_HAIO: Final = "Home Assistant Website" @@ -44,7 +43,6 @@ DEFAULT_BOARD: Final = "OVA" DEFAULT_CHANNEL: Final = "stable" DEFAULT_IMAGE: Final = "default" DEFAULT_NAME_CURRENT: Final = "Current Version" -DEFAULT_NAME_LATEST: Final = "Latest Version" DEFAULT_NAME: Final = "" DEFAULT_SOURCE: Final = "local" DEFAULT_CONFIGURATION: Final[dict[str, Any]] = { @@ -89,11 +87,6 @@ VERSION_SOURCE_MAP: Final[dict[str, str]] = { VERSION_SOURCE_PYPI: "pypi", } -VERSION_SOURCE_MAP_INVERTED: Final[dict[str, str]] = { - value: key for key, value in VERSION_SOURCE_MAP.items() -} - - VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [ "hassio", # Kept to not break existing configurations "docker", # Kept to not break existing configurations diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index 757afeac93d..64296498f35 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -19,19 +19,12 @@ from homeassistant.components.version.const import ( ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from homeassistant.util import dt +from .common import MOCK_VERSION, MOCK_VERSION_DATA, setup_version_integration + from tests.common import async_fire_time_changed -from tests.components.version.common import ( - MOCK_VERSION, - MOCK_VERSION_DATA, - setup_version_integration, -) async def test_reload_config_entry(hass: HomeAssistant): @@ -203,30 +196,3 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS, } assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_existing(hass: HomeAssistant) -> None: - """Test importing existing configuration.""" - with patch( - "homeassistant.components.version.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_ABORT - - assert len(mock_setup_entry.mock_calls) == 1 From 666cbebd281127cf3f3f7d4a29b5a5ec507942a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 12:04:55 -0700 Subject: [PATCH 0039/1224] DNS IP: Remove deprecated YAML import (#69007) Co-authored-by: Franck Nijhof --- homeassistant/components/dnsip/__init__.py | 4 -- homeassistant/components/dnsip/config_flow.py | 8 --- homeassistant/components/dnsip/sensor.py | 46 +------------- tests/components/dnsip/test_config_flow.py | 60 ------------------- 4 files changed, 2 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index 2f20f18580e..f679fb4ad30 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -1,15 +1,11 @@ """The dnsip component.""" from __future__ import annotations -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import PLATFORMS -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DNS IP from a config entry.""" diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index 2db0034b697..a5b51f06a45 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -82,14 +82,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Return Option handler.""" return DnsIPOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - - hostname = config.get(CONF_HOSTNAME, DEFAULT_HOSTNAME) - self._async_abort_entries_match({CONF_HOSTNAME: hostname}) - config[CONF_HOSTNAME] = hostname - return await self.async_step_user(user_input=config) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 7dfc3aaa544..a770afe388d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -6,20 +6,14 @@ import logging import aiodns from aiodns.error import DNSError -import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_HOSTNAME, @@ -27,10 +21,6 @@ from .const import ( CONF_IPV6, CONF_RESOLVER, CONF_RESOLVER_IPV6, - DEFAULT_HOSTNAME, - DEFAULT_IPV6, - DEFAULT_RESOLVER, - DEFAULT_RESOLVER_IPV6, DOMAIN, ) @@ -38,38 +28,6 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, - vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, - vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, - vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the DNS IP sensor.""" - _LOGGER.warning( - "Configuration of the DNS IP platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index f4684eb1cc4..51e169b8bb5 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -146,66 +146,6 @@ async def test_form_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_hostname"} -@pytest.mark.parametrize( - "p_input,p_output,p_options", - [ - ( - {CONF_HOSTNAME: "home-assistant.io"}, - { - "hostname": "home-assistant.io", - "name": "home-assistant.io", - "ipv4": True, - "ipv6": True, - }, - { - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", - }, - ), - ( - {}, - { - "hostname": "myip.opendns.com", - "name": "myip", - "ipv4": True, - "ipv6": True, - }, - { - "resolver": "208.67.222.222", - "resolver_ipv6": "2620:0:ccc::2", - }, - ), - ], -) -async def test_import_flow_success( - hass: HomeAssistant, - p_input: dict[str, str], - p_output: dict[str, str], - p_options: dict[str, str], -) -> None: - """Test a successful import of YAML.""" - - with patch( - "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", - return_value=RetrieveDNS(), - ), patch( - "homeassistant.components.dnsip.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=p_input, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == p_output["name"] - assert result2["data"] == p_output - assert result2["options"] == p_options - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_flow_already_exist(hass: HomeAssistant) -> None: """Test flow when unique id already exist.""" From b45399b164bf29d4435ca6fc07661c88d3ee4b5c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:07:48 +0200 Subject: [PATCH 0040/1224] Remove deprecated YAML configuration from Sensibo (#69028) --- homeassistant/components/sensibo/climate.py | 40 ++---------- .../components/sensibo/config_flow.py | 5 -- homeassistant/components/sensibo/const.py | 1 - tests/components/sensibo/test_config_flow.py | 64 ------------------- 4 files changed, 4 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 6e9d2709b8b..8f7b671a948 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -3,10 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.climate import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - ClimateEntity, -) +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -18,36 +15,26 @@ from homeassistant.components.climate.const import ( SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_STATE, ATTR_TEMPERATURE, - CONF_API_KEY, - CONF_ID, PRECISION_TENTHS, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.temperature import convert as convert_temperature -from .const import ALL, DOMAIN, LOGGER +from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]), - } -) - FIELD_TO_FLAG = { "fanLevel": SUPPORT_FAN_MODE, "swing": SUPPORT_SWING_MODE, @@ -74,25 +61,6 @@ AC_STATE_TO_DATA = { } -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up Sensibo devices.""" - LOGGER.warning( - "Loading Sensibo via platform setup is deprecated; Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index b8cca2e6fb8..c4b637e4439 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -75,11 +75,6 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, config: dict) -> FlowResult: - """Import a configuration from config.yaml.""" - - return await self.async_step_user(user_input=config) - async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 59f0d1c1179..ac3df435e29 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -19,7 +19,6 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, ] -ALL = ["all"] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index cb5c2b239df..7e92e1f2eb3 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -60,70 +60,6 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "1234567890", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Sensibo" - assert result2["data"] == { - "api_key": "1234567890", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "1234567890", - }, - unique_id="username", - ).add_to_hass(hass) - - with patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "1234567890", - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" - - @pytest.mark.parametrize( "error_message, p_error", [ From af6953157f7303f6172978529964df0a5035c450 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:08:51 +0200 Subject: [PATCH 0041/1224] Remove deprecated YAML configuration from Met.no (#69027) --- homeassistant/components/met/config_flow.py | 5 --- homeassistant/components/met/weather.py | 47 +-------------------- tests/components/met/test_config_flow.py | 18 -------- 3 files changed, 1 insertion(+), 69 deletions(-) diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 42186a60bb9..6c4c4d33d5b 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -6,7 +6,6 @@ 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 FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -79,10 +78,6 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - 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) - async def async_step_onboarding(self, data=None): """Handle a flow initialized by onboarding.""" # Don't create entry if latitude or longitude isn't set. diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index d17bfda03f1..251d99ad295 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -1,12 +1,9 @@ """Support for Met.no weather service.""" from __future__ import annotations -import logging from types import MappingProxyType from typing import Any -import voluptuous as vol - from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -16,13 +13,11 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - PLATFORM_SCHEMA, Forecast, WeatherEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, @@ -35,11 +30,9 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure @@ -55,8 +48,6 @@ from .const import ( FORECAST_MAP, ) -_LOGGER = logging.getLogger(__name__) - ATTRIBUTION = ( "Weather forecast from met.no, delivered by the Norwegian " "Meteorological Institute." @@ -64,42 +55,6 @@ ATTRIBUTION = ( DEFAULT_NAME = "Met.no" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - vol.Optional(CONF_ELEVATION): int, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Met.no weather platform.""" - _LOGGER.warning("Loading Met.no via platform config is deprecated") - - # Add defaults. - config = {CONF_ELEVATION: hass.config.elevation, **config} - - if config.get(CONF_LATITUDE) is None: - config[CONF_TRACK_HOME] = True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index ff5deb18194..358afe11cc2 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -125,21 +125,3 @@ async def test_onboarding_step_abort_no_home(hass, latitude, longitude): assert result["type"] == "abort" assert result["reason"] == "no_home" - - -async def test_import_step(hass): - """Test initializing via import step.""" - test_data = { - "name": "home", - CONF_LONGITUDE: None, - CONF_LATITUDE: None, - CONF_ELEVATION: 0, - "track_home": True, - } - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=test_data - ) - - assert result["type"] == "create_entry" - assert result["title"] == "home" - assert result["data"] == test_data From a9a14d6544329562b5950023cfcc9352daf569f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:09:24 +0200 Subject: [PATCH 0042/1224] Remove deprecated YAML configuration from Yale Smart Alarm (#69025) --- .../yale_smart_alarm/alarm_control_panel.py | 52 ++---------------- .../yale_smart_alarm/config_flow.py | 5 -- .../yale_smart_alarm/test_config_flow.py | 54 ------------------- 3 files changed, 5 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 7a74b675f0b..20d88ca4859 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -3,70 +3,28 @@ from __future__ import annotations from typing import TYPE_CHECKING -import voluptuous as vol from yalesmartalarmclient.const import ( YALE_STATE_ARM_FULL, YALE_STATE_ARM_PARTIAL, YALE_STATE_DISARM, ) -from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - AlarmControlPanelEntity, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType -from .const import ( - CONF_AREA_ID, - COORDINATOR, - DEFAULT_AREA_ID, - DEFAULT_NAME, - DOMAIN, - LOGGER, - STATE_MAP, - YALE_ALL_ERRORS, -) +from .const import COORDINATOR, DOMAIN, STATE_MAP, YALE_ALL_ERRORS from .coordinator import YaleDataUpdateCoordinator from .entity import YaleAlarmEntity -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Yale configuration from YAML.""" - LOGGER.warning( - "Loading Yale Alarm via platform setup is deprecated; Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index fe8b1c5b2fb..ae5f492bc6a 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -53,11 +53,6 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return YaleOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - - return await self.async_step_user(user_input=config) - async def async_step_reauth( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index fee5e5ab97a..97a4cdfed6b 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -114,60 +114,6 @@ async def test_form_invalid_auth( } -@pytest.mark.parametrize( - "p_input,p_output", - [ - ( - { - "username": "test-username", - "password": "test-password", - "area_id": "1", - }, - { - "username": "test-username", - "password": "test-password", - "name": "Yale Smart Alarm", - "area_id": "1", - }, - ), - ( - { - "username": "test-username", - "password": "test-password", - }, - { - "username": "test-username", - "password": "test-password", - "name": "Yale Smart Alarm", - "area_id": "1", - }, - ), - ], -) -async def test_import_flow_success( - hass, p_input: dict[str, str], p_output: dict[str, str] -): - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", - ), patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=p_input, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "test-username" - assert result2["data"] == p_output - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_reauth_flow(hass: HomeAssistant) -> None: """Test a reauthentication flow.""" entry = MockConfigEntry( From 5eb19b8a705334b480febac4f6ec62282696a29a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 13:10:07 -0700 Subject: [PATCH 0043/1224] Enforce RegistryEntryDisabler enum (#69017) Co-authored-by: Franck Nijhof --- homeassistant/helpers/entity_registry.py | 38 ++++++------------------ tests/components/prometheus/test_init.py | 7 +++-- tests/helpers/test_entity_registry.py | 23 +++++++------- 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index c02ffde4672..6c1c443ad2b 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -44,7 +44,6 @@ from homeassistant.util.yaml import load_yaml from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from .frame import report from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -92,14 +91,6 @@ class RegistryEntryHider(StrEnum): USER = "user" -# DISABLED_* are deprecated, to be removed in 2022.3 -DISABLED_CONFIG_ENTRY = RegistryEntryDisabler.CONFIG_ENTRY.value -DISABLED_DEVICE = RegistryEntryDisabler.DEVICE.value -DISABLED_HASS = RegistryEntryDisabler.HASS.value -DISABLED_INTEGRATION = RegistryEntryDisabler.INTEGRATION.value -DISABLED_USER = RegistryEntryDisabler.USER.value - - def _convert_to_entity_category( value: EntityCategory | str | None, raise_report: bool = True ) -> EntityCategory | None: @@ -389,17 +380,10 @@ class EntityRegistry: domain, suggested_object_id or f"{platform}_{unique_id}", known_object_ids ) - if isinstance(disabled_by, str) and not isinstance( - disabled_by, RegistryEntryDisabler - ): - report( # type: ignore[unreachable] - "uses str for entity registry disabled_by. This is deprecated and will " - "stop working in Home Assistant 2022.3, it should be updated to use " - "RegistryEntryDisabler instead", - error_if_core=False, - ) - disabled_by = RegistryEntryDisabler(disabled_by) - elif ( + if disabled_by and not isinstance(disabled_by, RegistryEntryDisabler): + raise ValueError("disabled_by must be a RegistryEntryDisabler value") + + if ( disabled_by is None and config_entry and config_entry.pref_disable_new_entities @@ -537,16 +521,12 @@ class EntityRegistry: new_values: dict[str, Any] = {} # Dict with new key/value pairs old_values: dict[str, Any] = {} # Dict with old key/value pairs - if isinstance(disabled_by, str) and not isinstance( - disabled_by, RegistryEntryDisabler + if ( + disabled_by + and disabled_by is not UNDEFINED + and not isinstance(disabled_by, RegistryEntryDisabler) ): - report( # type: ignore[unreachable] - "uses str for entity registry disabled_by. This is deprecated and will " - "stop working in Home Assistant 2022.3, it should be updated to use " - "RegistryEntryDisabler instead", - error_if_core=False, - ) - disabled_by = RegistryEntryDisabler(disabled_by) + raise ValueError("disabled_by must be a RegistryEntryDisabler value") for attr_name, value in ( ("area_id", area_id), diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 096f1405168..5864b551b3c 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -782,9 +782,12 @@ async def test_disabling_entity( assert "climate.heatpump" in registry.entities registry.async_update_entity( entity_id=data["sensor_1"].entity_id, - disabled_by="user", + disabled_by=entity_registry.RegistryEntryDisabler.USER, + ) + registry.async_update_entity( + entity_id="climate.heatpump", + disabled_by=entity_registry.RegistryEntryDisabler.USER, ) - registry.async_update_entity(entity_id="climate.heatpump", disabled_by="user") await hass.async_block_till_done() body = await generate_latest_metrics(client) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 7dc749b8592..eeac78a2654 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1217,17 +1217,20 @@ def test_entity_registry_items(): assert entities.get_entry(entry2.id) is None -async def test_deprecated_disabled_by_str(hass, registry, caplog): - """Test deprecated str use of disabled_by converts to enum and logs a warning.""" - entry = registry.async_get_or_create( - domain="light.kitchen", - platform="hue", - unique_id="5678", - disabled_by=er.RegistryEntryDisabler.USER.value, - ) +async def test_disabled_by_str_not_allowed(hass): + """Test we need to pass entity category type.""" + reg = er.async_get(hass) - assert entry.disabled_by is er.RegistryEntryDisabler.USER - assert " str for entity registry disabled_by. This is deprecated " in caplog.text + with pytest.raises(ValueError): + reg.async_get_or_create( + "light", "hue", "1234", disabled_by=er.RegistryEntryDisabler.USER.value + ) + + entity_id = reg.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(ValueError): + reg.async_update_entity( + entity_id, disabled_by=er.RegistryEntryDisabler.USER.value + ) async def test_deprecated_entity_category_str(hass, registry, caplog): From 72c4c359a490d0b6aa6a3b2313ffeacca8d1dd49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 13:25:32 -0700 Subject: [PATCH 0044/1224] iCloud: remove deprecated YAML import (#69006) Co-authored-by: Franck Nijhof --- homeassistant/components/icloud/__init__.py | 50 +------ .../components/icloud/config_flow.py | 4 - tests/components/icloud/conftest.py | 9 ++ tests/components/icloud/test_config_flow.py | 123 +----------------- 4 files changed, 12 insertions(+), 174 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 8b6c8355e40..17cc15b195a 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,13 +1,10 @@ """The iCloud component.""" -import logging - import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from .account import IcloudAccount @@ -15,9 +12,6 @@ from .const import ( CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, CONF_WITH_FAMILY, - DEFAULT_GPS_ACCURACY_THRESHOLD, - DEFAULT_MAX_INTERVAL, - DEFAULT_WITH_FAMILY, DOMAIN, PLATFORMS, STORAGE_KEY, @@ -69,47 +63,7 @@ SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( } ) -ACCOUNT_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_WITH_FAMILY, default=DEFAULT_WITH_FAMILY): cv.boolean, - vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int, - vol.Optional( - CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD - ): cv.positive_int, - } -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, - extra=vol.ALLOW_EXTRA, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up iCloud from legacy config file.""" - if (conf := config.get(DOMAIN)) is None: - return True - - # Note: need to remember to cleanup device_tracker (remove async_setup_scanner) - _LOGGER.warning( - "Configuration of the iCloud integration in YAML is deprecated and " - "will be removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - - for account_conf in conf: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 3eb6ced782c..f3630abfdaa 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -172,10 +172,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._validate_and_create_entry(user_input, "user") - async def async_step_import(self, user_input): - """Import a config entry.""" - return await self.async_step_user(user_input) - async def async_step_reauth(self, user_input=None): """Update password for a config entry that can't authenticate.""" # Store existing entry data so it can be used later and set unique ID diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index 2230cc2ea32..c8195471878 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -9,3 +9,12 @@ def icloud_bypass_setup_fixture(): """Mock component setup.""" with patch("homeassistant.components.icloud.async_setup_entry", return_value=True): yield + + +@pytest.fixture(autouse=True) +def icloud_not_create_dir(): + """Mock component setup.""" + with patch( + "homeassistant.components.icloud.config_flow.os.path.exists", return_value=True + ): + yield diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 59c5ebf24a9..cd72aae0eff 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -18,7 +18,7 @@ from homeassistant.components.icloud.const import ( DEFAULT_WITH_FAMILY, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -197,127 +197,6 @@ async def test_user_with_cookie(hass: HomeAssistant, service_authenticated: Magi assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD -async def test_import(hass: HomeAssistant, service: MagicMock): - """Test import step.""" - # import with required - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "trusted_device" - - # import with all - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: USERNAME_2, - CONF_PASSWORD: PASSWORD, - CONF_WITH_FAMILY: WITH_FAMILY, - CONF_MAX_INTERVAL: MAX_INTERVAL, - CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "trusted_device" - - -async def test_import_with_cookie( - hass: HomeAssistant, service_authenticated: MagicMock -): - """Test import step with presence of a cookie.""" - # import with required - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY - assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL - assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD - - # import with all - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: USERNAME_2, - CONF_PASSWORD: PASSWORD, - CONF_WITH_FAMILY: WITH_FAMILY, - CONF_MAX_INTERVAL: MAX_INTERVAL, - CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME_2 - assert result["title"] == USERNAME_2 - assert result["data"][CONF_USERNAME] == USERNAME_2 - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_WITH_FAMILY] == WITH_FAMILY - assert result["data"][CONF_MAX_INTERVAL] == MAX_INTERVAL - assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == GPS_ACCURACY_THRESHOLD - - -async def test_two_accounts_setup( - hass: HomeAssistant, service_authenticated: MagicMock -): - """Test to setup two accounts.""" - MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - unique_id=USERNAME, - ).add_to_hass(hass) - - # import with required - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME_2, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME_2 - assert result["title"] == USERNAME_2 - assert result["data"][CONF_USERNAME] == USERNAME_2 - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY - assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL - assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD - - -async def test_already_setup(hass: HomeAssistant): - """Test we abort if the account is already setup.""" - MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - unique_id=USERNAME, - ).add_to_hass(hass) - - # Should fail, same USERNAME (import) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - # Should fail, same USERNAME (flow) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_login_failed(hass: HomeAssistant): """Test when we have errors during login.""" with patch( From 5280bf22965b26140550177994baa2bfe667ebca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:28:15 +0200 Subject: [PATCH 0045/1224] Remove deprecated template support in persistent notifications (#69021) --- .../persistent_notification/__init__.py | 31 ++----------------- .../persistent_notification/test_init.py | 23 +------------- 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 88a61b52038..c247a326036 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -3,17 +3,15 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any, cast +from typing import Any import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import Context, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.template import Template, is_template_string from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import slugify @@ -82,36 +80,11 @@ def async_create( ) notification_id = entity_id.split(".")[1] - warn = False - - attr: dict[str, str] = {} + attr: dict[str, str] = {ATTR_MESSAGE: message} if title is not None: - if is_template_string(title): - warn = True - try: - title = cast( - str, Template(title, hass).async_render(parse_result=False) # type: ignore[no-untyped-call] - ) - except TemplateError as ex: - _LOGGER.error("Error rendering title %s: %s", title, ex) - attr[ATTR_TITLE] = title attr[ATTR_FRIENDLY_NAME] = title - if is_template_string(message): - warn = True - try: - message = Template(message, hass).async_render(parse_result=False) # type: ignore[no-untyped-call] - except TemplateError as ex: - _LOGGER.error("Error rendering message %s: %s", message, ex) - - attr[ATTR_MESSAGE] = message - - if warn: - _LOGGER.warning( - "Passing a template string to persistent_notification.async_create function is deprecated" - ) - hass.states.async_set(entity_id, STATE, attr, context=context) # Store notification and fire event diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index a26e243df11..bca6fe3b93d 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -20,7 +20,7 @@ async def test_create(hass): assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 assert len(notifications) == 0 - pn.async_create(hass, "Hello World {{ 1 + 1 }}", title="{{ 1 + 1 }} beers") + pn.async_create(hass, "Hello World 2", title="2 beers") entity_ids = hass.states.async_entity_ids(pn.DOMAIN) assert len(entity_ids) == 1 @@ -68,27 +68,6 @@ async def test_create_notification_id(hass): assert notification["message"] == "test 2" -async def test_create_template_error(hass): - """Ensure we output templates if contain error.""" - notifications = hass.data[pn.DOMAIN] - assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 - - pn.async_create(hass, "{{ message + 1 }}", "{{ title + 1 }}") - - entity_ids = hass.states.async_entity_ids(pn.DOMAIN) - assert len(entity_ids) == 1 - assert len(notifications) == 1 - - state = hass.states.get(entity_ids[0]) - assert state.attributes.get("message") == "{{ message + 1 }}" - assert state.attributes.get("title") == "{{ title + 1 }}" - - notification = notifications.get(entity_ids[0]) - assert notification["message"] == "{{ message + 1 }}" - assert notification["title"] == "{{ title + 1 }}" - - async def test_dismiss_notification(hass): """Ensure removal of specific notification.""" notifications = hass.data[pn.DOMAIN] From 388677e03bf5cd4625d96c11910126af306534f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:30:42 +0200 Subject: [PATCH 0046/1224] Remove deprecated YAML configuration from EZVIZ (#69031) --- homeassistant/components/ezviz/camera.py | 58 +-------- homeassistant/components/ezviz/config_flow.py | 44 ------- homeassistant/components/ezviz/const.py | 1 - tests/components/ezviz/__init__.py | 33 ------ tests/components/ezviz/test_config_flow.py | 110 +----------------- 5 files changed, 2 insertions(+), 244 deletions(-) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 58b45eeafd3..ed48ed4ee03 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -7,18 +7,16 @@ from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError import voluptuous as vol from homeassistant.components import ffmpeg -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ( SOURCE_IGNORE, - SOURCE_IMPORT, SOURCE_INTEGRATION_DISCOVERY, ConfigEntry, ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_DIRECTION, @@ -27,7 +25,6 @@ from .const import ( ATTR_SERIAL, ATTR_SPEED, ATTR_TYPE, - CONF_CAMERAS, CONF_FFMPEG_ARGUMENTS, DATA_COORDINATOR, DEFAULT_CAMERA_USERNAME, @@ -47,62 +44,9 @@ from .const import ( from .coordinator import EzvizDataUpdateCoordinator from .entity import EzvizEntity -CAMERA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CAMERAS, default={}): {cv.string: CAMERA_SCHEMA}, - } -) - _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a Ezviz IP Camera from platform config.""" - _LOGGER.warning( - "Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards" - ) - - # Check if entry config exists and skips import if it does. - if hass.config_entries.async_entries(DOMAIN): - return - - # Check if importing camera account. - if CONF_CAMERAS in config: - cameras_conf = config[CONF_CAMERAS] - for serial, camera in cameras_conf.items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - ATTR_SERIAL: serial, - CONF_USERNAME: camera[CONF_USERNAME], - CONF_PASSWORD: camera[CONF_PASSWORD], - }, - ) - ) - - # Check if importing main ezviz cloud account. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 780d06383f9..36cf2ac456e 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -307,50 +307,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, import_config): - """Handle config import from yaml.""" - _LOGGER.debug("import config: %s", import_config) - - # Check importing camera. - if ATTR_SERIAL in import_config: - return await self.async_step_import_camera(import_config) - - # Validate and setup of main ezviz cloud account. - try: - return await self._validate_and_create_auth(import_config) - - except InvalidURL: - _LOGGER.error("Error importing Ezviz platform config: invalid host") - return self.async_abort(reason="invalid_host") - - except InvalidHost: - _LOGGER.error("Error importing Ezviz platform config: cannot connect") - return self.async_abort(reason="cannot_connect") - - except (AuthTestResultFailed, PyEzvizError): - _LOGGER.error("Error importing Ezviz platform config: invalid auth") - return self.async_abort(reason="invalid_auth") - - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Error importing ezviz platform config: unexpected exception" - ) - - return self.async_abort(reason="unknown") - - async def async_step_import_camera(self, data): - """Create RTSP auth entry per camera in config.""" - - await self.async_set_unique_id(data[ATTR_SERIAL]) - self._abort_if_unique_id_configured() - - _LOGGER.debug("Create camera with: %s", data) - - cam_serial = data.pop(ATTR_SERIAL) - data[CONF_TYPE] = ATTR_TYPE_CAMERA - - return self.async_create_entry(title=cam_serial, data=data) - class EzvizOptionsFlowHandler(OptionsFlow): """Handle Ezviz client options.""" diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index 6131904be99..5340f48d0f6 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -5,7 +5,6 @@ MANUFACTURER = "Ezviz" # Configuration ATTR_SERIAL = "serial" -CONF_CAMERAS = "cameras" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" ATTR_HOME = "HOME_MODE" ATTR_AWAY = "AWAY_MODE" diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index b8dc04ef790..bfb30b893eb 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -3,9 +3,7 @@ from unittest.mock import patch from homeassistant.components.ezviz.const import ( ATTR_SERIAL, - ATTR_TYPE_CAMERA, ATTR_TYPE_CLOUD, - CONF_CAMERAS, CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT, @@ -48,37 +46,6 @@ USER_INPUT = { CONF_TYPE: ATTR_TYPE_CLOUD, } -USER_INPUT_CAMERA_VALIDATE = { - ATTR_SERIAL: "C666666", - CONF_PASSWORD: "test-password", - CONF_USERNAME: "test-username", -} - -USER_INPUT_CAMERA = { - CONF_PASSWORD: "test-password", - CONF_USERNAME: "test-username", - CONF_TYPE: ATTR_TYPE_CAMERA, -} - -YAML_CONFIG = { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_URL: "apiieu.ezvizlife.com", - CONF_CAMERAS: { - "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} - }, -} - -YAML_INVALID = { - "C666666": {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} -} - -YAML_CONFIG_CAMERA = { - ATTR_SERIAL: "C666666", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", -} - DISCOVERY_INFO = { ATTR_SERIAL: "C666666", CONF_USERNAME: None, diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 9a3129b7dd6..4ba3b5911f6 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.components.ezviz.const import ( DEFAULT_TIMEOUT, DOMAIN, ) -from homeassistant.config_entries import ( - SOURCE_IMPORT, - SOURCE_INTEGRATION_DISCOVERY, - SOURCE_USER, -) +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER from homeassistant.const import ( CONF_CUSTOMIZE, CONF_IP_ADDRESS, @@ -42,12 +38,7 @@ from homeassistant.data_entry_flow import ( from . import ( DISCOVERY_INFO, USER_INPUT, - USER_INPUT_CAMERA, - USER_INPUT_CAMERA_VALIDATE, USER_INPUT_VALIDATE, - YAML_CONFIG, - YAML_CONFIG_CAMERA, - YAML_INVALID, _patch_async_setup_entry, init_integration, ) @@ -115,66 +106,6 @@ async def test_user_custom_url(hass, ezviz_config_flow): assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_import(hass, ezviz_config_flow): - """Test the config import flow.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_camera(hass, ezviz_config_flow): - """Test the config import camera flow.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG_CAMERA - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT_CAMERA - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_2nd_form_returns_camera(hass, ezviz_config_flow): - """Test we get the user initiated form.""" - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT - - assert len(mock_setup_entry.mock_calls) == 1 - - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=USER_INPUT_CAMERA_VALIDATE - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == USER_INPUT_CAMERA - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_abort(hass, ezviz_config_flow): - """Test the config import flow with invalid data.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_INVALID - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - async def test_step_discovery_abort_if_cloud_account_missing(hass): """Test discovery and confirm step, abort if cloud account was removed.""" @@ -308,45 +239,6 @@ async def test_user_form_exception(hass, ezviz_config_flow): assert result["reason"] == "unknown" -async def test_import_exception(hass, ezviz_config_flow): - """Test we handle unexpected exception on import.""" - ezviz_config_flow.side_effect = PyEzvizError - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" - - ezviz_config_flow.side_effect = InvalidURL - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "invalid_host" - - ezviz_config_flow.side_effect = HTTPError - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - ezviz_config_flow.side_effect = Exception - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_CONFIG - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - async def test_discover_exception_step1( hass, ezviz_config_flow, From 7a31c8f53c29ee4b5ff5f6d0851f3e56e6b59890 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:31:17 +0200 Subject: [PATCH 0047/1224] Remove deprecated YAML configuration from Brunt (#69024) --- homeassistant/components/brunt/config_flow.py | 6 --- homeassistant/components/brunt/cover.py | 23 +-------- tests/components/brunt/test_config_flow.py | 48 ------------------- 3 files changed, 1 insertion(+), 76 deletions(-) diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index 636a9affddd..c81eb2de6ca 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -111,9 +111,3 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input) await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") - - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Import config from configuration.yaml.""" - await self.async_set_unique_id(import_config[CONF_USERNAME].lower()) - self._abort_if_unique_id_configured() - return await self.async_step_user(import_config) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index d3efdce0a5b..8bbb11914f7 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import MutableMapping -import logging from typing import Any from aiohttp.client_exceptions import ClientResponseError @@ -16,12 +15,11 @@ from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,28 +37,9 @@ from .const import ( REGULAR_INTERVAL, ) -_LOGGER = logging.getLogger(__name__) - COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Component setup, run import config flow for each entry in config.""" - _LOGGER.warning( - "Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index f053a6d18b0..4a949911ca6 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -41,54 +41,6 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test we get the form.""" - - with patch( - "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", - return_value=None, - ), patch( - "homeassistant.components.brunt.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test-username" - assert result["data"] == CONFIG - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_duplicate_login(hass): - """Test uniqueness of username.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG, - title="test-username", - unique_id="test-username", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", - return_value=None, - ), patch( - "homeassistant.components.brunt.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=CONFIG - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_form_duplicate_login(hass): """Test uniqueness of username.""" entry = MockConfigEntry( From beb54dfb63bb8587ede5123b5e04fcb4936e4e38 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:35:44 +0200 Subject: [PATCH 0048/1224] Remove deprecated YAML configuration from Yamaha Music Cast (#69033) --- .../yamaha_musiccast/config_flow.py | 15 ----- .../components/yamaha_musiccast/const.py | 1 - .../yamaha_musiccast/media_player.py | 57 +------------------ .../yamaha_musiccast/test_config_flow.py | 55 ------------------ 4 files changed, 3 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index 4049a5d6a37..94153a47fdc 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -131,18 +131,3 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="confirm") - - async def async_step_import(self, import_data: dict) -> data_entry_flow.FlowResult: - """Import data from configuration.yaml into the config flow.""" - res = await self.async_step_user(import_data) - if res["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - _LOGGER.info( - "Successfully imported %s from configuration.yaml", - import_data.get(CONF_HOST), - ) - elif res["type"] == data_entry_flow.RESULT_TYPE_FORM: - _LOGGER.error( - "Could not import %s from configuration.yaml", - import_data.get(CONF_HOST), - ) - return res diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 21b7de82184..7846cab1754 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -34,7 +34,6 @@ HA_REPEAT_MODE_TO_MC_MAPPING = { NULL_GROUP = "00000000000000000000000000000000" -INTERVAL_SECONDS = "interval_seconds" MC_REPEAT_MODE_TO_HA_MAPPING = { val: key for key, val in HA_REPEAT_MODE_TO_MC_MAPPING.items() diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index ff62799ec58..3618bc78dbe 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -6,14 +6,9 @@ import logging from aiomusiccast import MusicCastGroupException, MusicCastMediaContent from aiomusiccast.features import ZoneFeature -import voluptuous as vol from homeassistant.components import media_source -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, - BrowseMedia, - MediaPlayerEntity, -) +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) @@ -40,21 +35,12 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import uuid from . import MusicCastDataUpdateCoordinator, MusicCastDeviceEntity @@ -64,7 +50,6 @@ from .const import ( DEFAULT_ZONE, DOMAIN, HA_REPEAT_MODE_TO_MC_MAPPING, - INTERVAL_SECONDS, MC_REPEAT_MODE_TO_HA_MAPPING, MEDIA_CLASS_MAPPING, NULL_GROUP, @@ -81,42 +66,6 @@ MUSIC_PLAYER_BASE_SUPPORT = ( | SUPPORT_PLAY_MEDIA ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=5000): cv.port, - vol.Optional(INTERVAL_SECONDS, default=0): cv.positive_int, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import legacy configurations.""" - - if hass.config_entries.async_entries(DOMAIN) and config[CONF_HOST] not in [ - entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) - ]: - _LOGGER.error( - "Configuration in configuration.yaml is not supported anymore. " - "Please add this device using the config flow: %s", - config[CONF_HOST], - ) - else: - _LOGGER.warning( - "Configuration in configuration.yaml is deprecated. Use the config flow instead" - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index bddc350cff8..fc9a740d4a6 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -250,61 +250,6 @@ async def test_user_input_device_found_no_ssdp( } -async def test_import_device_already_existing( - hass, mock_get_device_info_valid, mock_get_source_ip -): - """Test when the configurations.yaml contains an existing device.""" - mock_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="1234567890", - data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"}, - ) - mock_entry.add_to_hass(hass) - - config = {"platform": "yamaha_musiccast", "host": "192.168.188.18", "port": 5006} - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_import_error(hass, mock_get_device_info_exception, mock_get_source_ip): - """Test when in the configuration.yaml a device is configured, which cannot be added..""" - config = {"platform": "yamaha_musiccast", "host": "192.168.188.18", "port": 5006} - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - - -async def test_import_device_successful( - hass, - mock_get_device_info_valid, - mock_valid_discovery_information, - mock_get_source_ip, -): - """Test when the device was imported successfully.""" - config = {"platform": "yamaha_musiccast", "host": "127.0.0.1", "port": 5006} - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert isinstance(result["result"], ConfigEntry) - assert result["data"] == { - "host": "127.0.0.1", - "serial": "1234567890", - "upnp_description": "http://127.0.0.1:9000/MediaRenderer/desc.xml", - } - - # SSDP Flows From e69450f7ca647ea4ea7014b29617c05a6e0b9d77 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 22:38:54 +0200 Subject: [PATCH 0049/1224] Remove deprecated YAML configuration from Fronius (#69032) --- .../components/fronius/config_flow.py | 6 +-- homeassistant/components/fronius/sensor.py | 40 +------------------ tests/components/fronius/test_config_flow.py | 32 +-------------- 3 files changed, 4 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index 00ddd9335a3..15b8cd7a3b8 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo -from homeassistant.const import CONF_HOST, CONF_RESOURCE +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -110,10 +110,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_import(self, conf: dict) -> FlowResult: - """Import a configuration from config.yaml.""" - return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]}) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle a flow initiated by the DHCP client.""" for entry in self._async_current_entries(include_ignore=False): diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index f13caf83a5d..c3b219c4b22 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,23 +1,17 @@ """Support for Fronius devices.""" from __future__ import annotations -import logging from typing import TYPE_CHECKING, Any, Final -import voluptuous as vol - from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_RESOURCE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -29,10 +23,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -49,38 +41,8 @@ if TYPE_CHECKING: FroniusStorageUpdateCoordinator, ) -_LOGGER: Final = logging.getLogger(__name__) - ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_RESOURCE): cv.url, - vol.Optional(CONF_MONITORED_CONDITIONS): object, - } - ), -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Fronius configuration from yaml.""" - _LOGGER.warning( - "Loading Fronius via platform setup is deprecated. Please remove it from your yaml configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index c6f2f69ce5f..256d64d4cbe 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -7,17 +7,15 @@ import pytest from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fronius.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_HOST, CONF_RESOURCE +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component -from . import MOCK_HOST, mock_responses +from . import mock_responses from tests.common import MockConfigEntry @@ -260,32 +258,6 @@ async def test_form_updates_host(hass, aioclient_mock): } -async def test_import(hass, aioclient_mock): - """Test import step.""" - mock_responses(aioclient_mock) - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_RESOURCE: MOCK_HOST, - } - }, - ) - await hass.async_block_till_done() - - fronius_entries = hass.config_entries.async_entries(DOMAIN) - assert len(fronius_entries) == 1 - - test_entry = fronius_entries[0] - assert test_entry.unique_id == "123.4567890" # has to match mocked logger unique_id - assert test_entry.data == { - "host": MOCK_HOST, - "is_logger": True, - } - - async def test_dhcp(hass, aioclient_mock): """Test starting a flow from discovery.""" with patch( From c6cd474312b541b3cd6e5b286a191ee2ad7867cd Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 31 Mar 2022 22:45:58 +0200 Subject: [PATCH 0050/1224] Add "station is open" sensor to Tankerkoenig (#68925) --- .coveragerc | 1 + .../components/tankerkoenig/__init__.py | 14 +-- .../components/tankerkoenig/binary_sensor.py | 78 ++++++++++++ .../components/tankerkoenig/const.py | 9 ++ .../components/tankerkoenig/sensor.py | 111 ++++++------------ 5 files changed, 131 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/tankerkoenig/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index d020a59c712..73814938619 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1197,6 +1197,7 @@ omit = homeassistant/components/tado/water_heater.py homeassistant/components/tank_utility/sensor.py homeassistant/components/tankerkoenig/__init__.py + homeassistant/components/tankerkoenig/binary_sensor.py homeassistant/components/tankerkoenig/const.py homeassistant/components/tankerkoenig/sensor.py homeassistant/components/tapsaff/binary_sensor.py diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 3051d70b06d..08520c8f5cc 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -75,7 +75,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -170,14 +170,14 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(minutes=update_interval), ) - self._api_key = entry.data[CONF_API_KEY] - self._selected_stations = entry.data[CONF_STATIONS] + self._api_key: str = entry.data[CONF_API_KEY] + self._selected_stations: list[str] = entry.data[CONF_STATIONS] self._hass = hass self.stations: dict[str, dict] = {} - self.fuel_types = entry.data[CONF_FUEL_TYPES] - self.show_on_map = entry.options[CONF_SHOW_ON_MAP] + self.fuel_types: list[str] = entry.data[CONF_FUEL_TYPES] + self.show_on_map: bool = entry.options[CONF_SHOW_ON_MAP] - def setup(self): + def setup(self) -> bool: """Set up the tankerkoenig API.""" for station_id in self._selected_stations: try: @@ -205,7 +205,7 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): ) return True - async def _async_update_data(self): + async def _async_update_data(self) -> dict: """Get the latest data from tankerkoenig.de.""" _LOGGER.debug("Fetching new data from tankerkoenig.de") station_ids = list(self.stations) diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py new file mode 100644 index 00000000000..4b58ea26703 --- /dev/null +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -0,0 +1,78 @@ +"""Tankerkoenig binary sensor integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import TankerkoenigDataUpdateCoordinator +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the tankerkoenig binary sensors.""" + + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] + + stations = coordinator.stations.values() + entities = [] + for station in stations: + sensor = StationOpenBinarySensorEntity( + station, + coordinator, + coordinator.show_on_map, + ) + entities.append(sensor) + _LOGGER.debug("Added sensors %s", entities) + + async_add_entities(entities) + + +class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): + """Shows if a station is open or closed.""" + + _attr_device_class = BinarySensorDeviceClass.DOOR + + def __init__( + self, + station: dict, + coordinator: TankerkoenigDataUpdateCoordinator, + show_on_map: bool, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._station_id = station["id"] + self._attr_name = ( + f"{station['brand']} {station['street']} {station['houseNumber']} status" + ) + self._attr_unique_id = f"{station['id']}_status" + self._attr_device_info = DeviceInfo( + identifiers={(ATTR_ID, station["id"])}, + name=f"{station['brand']} {station['street']} {station['houseNumber']}", + model=station["brand"], + configuration_url="https://www.tankerkoenig.de", + ) + if show_on_map: + self._attr_extra_state_attributes = { + ATTR_LATITUDE: station["lat"], + ATTR_LONGITUDE: station["lng"], + } + + @property + def is_on(self) -> bool | None: + """Return true if the station is open.""" + data = self.coordinator.data[self._station_id] + return data is not None and "status" in data diff --git a/homeassistant/components/tankerkoenig/const.py b/homeassistant/components/tankerkoenig/const.py index 5c4746bd3a1..c2a1dba9b6a 100644 --- a/homeassistant/components/tankerkoenig/const.py +++ b/homeassistant/components/tankerkoenig/const.py @@ -10,3 +10,12 @@ DEFAULT_RADIUS = 2 DEFAULT_SCAN_INTERVAL = 30 FUEL_TYPES = {"e5": "Super", "e10": "Super E10", "diesel": "Diesel"} + +ATTR_BRAND = "brand" +ATTR_CITY = "city" +ATTR_FUEL_TYPE = "fuel_type" +ATTR_HOUSE_NUMBER = "house_number" +ATTR_POSTCODE = "postcode" +ATTR_STATION_NAME = "station_name" +ATTR_STREET = "street" +ATTRIBUTION = "Data provided by https://www.tankerkoenig.de" diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index e22a7c1c82e..bbaeda44fd7 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -18,22 +18,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import TankerkoenigDataUpdateCoordinator -from .const import DOMAIN, FUEL_TYPES +from .const import ( + ATTR_BRAND, + ATTR_CITY, + ATTR_FUEL_TYPE, + ATTR_HOUSE_NUMBER, + ATTR_POSTCODE, + ATTR_STATION_NAME, + ATTR_STREET, + ATTRIBUTION, + DOMAIN, + FUEL_TYPES, +) _LOGGER = logging.getLogger(__name__) -ATTR_BRAND = "brand" -ATTR_CITY = "city" -ATTR_FUEL_TYPE = "fuel_type" -ATTR_HOUSE_NUMBER = "house_number" -ATTR_IS_OPEN = "is_open" -ATTR_POSTCODE = "postcode" -ATTR_STATION_NAME = "station_name" -ATTR_STREET = "street" -ATTRIBUTION = "Data provided by https://creativecommons.tankerkoenig.de" - -ICON = "mdi:gas-station" - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -67,79 +66,41 @@ class FuelPriceSensor(CoordinatorEntity, SensorEntity): """Contains prices for fuel in a given station.""" _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_icon = "mdi:gas-station" def __init__(self, fuel_type, station, coordinator, show_on_map): """Initialize the sensor.""" super().__init__(coordinator) - self._station = station self._station_id = station["id"] self._fuel_type = fuel_type - self._latitude = station["lat"] - self._longitude = station["lng"] - self._city = station["place"] - self._house_number = station["houseNumber"] - self._postcode = station["postCode"] - self._street = station["street"] - self._brand = self._station["brand"] - self._price = station[fuel_type] - self._show_on_map = show_on_map + self._attr_name = f"{station['brand']} {station['street']} {station['houseNumber']} {FUEL_TYPES[fuel_type]}" + self._attr_native_unit_of_measurement = CURRENCY_EURO + self._attr_unique_id = f"{station['id']}_{fuel_type}" + self._attr_device_info = DeviceInfo( + identifiers={(ATTR_ID, station["id"])}, + name=f"{station['brand']} {station['street']} {station['houseNumber']}", + model=station["brand"], + configuration_url="https://www.tankerkoenig.de", + ) - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._brand} {self._street} {self._house_number} {FUEL_TYPES[self._fuel_type]}" + attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_BRAND: station["brand"], + ATTR_FUEL_TYPE: fuel_type, + ATTR_STATION_NAME: station["name"], + ATTR_STREET: station["street"], + ATTR_HOUSE_NUMBER: station["houseNumber"], + ATTR_POSTCODE: station["postCode"], + ATTR_CITY: station["place"], + } - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON - - @property - def native_unit_of_measurement(self): - """Return unit of measurement.""" - return CURRENCY_EURO + if show_on_map: + attrs[ATTR_LATITUDE] = station["lat"] + attrs[ATTR_LONGITUDE] = station["lng"] + self._attr_extra_state_attributes = attrs @property def native_value(self): """Return the state of the device.""" # key Fuel_type is not available when the fuel station is closed, use "get" instead of "[]" to avoid exceptions return self.coordinator.data[self._station_id].get(self._fuel_type) - - @property - def unique_id(self) -> str: - """Return a unique identifier for this entity.""" - return f"{self._station_id}_{self._fuel_type}" - - @property - def device_info(self) -> DeviceInfo | None: - """Return device info.""" - return DeviceInfo( - identifiers={(ATTR_ID, self._station_id)}, - name=f"{self._brand} {self._street} {self._house_number}", - model=self._brand, - configuration_url="https://www.tankerkoenig.de", - ) - - @property - def extra_state_attributes(self): - """Return the attributes of the device.""" - data = self.coordinator.data[self._station_id] - - attrs = { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_BRAND: self._station["brand"], - ATTR_FUEL_TYPE: self._fuel_type, - ATTR_STATION_NAME: self._station["name"], - ATTR_STREET: self._street, - ATTR_HOUSE_NUMBER: self._house_number, - ATTR_POSTCODE: self._postcode, - ATTR_CITY: self._city, - } - - if self._show_on_map: - attrs[ATTR_LATITUDE] = self._latitude - attrs[ATTR_LONGITUDE] = self._longitude - - if data is not None and "status" in data: - attrs[ATTR_IS_OPEN] = data["status"] == "open" - return attrs From 39cc91dec64501381ef120e32f98bf06f22230d2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Mar 2022 23:12:11 +0200 Subject: [PATCH 0051/1224] Remove deprecated reject_call service from modem_callerid (#69019) --- .../components/modem_callerid/const.py | 1 - .../components/modem_callerid/sensor.py | 22 +++---------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/modem_callerid/const.py b/homeassistant/components/modem_callerid/const.py index 0b01c3b761f..d144e2afd5c 100644 --- a/homeassistant/components/modem_callerid/const.py +++ b/homeassistant/components/modem_callerid/const.py @@ -8,7 +8,6 @@ DATA_KEY_API = "api" DEFAULT_NAME = "Phone Modem" DOMAIN = "modem_callerid" ICON = "mdi:phone-classic" -SERVICE_REJECT_CALL = "reject_call" EXCEPTIONS: Final = ( FileNotFoundError, diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index f4b2f3c3e44..e50eabb17aa 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -7,11 +7,11 @@ from phone_modem import PhoneModem from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP, STATE_IDLE +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_platform -from .const import CID, DATA_KEY_API, DOMAIN, ICON, SERVICE_REJECT_CALL +from .const import CID, DATA_KEY_API, DOMAIN, ICON _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,6 @@ async def async_setup_entry( ModemCalleridSensor( api, entry.title, - entry.data[CONF_DEVICE], entry.entry_id, ) ] @@ -43,9 +42,6 @@ async def async_setup_entry( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_hass_stop) ) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_REJECT_CALL, {}, "async_reject_call") - class ModemCalleridSensor(SensorEntity): """Implementation of USB modem caller ID sensor.""" @@ -53,11 +49,8 @@ class ModemCalleridSensor(SensorEntity): _attr_icon = ICON _attr_should_poll = False - def __init__( - self, api: PhoneModem, name: str, device: str, server_unique_id: str - ) -> None: + def __init__(self, api: PhoneModem, name: str, server_unique_id: str) -> None: """Initialize the sensor.""" - self.device = device self.api = api self._attr_name = name self._attr_unique_id = server_unique_id @@ -85,12 +78,3 @@ class ModemCalleridSensor(SensorEntity): self._attr_extra_state_attributes[CID.CID_TIME] = self.api.cid_time self._attr_native_value = self.api.state self.async_write_ha_state() - - async def async_reject_call(self) -> None: - """Reject Incoming Call.""" - _LOGGER.warning( - "Calling reject_call service is deprecated and will be removed after 2022.4; " - "A new button entity is now available with the same function " - "and replaces the existing service" - ) - await self.api.reject_call(self.device) From 69ee4cd978194e2074d4cd57ebbbf5a028f71f7a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Mar 2022 23:29:44 +0200 Subject: [PATCH 0052/1224] Deprecate temperature conversion in base entity class (#68978) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/entity.py | 57 +++++++++++++++++++++++++------ tests/helpers/test_entity.py | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f6ff8c73bf5..75ac1e0c1b8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -295,6 +295,9 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False + # If we reported this entity is relying on deprecated temperature conversion + _temperature_reported = False + # Protect for multiple updates _update_staged = False @@ -642,23 +645,57 @@ class Entity(ABC): if DATA_CUSTOMIZE in self.hass.data: attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)) - # Convert temperature if we detect one - try: + def _convert_temperature(state: str, attr: dict) -> str: + # Convert temperature if we detect one + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.sensor import SensorEntity + unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) units = self.hass.config.units - domain = split_entity_id(self.entity_id)[0] - if ( - unit_of_measure in (TEMP_CELSIUS, TEMP_FAHRENHEIT) - and unit_of_measure != units.temperature_unit - and domain != "sensor" + if unit_of_measure == units.temperature_unit or unit_of_measure not in ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ): + return state + + domain = split_entity_id(self.entity_id)[0] + if domain != "sensor": + if not self._temperature_reported: + self._temperature_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) relies on automatic temperature conversion, this will " + "be unsupported in Home Assistant Core 2022.7. Please %s", + self.entity_id, + type(self), + report_issue, + ) + elif not isinstance(self, SensorEntity): + if not self._temperature_reported: + self._temperature_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Temperature sensor %s (%s) does not inherit SensorEntity, " + "this will be unsupported in Home Assistant Core 2022.7." + "Please %s", + self.entity_id, + type(self), + report_issue, + ) + else: + return state + + try: prec = len(state) - state.index(".") - 1 if "." in state else 0 temp = units.temperature(float(state), unit_of_measure) state = str(round(temp) if prec == 0 else round(temp, prec)) attr[ATTR_UNIT_OF_MEASUREMENT] = units.temperature_unit - except ValueError: - # Could not convert state to float - pass + except ValueError: + # Could not convert state to float + pass + return state + + state = _convert_temperature(state, attr) if ( self._context_set is not None diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index afc0887371e..9130de04e0f 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN, + TEMP_FAHRENHEIT, ) from homeassistant.core import Context, HomeAssistantError from homeassistant.helpers import entity, entity_registry @@ -814,6 +815,64 @@ async def test_float_conversion(hass): assert state.state == "3.6" +async def test_temperature_conversion(hass, caplog): + """Test conversion of temperatures.""" + # Non sensor entity reporting a temperature + with patch.object( + entity.Entity, "state", PropertyMock(return_value=100) + ), patch.object( + entity.Entity, "unit_of_measurement", PropertyMock(return_value=TEMP_FAHRENHEIT) + ): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == "38" + assert ( + "Entity hello.world () relies on automatic " + "temperature conversion, this will be unsupported in Home Assistant Core 2022.7. " + "Please create a bug report" in caplog.text + ) + + # Sensor entity, not extending SensorEntity, reporting a temperature + with patch.object( + entity.Entity, "state", PropertyMock(return_value=100) + ), patch.object( + entity.Entity, "unit_of_measurement", PropertyMock(return_value=TEMP_FAHRENHEIT) + ): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "sensor.temp" + ent.async_write_ha_state() + + state = hass.states.get("sensor.temp") + assert state is not None + assert state.state == "38" + assert ( + "Temperature sensor sensor.temp () " + "does not inherit SensorEntity, this will be unsupported in Home Assistant Core " + "2022.7.Please create a bug report" in caplog.text + ) + + # Sensor entity, not extending SensorEntity, not reporting a number + with patch.object( + entity.Entity, "state", PropertyMock(return_value="really warm") + ), patch.object( + entity.Entity, "unit_of_measurement", PropertyMock(return_value=TEMP_FAHRENHEIT) + ): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "sensor.temp" + ent.async_write_ha_state() + + state = hass.states.get("sensor.temp") + assert state is not None + assert state.state == "really warm" + + async def test_attribution_attribute(hass): """Test attribution attribute.""" mock_entity = entity.Entity() From 824066f519321161b47641afcf6b779439dc0e20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 14:30:11 -0700 Subject: [PATCH 0053/1224] Device Automation: enforce passing in device-automation-enum (#69013) --- .../components/device_automation/__init__.py | 42 ++++--------------- .../components/device_automation/test_init.py | 7 ---- .../components/webostv/test_device_trigger.py | 5 ++- 3 files changed, 12 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 75613b3d118..46a3bf6815d 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -20,7 +20,6 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound, bind_hass from homeassistant.requirements import async_get_integration_with_requirements @@ -88,24 +87,6 @@ TYPES = { } -@bind_hass -async def async_get_device_automations( - hass: HomeAssistant, - automation_type: DeviceAutomationType | str, - device_ids: Iterable[str] | None = None, -) -> Mapping[str, Any]: - """Return all the device automations for a type optionally limited to specific device ids.""" - if isinstance(automation_type, str): - report( - "uses str for async_get_device_automations automation_type. This is " - "deprecated and will stop working in Home Assistant 2022.4, it should be " - "updated to use DeviceAutomationType instead", - error_if_core=False, - ) - automation_type = DeviceAutomationType[automation_type.upper()] - return await _async_get_device_automations(hass, automation_type, device_ids) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up device automation.""" websocket_api.async_register_command(hass, websocket_device_automation_list_actions) @@ -156,26 +137,18 @@ async def async_get_device_automation_platform( # noqa: D103 @overload async def async_get_device_automation_platform( # noqa: D103 - hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str + hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType ) -> "DeviceAutomationPlatformType": ... async def async_get_device_automation_platform( - hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str + hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType ) -> "DeviceAutomationPlatformType": """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. """ - if isinstance(automation_type, str): - report( - "uses str for async_get_device_automation_platform automation_type. This " - "is deprecated and will stop working in Home Assistant 2022.4, it should " - "be updated to use DeviceAutomationType instead", - error_if_core=False, - ) - automation_type = DeviceAutomationType[automation_type.upper()] platform_name = automation_type.value.section try: integration = await async_get_integration_with_requirements(hass, domain) @@ -215,10 +188,11 @@ async def _async_get_device_automations_from_domain( ) -async def _async_get_device_automations( +@bind_hass +async def async_get_device_automations( hass: HomeAssistant, automation_type: DeviceAutomationType, - device_ids: Iterable[str] | None, + device_ids: Iterable[str] | None = None, ) -> Mapping[str, list[dict[str, Any]]]: """List device automations.""" device_registry = dr.async_get(hass) @@ -336,7 +310,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] actions = ( - await _async_get_device_automations( + await async_get_device_automations( hass, DeviceAutomationType.ACTION, [device_id] ) ).get(device_id) @@ -355,7 +329,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] conditions = ( - await _async_get_device_automations( + await async_get_device_automations( hass, DeviceAutomationType.CONDITION, [device_id] ) ).get(device_id) @@ -374,7 +348,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] triggers = ( - await _async_get_device_automations( + await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, [device_id] ) ).get(device_id) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index f7b1339014a..ad6a08814ff 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -404,13 +404,6 @@ async def test_async_get_device_automations_single_device_trigger( assert device_entry.id in result assert len(result[device_entry.id]) == 3 - # Test deprecated str automation_type works, to be removed in 2022.4 - result = await device_automation.async_get_device_automations( - hass, "trigger", [device_entry.id] - ) - assert device_entry.id in result - assert len(result[device_entry.id]) == 3 # toggled, turned_on, turned_off - async def test_async_get_device_automations_all_devices_trigger( hass, device_reg, entity_reg diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index fb8512d56f1..337b4a8acae 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -31,7 +32,9 @@ async def test_get_triggers(hass, client): "device_id": device.id, } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert turn_on_trigger in triggers From 130ca2213f865b7d90fc36cf5a84fbddd9f4e0b1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Mar 2022 15:04:33 -0700 Subject: [PATCH 0054/1224] Enforce EntityCategory enum (#69015) Co-authored-by: Franck Nijhof --- homeassistant/components/mqtt/mixins.py | 2 +- homeassistant/components/neato/switch.py | 2 +- homeassistant/components/update/__init__.py | 2 +- homeassistant/helpers/entity.py | 32 +------------- homeassistant/helpers/entity_registry.py | 44 ++++++++++--------- tests/components/cloud/test_alexa_config.py | 7 +-- tests/components/cloud/test_google_config.py | 7 +-- .../google_assistant/test_google_assistant.py | 7 +-- tests/helpers/test_entity_platform.py | 10 +++-- tests/helpers/test_entity_registry.py | 33 +++++--------- tests/helpers/test_service.py | 7 +-- 11 files changed, 63 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 659d0debe31..09efb384196 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -814,7 +814,7 @@ class MqttEntity( return self._config[CONF_ENABLED_BY_DEFAULT] @property - def entity_category(self) -> EntityCategory | str | None: + def entity_category(self) -> EntityCategory | None: """Return the entity category if any.""" return self._config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8817e280f43..52226292248 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -108,7 +108,7 @@ class NeatoConnectedSwitch(SwitchEntity): ) @property - def entity_category(self) -> str: + def entity_category(self) -> EntityCategory: """Device entity category.""" return EntityCategory.CONFIG diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 1ecb5d13090..0a78840e66e 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -183,7 +183,7 @@ class UpdateEntity(RestoreEntity): return None @property - def entity_category(self) -> EntityCategory | str | None: + def entity_category(self) -> EntityCategory | None: """Return the category of the entity, if any.""" if hasattr(self, "_attr_entity_category"): return self._attr_entity_category diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 75ac1e0c1b8..f6869787f5b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -52,7 +52,6 @@ from . import entity_registry as er from .device_registry import DeviceEntryType from .entity_platform import EntityPlatform from .event import async_track_entity_registry_updated_event -from .frame import report from .typing import StateType _LOGGER = logging.getLogger(__name__) @@ -228,29 +227,6 @@ class EntityPlatformState(Enum): REMOVED = auto() -def convert_to_entity_category( - value: EntityCategory | str | None, raise_report: bool = True -) -> EntityCategory | None: - """Force incoming entity_category to be an enum.""" - - if value is None: - return value - - if not isinstance(value, EntityCategory): - if raise_report: - report( - "uses %s (%s) for entity category. This is deprecated and will " - "stop working in Home Assistant 2022.4, it should be updated to use " - "EntityCategory instead" % (type(value).__name__, value), - error_if_core=False, - ) - try: - return EntityCategory(value) - except ValueError: - return None - return value - - @dataclass class EntityDescription: """A class that describes Home Assistant entities.""" @@ -259,10 +235,7 @@ class EntityDescription: key: str device_class: str | None = None - # Type string is deprecated as of 2021.12, use EntityCategory - entity_category: EntityCategory | Literal[ - "config", "diagnostic", "system" - ] | None = None + entity_category: EntityCategory | None = None entity_registry_enabled_default: bool = True force_update: bool = False icon: str | None = None @@ -491,9 +464,8 @@ class Entity(ABC): """Return the attribution.""" return self._attr_attribution - # Type str is deprecated as of 2021.12, use EntityCategory @property - def entity_category(self) -> EntityCategory | str | None: + def entity_category(self) -> EntityCategory | None: """Return the category of the entity, if any.""" if hasattr(self, "_attr_entity_category"): return self._attr_entity_category diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 6c1c443ad2b..f171f3f7f70 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -91,16 +91,6 @@ class RegistryEntryHider(StrEnum): USER = "user" -def _convert_to_entity_category( - value: EntityCategory | str | None, raise_report: bool = True -) -> EntityCategory | None: - """Force incoming entity_category to be an enum.""" - # pylint: disable=import-outside-toplevel - from .entity import convert_to_entity_category - - return convert_to_entity_category(value, raise_report=raise_report) - - @attr.s(slots=True, frozen=True) class RegistryEntry: """Entity Registry Entry.""" @@ -115,9 +105,7 @@ class RegistryEntry: device_id: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) disabled_by: RegistryEntryDisabler | None = attr.ib(default=None) - entity_category: EntityCategory | None = attr.ib( - default=None, converter=_convert_to_entity_category - ) + entity_category: EntityCategory | None = attr.ib(default=None) hidden_by: RegistryEntryHider | None = attr.ib(default=None) icon: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) @@ -339,8 +327,7 @@ class EntityRegistry: capabilities: Mapping[str, Any] | None = None, config_entry: ConfigEntry | None = None, device_id: str | None = None, - # Type str (ENTITY_CATEG*) is deprecated as of 2021.12, use EntityCategory - entity_category: EntityCategory | str | None = None, + entity_category: EntityCategory | None = None, original_device_class: str | None = None, original_icon: str | None = None, original_name: str | None = None, @@ -390,13 +377,18 @@ class EntityRegistry: ): disabled_by = RegistryEntryDisabler.INTEGRATION + from .entity import EntityCategory # pylint: disable=import-outside-toplevel + + if entity_category and not isinstance(entity_category, EntityCategory): + raise ValueError("entity_category must be a valid EntityCategory instance") + entry = RegistryEntry( area_id=area_id, capabilities=capabilities, config_entry_id=config_entry_id, device_id=device_id, disabled_by=disabled_by, - entity_category=_convert_to_entity_category(entity_category), + entity_category=entity_category, entity_id=entity_id, hidden_by=hidden_by, original_device_class=original_device_class, @@ -502,8 +494,7 @@ class EntityRegistry: device_class: str | None | UndefinedType = UNDEFINED, device_id: str | None | UndefinedType = UNDEFINED, disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED, - # Type str (ENTITY_CATEG*) is deprecated as of 2021.12, use EntityCategory - entity_category: EntityCategory | str | None | UndefinedType = UNDEFINED, + entity_category: EntityCategory | None | UndefinedType = UNDEFINED, hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, @@ -528,6 +519,15 @@ class EntityRegistry: ): raise ValueError("disabled_by must be a RegistryEntryDisabler value") + from .entity import EntityCategory # pylint: disable=import-outside-toplevel + + if ( + entity_category + and entity_category is not UNDEFINED + and not isinstance(entity_category, EntityCategory) + ): + raise ValueError("entity_category must be a valid EntityCategory instance") + for attr_name, value in ( ("area_id", area_id), ("capabilities", capabilities), @@ -629,6 +629,8 @@ class EntityRegistry: ) entities = EntityRegistryItems() + from .entity import EntityCategory # pylint: disable=import-outside-toplevel + if data is not None: for entity in data["entities"]: # Some old installations can have some bad entities. @@ -646,9 +648,9 @@ class EntityRegistry: disabled_by=RegistryEntryDisabler(entity["disabled_by"]) if entity["disabled_by"] else None, - entity_category=_convert_to_entity_category( - entity["entity_category"], raise_report=False - ), + entity_category=EntityCategory(entity["entity_category"]) + if entity["entity_category"] + else None, entity_id=entity["entity_id"], hidden_by=entity["hidden_by"], icon=entity["icon"], diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index ced2a664763..115d39d3aeb 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -8,6 +8,7 @@ from homeassistant.components.alexa import errors from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -28,21 +29,21 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): "test", "light_config_id", suggested_object_id="config_light", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) entity_entry2 = entity_registry.async_get_or_create( "light", "test", "light_diagnostic_id", suggested_object_id="diagnostic_light", - entity_category="diagnostic", + entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( "light", "test", "light_system_id", suggested_object_id="system_light", - entity_category="system", + entity_category=EntityCategory.SYSTEM, ) entity_entry4 = entity_registry.async_get_or_create( "light", diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 1fbff368602..e7867f52e6c 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -10,6 +10,7 @@ from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, State from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -227,21 +228,21 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): "test", "light_config_id", suggested_object_id="config_light", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) entity_entry2 = entity_registry.async_get_or_create( "light", "test", "light_diagnostic_id", suggested_object_id="diagnostic_light", - entity_category="diagnostic", + entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( "light", "test", "light_system_id", suggested_object_id="system_light", - entity_category="system", + entity_category=EntityCategory.SYSTEM, ) entity_entry4 = entity_registry.async_get_or_create( "light", diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index fe86aae4b1a..642ef15451a 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -22,6 +22,7 @@ from homeassistant.components.climate import const as climate from homeassistant.components.humidifier import const as humidifier from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from . import DEMO_DEVICES @@ -136,21 +137,21 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): "test", "switch_config_id", suggested_object_id="config_switch", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) entity_entry2 = entity_registry.async_get_or_create( "switch", "test", "switch_diagnostic_id", suggested_object_id="diagnostic_switch", - entity_category="diagnostic", + entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( "switch", "test", "switch_system_id", suggested_object_id="system_switch", - entity_category="system", + entity_category=EntityCategory.SYSTEM, ) entity_entry4 = entity_registry.async_get_or_create( "switch", diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index c98fdff7858..ff29897bb12 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -14,7 +14,11 @@ from homeassistant.helpers import ( entity_platform, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.entity import ( + DeviceInfo, + EntityCategory, + async_generate_entity_id, +) from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, @@ -1151,7 +1155,7 @@ async def test_entity_info_added_to_entity_registry(hass): entity_default = MockEntity( capability_attributes={"max": 100}, device_class="mock-device-class", - entity_category="config", + entity_category=EntityCategory.CONFIG, icon="nice:icon", name="best name", supported_features=5, @@ -1170,7 +1174,7 @@ async def test_entity_info_added_to_entity_registry(hass): "test_domain", capabilities={"max": 100}, device_class=None, - entity_category="config", + entity_category=EntityCategory.CONFIG, icon=None, id=ANY, name=None, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index eeac78a2654..ab4e15289b9 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1233,26 +1233,17 @@ async def test_disabled_by_str_not_allowed(hass): ) -async def test_deprecated_entity_category_str(hass, registry, caplog): - """Test deprecated str use of entity_category converts to enum and logs a warning.""" - entry = er.RegistryEntry( - entity_id="light.kitchen", - unique_id="5678", - platform="hue", - entity_category="diagnostic", - ) +async def test_entity_category_str_not_allowed(hass): + """Test we need to pass entity category type.""" + reg = er.async_get(hass) - assert entry.entity_category is EntityCategory.DIAGNOSTIC - assert " should be updated to use EntityCategory" in caplog.text + with pytest.raises(ValueError): + reg.async_get_or_create( + "light", "hue", "1234", entity_category=EntityCategory.DIAGNOSTIC.value + ) - -async def test_invalid_entity_category_str(hass, registry, caplog): - """Test use of invalid entity category.""" - entry = er.RegistryEntry( - entity_id="light.kitchen", - unique_id="5678", - platform="hue", - entity_category="invalid", - ) - - assert entry.entity_category is None + entity_id = reg.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(ValueError): + reg.async_update_entity( + entity_id, entity_category=EntityCategory.DIAGNOSTIC.value + ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 7bde035d1f8..1b8de6ca6e2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -25,6 +25,7 @@ from homeassistant.helpers import ( template, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component from tests.common import ( @@ -119,7 +120,7 @@ def area_mock(hass): unique_id="config-in-own-area-id", platform="test", area_id="own-area", - entity_category="config", + entity_category=EntityCategory.CONFIG, ) hidden_entity_in_own_area = ent_reg.RegistryEntry( entity_id="light.hidden_in_own_area", @@ -139,7 +140,7 @@ def area_mock(hass): unique_id="config-in-area-id", platform="test", device_id=device_in_area.id, - entity_category="config", + entity_category=EntityCategory.CONFIG, ) hidden_entity_in_area = ent_reg.RegistryEntry( entity_id="light.hidden_in_area", @@ -173,7 +174,7 @@ def area_mock(hass): unique_id="config-no-area-id", platform="test", device_id=device_no_area.id, - entity_category="config", + entity_category=EntityCategory.CONFIG, ) hidden_entity_no_area = ent_reg.RegistryEntry( entity_id="light.hidden_no_area", From a741b26e2866af521409f10e98f11e437815fda2 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 31 Mar 2022 23:05:39 +0100 Subject: [PATCH 0055/1224] Improve image checks for generic camera (#69037) --- homeassistant/components/generic/config_flow.py | 12 +++++++++--- homeassistant/components/generic/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index c5c645264d6..d3b2a260477 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -4,12 +4,13 @@ from __future__ import annotations import contextlib from errno import EHOSTUNREACH, EIO from functools import partial -import imghdr +import io import logging from types import MappingProxyType from typing import Any from urllib.parse import urlparse, urlunparse +import PIL from async_timeout import timeout import av from httpx import HTTPStatusError, RequestError, TimeoutException @@ -110,9 +111,14 @@ def build_schema( def get_image_type(image): """Get the format of downloaded bytes that could be an image.""" - fmt = imghdr.what(None, h=image) + fmt = None + imagefile = io.BytesIO(image) + with contextlib.suppress(PIL.UnidentifiedImageError): + img = PIL.Image.open(imagefile) + fmt = img.format.lower() + if fmt is None: - # if imghdr can't figure it out, could be svg. + # if PIL can't figure it out, could be svg. with contextlib.suppress(UnicodeDecodeError): if image.decode("utf-8").lstrip().startswith(" Date: Fri, 1 Apr 2022 00:15:53 +0200 Subject: [PATCH 0056/1224] Update KNX flow strings to use "data_description" and remove invalid options (#68935) * use `data_description` * use selectors so `data_description` shows * remove unused config option `individual_address` has no effect for tunneling. The address is always assigned by the tunnelling server. * remove unsupported option for tunneling V1 devices --- homeassistant/components/knx/config_flow.py | 67 +++++++++------ homeassistant/components/knx/strings.json | 65 ++++++++++----- .../components/knx/translations/en.json | 81 +++++++++++++------ 3 files changed, 144 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 3cedf518e1a..e7e8854a9fb 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import selector from homeassistant.helpers.storage import STORAGE_DIR from .const import ( @@ -63,6 +63,10 @@ CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure" CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP" CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode" +_IA_SELECTOR = selector.selector({"text": {}}) +_IP_SELECTOR = selector.selector({"text": {}}) +_PORT_SELECTOR = selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}) + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a KNX config flow.""" @@ -164,7 +168,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): **DEFAULT_ENTRY_DATA, # type: ignore[misc] CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], - CONF_KNX_INDIVIDUAL_ADDRESS: user_input[CONF_KNX_INDIVIDUAL_ADDRESS], CONF_KNX_ROUTE_BACK: ( connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK ), @@ -202,18 +205,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): port = self._selected_tunnel.port if not self._selected_tunnel.supports_tunnelling_tcp: connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP) + connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE) fields = { vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods), - vol.Required(CONF_HOST, default=ip_address): str, - vol.Required(CONF_PORT, default=port): cv.port, - vol.Required( - CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): str, + vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR, + vol.Required(CONF_PORT, default=port): _PORT_SELECTOR, } if self.show_advanced_options: - fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str + fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR return self.async_show_form( step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors @@ -245,9 +246,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) fields = { - vol.Required(CONF_KNX_SECURE_USER_ID): int, - vol.Required(CONF_KNX_SECURE_USER_PASSWORD): str, - vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): str, + vol.Required(CONF_KNX_SECURE_USER_ID, default=2): selector.selector( + {"number": {"min": 1, "max": 127, "mode": "box"}} + ), + vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector( + {"text": {"type": "password"}} + ), + vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector( + {"text": {"type": "password"}} + ), } return self.async_show_form( @@ -290,8 +297,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "file_not_found" fields = { - vol.Required(CONF_KNX_KNXKEY_FILENAME): str, - vol.Required(CONF_KNX_KNXKEY_PASSWORD): str, + vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}), + vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}), } return self.async_show_form( @@ -319,13 +326,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): fields = { vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): str, - vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): str, - vol.Required(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, + ): _IA_SELECTOR, + vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): _IP_SELECTOR, + vol.Required( + CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT + ): _PORT_SELECTOR, } if self.show_advanced_options: - fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str + fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR return self.async_show_form( step_id="routing", data_schema=vol.Schema(fields), errors=errors @@ -370,17 +379,17 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS], - ): str, + ): selector.selector({"text": {}}), vol.Required( CONF_KNX_MCAST_GRP, default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP), - ): str, + ): _IP_SELECTOR, vol.Required( CONF_KNX_MCAST_PORT, default=self.current_config.get( CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT ), - ): cv.port, + ): _PORT_SELECTOR, } if self.show_advanced_options: @@ -394,7 +403,7 @@ class KNXOptionsFlowHandler(OptionsFlow): CONF_KNX_LOCAL_IP, default=local_ip, ) - ] = str + ] = _IP_SELECTOR data_schema[ vol.Required( CONF_KNX_STATE_UPDATER, @@ -403,7 +412,7 @@ class KNXOptionsFlowHandler(OptionsFlow): CONF_KNX_DEFAULT_STATE_UPDATER, ), ) - ] = bool + ] = selector.selector({"boolean": {}}) data_schema[ vol.Required( CONF_KNX_RATE_LIMIT, @@ -412,7 +421,15 @@ class KNXOptionsFlowHandler(OptionsFlow): CONF_KNX_DEFAULT_RATE_LIMIT, ), ) - ] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT)) + ] = selector.selector( + { + "number": { + "min": 1, + "max": CONF_MAX_RATE_LIMIT, + "mode": "box", + } + } + ) return self.async_show_form( step_id="init", @@ -444,10 +461,10 @@ class KNXOptionsFlowHandler(OptionsFlow): ): vol.In(connection_methods), vol.Required( CONF_HOST, default=self.current_config.get(CONF_HOST) - ): str, + ): _IP_SELECTOR, vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) - ): cv.port, + ): _PORT_SELECTOR, } ), last_step=True, diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 1a7f3481a1a..2149dd96a47 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -19,39 +19,56 @@ "tunneling_type": "KNX Tunneling Type", "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "individual_address": "Individual address for the connection", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)" + "local_ip": "Local IP of Home Assistant" + }, + "data_description": { + "port": "Port of the KNX/IP tunneling device.", + "host": "IP address of the KNX/IP tunneling device.", + "local_ip": "Leave blank to use auto-discovery." } }, "secure_tunneling": { - "description": "Select how you want to configure IP Secure.", + "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { - "secure_knxkeys": "Configure a knxkeys file containing IP secure information", - "secure_manual": "Configure IP secure manually" + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_manual": "Configure IP secure keys manually" } }, "secure_knxkeys": { - "description": "Please enter the information for your knxkeys file.", + "description": "Please enter the information for your `.knxkeys` file.", "data": { - "knxkeys_filename": "The full name of your knxkeys file", - "knxkeys_password": "The password to decrypt the knxkeys file" + "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", + "knxkeys_password": "The password to decrypt the `.knxkeys` file" + }, + "data_description": { + "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", + "knxkeys_password": "This was set when exporting the file from ETS." } }, "secure_manual": { - "description": "Please enter the IP secure information.", + "description": "Please enter your IP secure information.", "data": { "user_id": "User ID", "user_password": "User password", "device_authentication": "Device authentication password" + }, + "data_description": { + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS.", + "device_authentication": "This is set in the 'IP' panel of the interface in ETS." } }, "routing": { "description": "Please configure the routing options.", "data": { - "individual_address": "Individual address for the routing connection", - "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)" + "individual_address": "Individual address", + "multicast_group": "Multicast group used for routing", + "multicast_port": "Multicast port used for routing", + "local_ip": "Local IP of Home Assistant" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Leave blank to use auto-discovery." } } }, @@ -71,11 +88,19 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", - "multicast_group": "Multicast group used for routing and discovery", - "multicast_port": "Multicast port used for routing and discovery", - "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", - "state_updater": "Globally enable reading states from the KNX Bus", - "rate_limit": "Maximum outgoing telegrams per second" + "multicast_group": "Multicast group", + "multicast_port": "Multicast port", + "local_ip": "Local IP of Home Assistant", + "state_updater": "State updater", + "rate_limit": "Rate limit" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", + "multicast_port": "Used for routing and discovery. Default: `3671`", + "local_ip": "Use `0.0.0.0` for auto-discovery.", + "state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect.", + "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40" } }, "tunnel": { @@ -83,6 +108,10 @@ "tunneling_type": "KNX Tunneling Type", "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "port": "Port of the KNX/IP tunneling device.", + "host": "IP address of the KNX/IP tunneling device." } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index f5ec7afc46b..640cb4a5358 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -13,44 +13,61 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Individual address for the connection", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", + "local_ip": "Local IP of Home Assistant", "port": "Port", "tunneling_type": "KNX Tunneling Type" }, + "data_description": { + "host": "IP address of the KNX/IP tunneling device.", + "local_ip": "Leave blank to use auto-discovery.", + "port": "Port of the KNX/IP tunneling device." + }, "description": "Please enter the connection information of your tunneling device." }, "routing": { "data": { - "individual_address": "Individual address for the routing connection", - "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", - "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" + "individual_address": "Individual address", + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group used for routing", + "multicast_port": "Multicast port used for routing" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Leave blank to use auto-discovery." }, "description": "Please configure the routing options." }, "secure_knxkeys": { "data": { - "knxkeys_filename": "The full name of your knxkeys file", - "knxkeys_password": "The password to decrypt the knxkeys file." + "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", + "knxkeys_password": "The password to decrypt the `.knxkeys` file" }, - "description": "Please enter the information for your knxkeys file." - }, - "secure_tunneling": { - "description": "Select how you want to configure IP Secure.", - "menu_options": { - "secure_knxkeys": "Configure a knxkeys file containing IP secure information", - "secure_manual": "Configure IP secure manually" - } + "data_description": { + "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", + "knxkeys_password": "This was set when exporting the file from ETS." + }, + "description": "Please enter the information for your `.knxkeys` file." }, "secure_manual": { - "description": "Please enter the IP secure information.", "data": { - "user_id": "User ID", - "user_password": "User password", - "device_authentication": "Device authentication password" + "device_authentication": "Device authentication password", + "user_id": "User ID", + "user_password": "User password" + }, + "data_description": { + "device_authentication": "This is set in the 'IP' panel of the interface in ETS.", + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." + }, + "description": "Please enter your IP secure information." + }, + "secure_tunneling": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_manual": "Configure IP secure keys manually" } - }, + }, "tunnel": { "data": { "gateway": "KNX Tunnel Connection" @@ -71,11 +88,19 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", - "local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)", - "multicast_group": "Multicast group used for routing and discovery", - "multicast_port": "Multicast port used for routing and discovery", - "rate_limit": "Maximum outgoing telegrams per second", - "state_updater": "Globally enable reading states from the KNX Bus" + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group", + "multicast_port": "Multicast port", + "rate_limit": "Rate limit", + "state_updater": "State updater" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Use `0.0.0.0` for auto-discovery.", + "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", + "multicast_port": "Used for routing and discovery. Default: `3671`", + "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40", + "state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect." } }, "tunnel": { @@ -83,6 +108,10 @@ "host": "Host", "port": "Port", "tunneling_type": "KNX Tunneling Type" + }, + "data_description": { + "host": "IP address of the KNX/IP tunneling device.", + "port": "Port of the KNX/IP tunneling device." } } } From 2c49323b5fcf2cbf3cbe33f51233277b678af5e6 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 1 Apr 2022 01:39:47 +0300 Subject: [PATCH 0057/1224] Remove update throttle in LG Netcast (#68902) --- homeassistant/components/lg_netcast/media_player.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index a76d74481d9..d7f2e5ca3a2 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -1,13 +1,12 @@ """Support for LG TV running on NetCast 3 or 4.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime from pylgnetcast import LgNetCastClient, LgNetCastError from requests import RequestException import voluptuous as vol -from homeassistant import util from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerDeviceClass, @@ -47,9 +46,6 @@ DEFAULT_NAME = "LG TV Remote" CONF_ON_ACTION = "turn_on_action" -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - SUPPORT_LGTV = ( SUPPORT_PAUSE | SUPPORT_VOLUME_STEP @@ -122,7 +118,6 @@ class LgTVDevice(MediaPlayerEntity): except (LgNetCastError, RequestException): self._state = STATE_OFF - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data from the LG TV.""" From 91404041e08beab99b8cd1c48b372b617f8e6bcc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 00:54:13 +0200 Subject: [PATCH 0058/1224] Update jinja2 to 3.1.1 (#68988) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 641a78fb4a4..8c7b86db1c3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ hass-nabucasa==0.54.0 home-assistant-frontend==20220330.0 httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.1.0 +jinja2==3.1.1 lru-dict==1.1.7 paho-mqtt==1.6.1 pillow==9.0.1 diff --git a/requirements.txt b/requirements.txt index d86a7be1e73..e91988d10cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.1.0 +jinja2==3.1.1 PyJWT==2.3.0 cryptography==35.0.0 pip>=21.0,<22.1 diff --git a/setup.cfg b/setup.cfg index 1aef468f0ad..6d99009e347 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = # httpcore, anyio, and h11 in gen_requirements_all httpx==0.22.0 ifaddr==0.1.7 - jinja2==3.1.0 + jinja2==3.1.1 PyJWT==2.3.0 # PyJWT has loose dependency. We want the latest one. cryptography==35.0.0 From 94a8d751429427f0d8bb3abfec954f30a93b0bb2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 1 Apr 2022 01:44:52 -0400 Subject: [PATCH 0059/1224] Fix zwave_js device action logic (#69049) * Fix zwave_js device action logic * Add test for this behavior --- .../components/zwave_js/device_action.py | 6 +-- .../components/zwave_js/test_device_action.py | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 7e5e8c6c78d..a67bd44e533 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -241,7 +241,7 @@ async def async_call_action_from_config( hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - action_type = service = config.pop(CONF_TYPE) + action_type = service = config[CONF_TYPE] if action_type not in ACTION_TYPES: raise HomeAssistantError(f"Unhandled action type {action_type}") @@ -249,10 +249,10 @@ async def async_call_action_from_config( service_data = { k: v for k, v in config.items() - if k not in (ATTR_DOMAIN, CONF_SUBTYPE) and v not in (None, "") + if k not in (ATTR_DOMAIN, CONF_TYPE, CONF_SUBTYPE) and v not in (None, "") } - # Entity services (including refresh value which is a fake entity service) expects + # Entity services (including refresh value which is a fake entity service) expect # just an entity ID if action_type in ( SERVICE_REFRESH_VALUE, diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 07663ce9456..657540db94d 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -169,6 +169,15 @@ async def test_actions( }, ) + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + # Call action a second time to confirm that it works (this was previously a bug) with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: hass.bus.async_fire("test_event_refresh_value") await hass.async_block_till_done() @@ -206,6 +215,51 @@ async def test_actions( assert args[2] == 1 +async def test_actions_multiple_calls( + hass: HomeAssistant, + client: Client, + climate_radio_thermostat_ct100_plus: Node, + integration: ConfigEntry, +) -> None: + """Test actions can be called multiple times and still work.""" + node = climate_radio_thermostat_ct100_plus + device_id = get_device_id(client, node) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_device({device_id}) + assert device + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_refresh_value", + }, + "action": { + "domain": DOMAIN, + "type": "refresh_value", + "device_id": device.id, + "entity_id": "climate.z_wave_thermostat", + }, + }, + ] + }, + ) + + # Trigger automation multiple times to confirm that it works each time + for _ in range(5): + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + async def test_lock_actions( hass: HomeAssistant, client: Client, From 68d563c630a69fec4cf0c5a50136f68049d20c62 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 31 Mar 2022 22:46:48 -0700 Subject: [PATCH 0060/1224] Remove calendar mypy ignores, now that calendar has full typing (#69051) --- homeassistant/components/calendar/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 4449084373b..d58903e8b0c 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -24,8 +24,6 @@ from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" From 4a921ac67fbd200ea6c8cd32af13f6e55a7835e3 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 1 Apr 2022 11:05:59 +0300 Subject: [PATCH 0061/1224] Remove webostv deprecated YAML import (#69043) * webostv: remove deprecated YAML import * Remove unused CUSTOMIZE_SCHEMA and WEBOSTV_CONFIG_FILE --- homeassistant/components/webostv/__init__.py | 145 +----------------- .../components/webostv/config_flow.py | 30 +--- homeassistant/components/webostv/const.py | 2 - .../components/webostv/manifest.json | 2 +- requirements_all.txt | 1 - requirements_test_all.txt | 1 - tests/components/webostv/__init__.py | 55 +------ tests/components/webostv/test_config_flow.py | 61 -------- tests/components/webostv/test_init.py | 141 ----------------- 9 files changed, 8 insertions(+), 430 deletions(-) delete mode 100644 tests/components/webostv/test_init.py diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 9f78dcd0964..e759077ad3e 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,33 +1,24 @@ """Support for LG webOS Smart TV.""" from __future__ import annotations -import asyncio from collections.abc import Callable from contextlib import suppress -import json import logging -import os -from pickle import loads from typing import Any from aiowebostv import WebOsClient, WebOsTvPairError -import sqlalchemy as db import voluptuous as vol from homeassistant.components import notify as hass_notify from homeassistant.components.automation import AutomationActionType -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, CONF_CLIENT_SECRET, - CONF_CUSTOMIZE, CONF_HOST, - CONF_ICON, CONF_NAME, - CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_STOP, - Platform, ) from homeassistant.core import ( Context, @@ -37,7 +28,7 @@ from homeassistant.core import ( ServiceCall, callback, ) -from homeassistant.helpers import config_validation as cv, discovery, entity_registry +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -46,46 +37,17 @@ from .const import ( ATTR_CONFIG_ENTRY_ID, ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, - CONF_ON_ACTION, - CONF_SOURCES, DATA_CONFIG_ENTRY, DATA_HASS_CONFIG, - DEFAULT_NAME, DOMAIN, PLATFORMS, SERVICE_BUTTON, SERVICE_COMMAND, SERVICE_SELECT_SOUND_OUTPUT, - WEBOSTV_CONFIG_FILE, WEBOSTV_EXCEPTIONS, ) -CUSTOMIZE_SCHEMA = vol.Schema( - {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_ICON): cv.string, - } - ) - ], - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) @@ -109,113 +71,12 @@ SERVICE_TO_METHOD = { _LOGGER = logging.getLogger(__name__) -def read_client_keys(config_file: str) -> dict[str, str]: - """Read legacy client keys from file.""" - if not os.path.isfile(config_file): - return {} - - # Try to parse the file as being JSON - with open(config_file, encoding="utf8") as json_file: - try: - client_keys = json.load(json_file) - if isinstance(client_keys, dict): - return client_keys - return {} - except (json.JSONDecodeError, UnicodeDecodeError): - pass - - # If the file is not JSON, read it as Sqlite DB - engine = db.create_engine(f"sqlite:///{config_file}") - table = db.Table("unnamed", db.MetaData(), autoload=True, autoload_with=engine) - results = engine.connect().execute(db.select([table])).fetchall() - db_client_keys = {k: loads(v) for k, v in results} - return db_client_keys - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LG WebOS TV platform.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = config - if DOMAIN not in config: - return True - - config_file = hass.config.path(WEBOSTV_CONFIG_FILE) - if not ( - client_keys := await hass.async_add_executor_job(read_client_keys, config_file) - ): - _LOGGER.debug("No pairing keys, Not importing webOS Smart TV YAML config") - return True - - async def async_migrate_task( - entity_id: str, conf: dict[str, str], key: str - ) -> None: - _LOGGER.debug("Migrating webOS Smart TV entity %s unique_id", entity_id) - client = WebOsClient(conf[CONF_HOST], key) - tries = 0 - while not client.is_connected(): - try: - await client.connect() - except WEBOSTV_EXCEPTIONS: - if tries == 0: - _LOGGER.warning( - "Please make sure webOS TV %s is turned on to complete " - "the migration of configuration.yaml to the UI", - entity_id, - ) - wait_time = 2 ** min(tries, 4) * 5 - tries += 1 - await asyncio.sleep(wait_time) - except WebOsTvPairError: - return - - ent_reg = entity_registry.async_get(hass) - if not ( - new_entity_id := ent_reg.async_get_entity_id( - Platform.MEDIA_PLAYER, DOMAIN, key - ) - ): - _LOGGER.debug( - "Not updating webOSTV Smart TV entity %s unique_id, entity missing", - entity_id, - ) - return - - uuid = client.hello_info["deviceUUID"] - ent_reg.async_update_entity(new_entity_id, new_unique_id=uuid) - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - **conf, - CONF_CLIENT_SECRET: key, - CONF_UNIQUE_ID: uuid, - }, - ) - - ent_reg = entity_registry.async_get(hass) - - tasks = [] - for conf in config[DOMAIN]: - host = conf[CONF_HOST] - if (key := client_keys.get(host)) is None: - _LOGGER.debug( - "Not importing webOS Smart TV host %s without pairing key", host - ) - continue - - if entity_id := ent_reg.async_get_entity_id(Platform.MEDIA_PLAYER, DOMAIN, key): - tasks.append(asyncio.create_task(async_migrate_task(entity_id, conf, key))) - - async def async_tasks_cancel(_event: Event) -> None: - """Cancel config flow import tasks.""" - for task in tasks: - if not task.done(): - task.cancel() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_tasks_cancel) - return True diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 18338e86f1a..85da9250539 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -10,13 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp -from homeassistant.const import ( - CONF_CLIENT_SECRET, - CONF_CUSTOMIZE, - CONF_HOST, - CONF_NAME, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv @@ -55,28 +49,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: - """Set the config entry up from yaml.""" - self._host = import_info[CONF_HOST] - self._name = import_info.get(CONF_NAME) or import_info[CONF_HOST] - await self.async_set_unique_id( - import_info[CONF_UNIQUE_ID], raise_on_progress=False - ) - data = { - CONF_HOST: self._host, - CONF_CLIENT_SECRET: import_info[CONF_CLIENT_SECRET], - } - self._abort_if_unique_id_configured() - - options: dict[str, list[str]] | None = None - if sources := import_info.get(CONF_CUSTOMIZE, {}).get(CONF_SOURCES): - if not isinstance(sources, list): - sources = [s.strip() for s in sources.split(",")] - options = {CONF_SOURCES: sources} - - _LOGGER.debug("WebOS Smart TV host %s imported from YAML config", self._host) - return self.async_create_entry(title=self._name, data=data, options=options) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index 9be44d86469..f471ca7340d 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -33,5 +33,3 @@ WEBOSTV_EXCEPTIONS = ( asyncio.TimeoutError, asyncio.CancelledError, ) - -WEBOSTV_CONFIG_FILE = "webostv.conf" diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 2725609f119..81c4d04901f 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.2.0", "sqlalchemy==1.4.32"], + "requirements": ["aiowebostv==0.2.0"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 4953f1bc161..6d5884d725f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2194,7 +2194,6 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -# homeassistant.components.webostv sqlalchemy==1.4.32 # homeassistant.components.srp_energy diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de51f1611b3..b0b0f7ffcf9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1419,7 +1419,6 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -# homeassistant.components.webostv sqlalchemy==1.4.32 # homeassistant.components.srp_energy diff --git a/tests/components/webostv/__init__.py b/tests/components/webostv/__init__.py index 1cbc72b43fc..5ef210da56d 100644 --- a/tests/components/webostv/__init__.py +++ b/tests/components/webostv/__init__.py @@ -1,17 +1,10 @@ """Tests for the WebOS TV integration.""" -from pickle import dumps -from unittest.mock import patch -import sqlalchemy as db -from sqlalchemy import create_engine - -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.webostv.const import DOMAIN from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST -from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_CLIENT_KEYS, TV_NAME +from .const import CLIENT_KEY, FAKE_UUID, HOST, TV_NAME from tests.common import MockConfigEntry @@ -29,53 +22,11 @@ async def setup_webostv(hass, unique_id=FAKE_UUID): ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.webostv.read_client_keys", - return_value=MOCK_CLIENT_KEYS, - ): - await async_setup_component( - hass, - DOMAIN, - {DOMAIN: {CONF_HOST: HOST}}, - ) - await hass.async_block_till_done() - - return entry - - -async def setup_legacy_component(hass, create_entity=True): - """Initialize webostv component with legacy entity.""" - if create_entity: - ent_reg = entity_registry.async_get(hass) - assert ent_reg.async_get_or_create(MP_DOMAIN, DOMAIN, CLIENT_KEY) - - assert await async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: HOST}}, ) await hass.async_block_till_done() - -def create_memory_sqlite_engine(url): - """Create fake db keys file in memory.""" - mem_eng = create_engine("sqlite:///:memory:") - table = db.Table( - "unnamed", - db.MetaData(), - db.Column("key", db.String), - db.Column("value", db.String), - ) - table.create(mem_eng) - query = db.insert(table).values(key=HOST, value=dumps(CLIENT_KEY)) - connection = mem_eng.connect() - connection.execute(query) - return mem_eng - - -def is_entity_unique_id_updated(hass): - """Check if entity has new unique_id from UUID.""" - ent_reg = entity_registry.async_get(hass) - return ent_reg.async_get_entity_id( - MP_DOMAIN, DOMAIN, FAKE_UUID - ) and not ent_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, CLIENT_KEY) + return entry diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index a874532ff51..012682615dc 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -11,7 +11,6 @@ from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import ( CONF_CLIENT_SECRET, - CONF_CUSTOMIZE, CONF_HOST, CONF_ICON, CONF_NAME, @@ -46,66 +45,6 @@ MOCK_DISCOVERY_INFO = ssdp.SsdpServiceInfo( ) -async def test_import(hass, client): - """Test we can import yaml config.""" - assert client - - with patch("homeassistant.components.webostv.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: config_entries.SOURCE_IMPORT}, - data=MOCK_YAML_CONFIG, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == TV_NAME - assert result["data"][CONF_HOST] == MOCK_YAML_CONFIG[CONF_HOST] - assert result["data"][CONF_CLIENT_SECRET] == MOCK_YAML_CONFIG[CONF_CLIENT_SECRET] - assert result["result"].unique_id == MOCK_YAML_CONFIG[CONF_UNIQUE_ID] - - with patch("homeassistant.components.webostv.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: config_entries.SOURCE_IMPORT}, - data=MOCK_YAML_CONFIG, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.parametrize( - "sources", - [ - ["Live TV", "Input01", "Input02"], - "Live TV, Input01 , Input02", - "Live TV,Input01 ,Input02", - ], -) -async def test_import_sources(hass, client, sources): - """Test import yaml config with sources list/csv.""" - assert client - - with patch("homeassistant.components.webostv.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: config_entries.SOURCE_IMPORT}, - data={ - **MOCK_YAML_CONFIG, - CONF_CUSTOMIZE: { - CONF_SOURCES: sources, - }, - }, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == TV_NAME - assert result["data"][CONF_HOST] == MOCK_YAML_CONFIG[CONF_HOST] - assert result["data"][CONF_CLIENT_SECRET] == MOCK_YAML_CONFIG[CONF_CLIENT_SECRET] - assert result["options"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] - assert result["result"].unique_id == MOCK_YAML_CONFIG[CONF_UNIQUE_ID] - - async def test_form(hass, client): """Test we get the form.""" assert client diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py deleted file mode 100644 index 8729576d869..00000000000 --- a/tests/components/webostv/test_init.py +++ /dev/null @@ -1,141 +0,0 @@ -"""The tests for the WebOS TV platform.""" - -from unittest.mock import Mock, mock_open, patch - -from aiowebostv import WebOsTvPairError - -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.webostv import DOMAIN - -from . import ( - create_memory_sqlite_engine, - is_entity_unique_id_updated, - setup_legacy_component, -) -from .const import MOCK_JSON - - -async def test_missing_keys_file_abort(hass, client, caplog): - """Test abort import when no pairing keys file.""" - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=False) - ): - await setup_legacy_component(hass) - - assert "No pairing keys, Not importing" in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_empty_json_abort(hass, client, caplog): - """Test abort import when keys file is empty.""" - m_open = mock_open(read_data="[]") - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass) - - assert "No pairing keys, Not importing" in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_valid_json_migrate_not_needed(hass, client, caplog): - """Test import from valid json entity already migrated or removed.""" - m_open = mock_open(read_data=MOCK_JSON) - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass, False) - - assert "Migrating webOS Smart TV entity" not in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_valid_json_missing_host_key(hass, client, caplog): - """Test import from valid json missing host key.""" - m_open = mock_open(read_data='{"1.2.3.5": "other-key"}') - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass) - - assert "Not importing webOS Smart TV host" in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_not_connected_import(hass, client, caplog, monkeypatch): - """Test import while device is not connected.""" - m_open = mock_open(read_data=MOCK_JSON) - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=OSError)) - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass) - - assert f"Please make sure webOS TV {MP_DOMAIN}.{DOMAIN}" in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_pair_error_import_abort(hass, client, caplog, monkeypatch): - """Test abort import if device is not paired.""" - m_open = mock_open(read_data=MOCK_JSON) - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass) - - assert f"Please make sure webOS TV {MP_DOMAIN}.{DOMAIN}" not in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_entity_removed_import_abort(hass, client_entity_removed, caplog): - """Test abort import if entity removed by user during import.""" - m_open = mock_open(read_data=MOCK_JSON) - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass) - - assert "Not updating webOSTV Smart TV entity" in caplog.text - assert not is_entity_unique_id_updated(hass) - - -async def test_json_import(hass, client, caplog, monkeypatch): - """Test import from json keys file.""" - m_open = mock_open(read_data=MOCK_JSON) - monkeypatch.setattr(client, "is_connected", Mock(return_value=True)) - monkeypatch.setattr(client, "connect", Mock(return_value=True)) - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True): - await setup_legacy_component(hass) - - assert "imported from YAML config" in caplog.text - assert is_entity_unique_id_updated(hass) - - -async def test_sqlite_import(hass, client, caplog, monkeypatch): - """Test import from sqlite keys file.""" - m_open = mock_open(read_data="will raise JSONDecodeError") - monkeypatch.setattr(client, "is_connected", Mock(return_value=True)) - monkeypatch.setattr(client, "connect", Mock(return_value=True)) - - with patch( - "homeassistant.components.webostv.os.path.isfile", Mock(return_value=True) - ), patch("homeassistant.components.webostv.open", m_open, create=True), patch( - "homeassistant.components.webostv.db.create_engine", - side_effect=create_memory_sqlite_engine, - ): - await setup_legacy_component(hass) - - assert "imported from YAML config" in caplog.text - assert is_entity_unique_id_updated(hass) From 273360075594fc767befef4bd49274bfdd426b81 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 1 Apr 2022 10:07:13 +0200 Subject: [PATCH 0062/1224] coerce number selector values to int (#69059) --- homeassistant/components/knx/config_flow.py | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index e7e8854a9fb..e45eb3a87a1 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -65,7 +65,10 @@ CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode _IA_SELECTOR = selector.selector({"text": {}}) _IP_SELECTOR = selector.selector({"text": {}}) -_PORT_SELECTOR = selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}) +_PORT_SELECTOR = vol.All( + selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}), + vol.Coerce(int), +) class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -246,8 +249,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) fields = { - vol.Required(CONF_KNX_SECURE_USER_ID, default=2): selector.selector( - {"number": {"min": 1, "max": 127, "mode": "box"}} + vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All( + selector.selector({"number": {"min": 1, "max": 127, "mode": "box"}}), + vol.Coerce(int), ), vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector( {"text": {"type": "password"}} @@ -421,14 +425,17 @@ class KNXOptionsFlowHandler(OptionsFlow): CONF_KNX_DEFAULT_RATE_LIMIT, ), ) - ] = selector.selector( - { - "number": { - "min": 1, - "max": CONF_MAX_RATE_LIMIT, - "mode": "box", + ] = vol.All( + selector.selector( + { + "number": { + "min": 1, + "max": CONF_MAX_RATE_LIMIT, + "mode": "box", + } } - } + ), + vol.Coerce(int), ) return self.async_show_form( From 72d0cef169102175f33b74b1ea61785a366eeff4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 10:30:53 +0200 Subject: [PATCH 0063/1224] Update watchdog to 2.1.7 (#68985) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index fb9a9ea5d63..918492c82e9 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.1.6"], + "requirements": ["watchdog==2.1.7"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6d5884d725f..a5073f20a09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2395,7 +2395,7 @@ wallbox==0.4.4 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.6 +watchdog==2.1.7 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0b0f7ffcf9..489df320a38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1545,7 +1545,7 @@ wakeonlan==2.0.1 wallbox==0.4.4 # homeassistant.components.folder_watcher -watchdog==2.1.6 +watchdog==2.1.7 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.15.1 From fbba318a180d660e07593ad57c764d1d754e7c22 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 1 Apr 2022 01:38:00 -0700 Subject: [PATCH 0064/1224] Invert number option (#68972) --- homeassistant/components/overkiz/number.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 40d04b9bf71..e6e64162dd8 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -28,6 +28,8 @@ class OverkizNumberDescriptionMixin: class OverkizNumberDescription(NumberEntityDescription, OverkizNumberDescriptionMixin): """Class to describe an Overkiz number.""" + inverted: bool = False + NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # Cover: My Position (0 - 100) @@ -76,6 +78,14 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ max_value=15, entity_category=EntityCategory.CONFIG, ), + # DimmerExteriorHeating (Somfy Terrace Heater) (0 - 100) + # Needs to be inverted since 100 = off, 0 = on + OverkizNumberDescription( + key=OverkizState.CORE_LEVEL, + icon="mdi:patio-heater", + command=OverkizCommand.SET_LEVEL, + inverted=True, + ), ] SUPPORTED_STATES = {description.key: description for description in NUMBER_DESCRIPTIONS} @@ -119,12 +129,18 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): def value(self) -> float | None: """Return the entity value to represent the entity state.""" if state := self.device.states.get(self.entity_description.key): + if self.entity_description.inverted: + return self._attr_max_value - cast(float, state.value) + return cast(float, state.value) return None async def async_set_value(self, value: float) -> None: """Set new value.""" + if self.entity_description.inverted: + value = self._attr_max_value - value + await self.executor.async_execute_command( self.entity_description.command, value ) From 480d9d63ee9e27b89d73039371895752e77be99e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 1 Apr 2022 10:40:29 +0200 Subject: [PATCH 0065/1224] LIFX device cleanup cleanup (#68937) --- homeassistant/components/lifx/light.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index a6dd643655f..323bec3f1de 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -204,9 +204,6 @@ async def async_setup_entry( lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities) hass.data[DATA_LIFX_MANAGER] = lifx_manager - # This is to clean up old litter. Can be removed in Home Assistant 2022.5. - await lifx_manager.remove_empty_devices() - for interface in interfaces: lifx_manager.start_discovery(interface) @@ -438,14 +435,15 @@ class LIFXManager: entity.registered = False entity.async_write_ha_state() - async def entity_registry_updated(self, event): + @callback + def entity_registry_updated(self, event): """Handle entity registry updated.""" if event.data["action"] == "remove": - await self.remove_empty_devices() + self.remove_empty_devices() - async def remove_empty_devices(self): + def remove_empty_devices(self): """Remove devices with no entities.""" - entity_reg = await er.async_get_registry(self.hass) + entity_reg = er.async_get(self.hass) device_reg = dr.async_get(self.hass) device_list = dr.async_entries_for_config_entry( device_reg, self.config_entry.entry_id @@ -456,7 +454,9 @@ class LIFXManager: device_entry.id, include_disabled_entities=True, ): - device_reg.async_remove_device(device_entry.id) + device_reg.async_update_device( + device_entry.id, remove_config_entry_id=self.config_entry.entry_id + ) class AwaitAioLIFX: From c22a08334c10f26f236f2538c2f9037941606bf5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Apr 2022 10:42:21 +0200 Subject: [PATCH 0066/1224] Bump voluptuous to 0.13.0 (#68897) * Bump voluptuous to v0.13.0 * Update DEPENDENCY_CONFLICTS * Update following python_awair bump * Revert "Update following python_awair bump" This reverts commit 089bd7b4498d170e06b72cc815b8e61c51f2f42a. * Revert "Update DEPENDENCY_CONFLICTS" This reverts commit ddd83212b8a0c21f01e00feafe181dc7b1866b0f. * Test fail-fast * Revert "Test fail-fast" This reverts commit 446e104a2463a7ad1ac1eb21ea04c21fff27f88c. --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8c7b86db1c3..02979392ade 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ scapy==2.4.5 sqlalchemy==1.4.32 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 -voluptuous==0.12.2 +voluptuous==0.13.0 yarl==1.7.2 zeroconf==0.38.4 diff --git a/requirements.txt b/requirements.txt index e91988d10cd..559c590fd5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,6 @@ python-slugify==4.0.1 pyyaml==6.0 requests==2.27.1 typing-extensions>=3.10.0.2,<5.0 -voluptuous==0.12.2 +voluptuous==0.13.0 voluptuous-serialize==2.5.0 yarl==1.7.2 diff --git a/setup.cfg b/setup.cfg index 6d99009e347..cf2a54ae83e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,7 @@ install_requires = pyyaml==6.0 requests==2.27.1 typing-extensions>=3.10.0.2,<5.0 - voluptuous==0.12.2 + voluptuous==0.13.0 voluptuous-serialize==2.5.0 yarl==1.7.2 From 93ce18806c52eccbceb1f7e49d2ce5056beb9c82 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 11:00:16 +0200 Subject: [PATCH 0067/1224] Update debugpy to 1.6.0 (#68989) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 041c2f9e31e..00e4839be63 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.5.1"], + "requirements": ["debugpy==1.6.0"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a5073f20a09..270e37f5e89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.5.1 +debugpy==1.6.0 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 489df320a38..870aebae0b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.5.1 +debugpy==1.6.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 617f459b57fa7bd700a45227ba7c2bfd103b0186 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 11:01:51 +0200 Subject: [PATCH 0068/1224] Bump pychromecast to 11.0.0 (#69057) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 85457307c94..1e933d5e10e 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.3.0"], + "requirements": ["pychromecast==11.0.0"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 270e37f5e89..50017009dd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1386,7 +1386,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.3.0 +pychromecast==11.0.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 870aebae0b2..78db847ddf4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -923,7 +923,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.3.0 +pychromecast==11.0.0 # homeassistant.components.climacell pyclimacell==0.18.2 From 2963aea3ecf15530187416aa73d7fc6519e21853 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 1 Apr 2022 10:08:00 +0100 Subject: [PATCH 0069/1224] Ignore old_state when using delta_values (#68402) * delta value updates don't require old_state * add test * merge --- .../components/utility_meter/sensor.py | 20 +++++++++----- tests/components/utility_meter/test_sensor.py | 27 +++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index c3d2be63a4b..92656fd0769 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -288,10 +288,15 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): sensor.start(source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) if ( - old_state is None - or new_state is None - or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + new_state is None or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + or ( + not self._sensor_delta_values + and ( + old_state is None + or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ) + ) ): return @@ -309,9 +314,12 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._state += adjustment except DecimalException as err: - _LOGGER.warning( - "Invalid state (%s > %s): %s", old_state.state, new_state.state, err - ) + if self._sensor_delta_values: + _LOGGER.warning("Invalid adjustment of %s: %s", new_state.state, err) + else: + _LOGGER.warning( + "Invalid state (%s > %s): %s", old_state.state, new_state.state, err + ) self.async_write_ha_state() @callback diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 1b8328f5a62..2ee2f0b1c74 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -587,7 +587,7 @@ async def test_net_consumption(hass, yaml_config, config_entry_config): ), ), ) -async def test_non_net_consumption(hass, yaml_config, config_entry_config): +async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplog): """Test utility sensor state.""" if yaml_config: assert await async_setup_component(hass, DOMAIN, yaml_config) @@ -621,6 +621,17 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config): ) await hass.async_block_till_done() + now = dt_util.utcnow() + timedelta(seconds=10) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set( + entity_id, + None, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + force_update=True, + ) + await hass.async_block_till_done() + assert "Invalid state " in caplog.text + state = hass.states.get("sensor.energy_bill") assert state is not None @@ -655,7 +666,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config): ), ), ) -async def test_delta_values(hass, yaml_config, config_entry_config): +async def test_delta_values(hass, yaml_config, config_entry_config, caplog): """Test utility meter "delta_values" mode.""" now = dt_util.utcnow() with alter_time(now): @@ -686,6 +697,18 @@ async def test_delta_values(hass, yaml_config, config_entry_config): state = hass.states.get("sensor.energy_bill") assert state.attributes.get("status") == PAUSED + now += timedelta(seconds=30) + with alter_time(now): + async_fire_time_changed(hass, now) + hass.states.async_set( + entity_id, + None, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + force_update=True, + ) + await hass.async_block_till_done() + assert "Invalid adjustment of None" in caplog.text + now += timedelta(seconds=30) with alter_time(now): async_fire_time_changed(hass, now) From a81194cdd77135600bb9f67549a5239ba75f766b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 1 Apr 2022 12:56:58 +0200 Subject: [PATCH 0070/1224] Add auto_update property to UpdateEntity (#69054) --- homeassistant/components/update/__init__.py | 17 +++++++- homeassistant/components/update/const.py | 1 + tests/components/update/test_init.py | 42 +++++++++++++++++++ .../custom_components/test/update.py | 13 ++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 0a78840e66e..6b30f45b023 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from .const import ( + ATTR_AUTO_UPDATE, ATTR_BACKUP, ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, @@ -93,7 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SKIP, {}, - UpdateEntity.async_skip.__name__, + async_skip, ) websocket_api.async_register_command(hass, websocket_release_notes) @@ -144,6 +145,13 @@ async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None await entity.async_install_with_progress(version, backup) +async def async_skip(entity: UpdateEntity, service_call: ServiceCall) -> None: + """Service call wrapper to validate the call.""" + if entity.auto_update: + raise HomeAssistantError(f"Skipping update is not supported for {entity.name}") + await entity.async_skip() + + @dataclass class UpdateEntityDescription(EntityDescription): """A class that describes update entities.""" @@ -156,6 +164,7 @@ class UpdateEntity(RestoreEntity): """Representation of an update entity.""" entity_description: UpdateEntityDescription + _attr_auto_update: bool = False _attr_current_version: str | None = None _attr_device_class: UpdateDeviceClass | str | None _attr_in_progress: bool | int = False @@ -168,6 +177,11 @@ class UpdateEntity(RestoreEntity): __skipped_version: str | None = None __in_progress: bool = False + @property + def auto_update(self) -> bool: + """Indicate if the device or service has auto update enabled.""" + return self._attr_auto_update + @property def current_version(self) -> str | None: """Version currently in use.""" @@ -329,6 +343,7 @@ class UpdateEntity(RestoreEntity): self.__skipped_version = None return { + ATTR_AUTO_UPDATE: self.auto_update, ATTR_CURRENT_VERSION: self.current_version, ATTR_IN_PROGRESS: in_progress, ATTR_LATEST_VERSION: self.latest_version, diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py index 7c70572f458..916d2cbaceb 100644 --- a/homeassistant/components/update/const.py +++ b/homeassistant/components/update/const.py @@ -20,6 +20,7 @@ class UpdateEntityFeature(IntEnum): SERVICE_INSTALL: Final = "install" SERVICE_SKIP: Final = "skip" +ATTR_AUTO_UPDATE: Final = "auto_update" ATTR_BACKUP: Final = "backup" ATTR_CURRENT_VERSION: Final = "current_version" ATTR_IN_PROGRESS: Final = "in_progress" diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index c6845f73d0c..b8db7f24f4a 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -16,6 +16,7 @@ from homeassistant.components.update import ( UpdateEntityDescription, ) from homeassistant.components.update.const import ( + ATTR_AUTO_UPDATE, ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, ATTR_LATEST_VERSION, @@ -65,6 +66,7 @@ async def test_update(hass: HomeAssistant) -> None: assert update.in_progress is False assert update.state == STATE_ON assert update.state_attributes == { + ATTR_AUTO_UPDATE: False, ATTR_CURRENT_VERSION: "1.0.0", ATTR_IN_PROGRESS: False, ATTR_LATEST_VERSION: "1.0.1", @@ -242,6 +244,46 @@ async def test_entity_with_no_updates( ) +async def test_entity_with_auto_update( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity that has auto update feature.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("update.update_with_auto_update") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] is None + + # Should be able to manually install an update even if it can auto update + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_with_auto_update"}, + blocking=True, + ) + + # Should not be to skip the update + with pytest.raises( + HomeAssistantError, + match="Skipping update is not supported for Update with auto update", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SKIP, + {ATTR_ENTITY_ID: "update.update_with_auto_update"}, + blocking=True, + ) + + async def test_entity_with_updates_available( hass: HomeAssistant, enable_custom_integrations: None, diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py index ce8cd3869f5..fc5ee31246e 100644 --- a/tests/testing_config/custom_components/test/update.py +++ b/tests/testing_config/custom_components/test/update.py @@ -20,6 +20,11 @@ _LOGGER = logging.getLogger(__name__) class MockUpdateEntity(MockEntity, UpdateEntity): """Mock UpdateEntity class.""" + @property + def auto_update(self) -> bool: + """Indicate if the device or service has auto update enabled.""" + return self._handle("auto_update") + @property def current_version(self) -> str | None: """Version currently in use.""" @@ -135,6 +140,14 @@ def init(empty=False): latest_version="1.0.1", supported_features=UpdateEntityFeature.RELEASE_NOTES, ), + MockUpdateEntity( + name="Update with auto update", + unique_id="with_auto_update", + current_version="1.0.0", + latest_version="1.0.1", + auto_update=True, + supported_features=UpdateEntityFeature.INSTALL, + ), ] ) From 220beefb89fb4b62e80412137398f13ea011fc41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Apr 2022 01:34:44 -1000 Subject: [PATCH 0071/1224] Prevent HomeKit from offering hidden entities (#69042) --- .../components/homekit/config_flow.py | 47 ++++++----- tests/components/homekit/test_config_flow.py | 78 +++++++++++++++++++ 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index a147c8dcb5d..79193cd3dac 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -466,7 +466,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entity_filter = self.hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - all_supported_entities = _async_get_matching_entities(self.hass, domains) + all_supported_entities = _async_get_matching_entities( + self.hass, domains, include_entity_category=True + ) # In accessory mode we can only have one default_value = next( iter( @@ -505,7 +507,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entity_filter = self.hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - all_supported_entities = _async_get_matching_entities(self.hass, domains) + all_supported_entities = _async_get_matching_entities( + self.hass, domains, include_entity_category=True + ) if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) # Strip out entities that no longer exist to prevent error in the UI @@ -559,21 +563,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): all_supported_entities = _async_get_matching_entities(self.hass, domains) if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) - ent_reg = entity_registry.async_get(self.hass) - excluded_entities = set() - for entity_id in all_supported_entities: - if ent_reg_ent := ent_reg.async_get(entity_id): - if ( - ent_reg_ent.entity_category is not None - or ent_reg_ent.hidden_by is not None - ): - excluded_entities.add(entity_id) - # Remove entity category entities since we will exclude them anyways - all_supported_entities = { - k: v - for k, v in all_supported_entities.items() - if k not in excluded_entities - } + # Strip out entities that no longer exist to prevent error in the UI default_value = [ entity_id for entity_id in entities if entity_id in all_supported_entities @@ -652,16 +642,37 @@ async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]: return dict(sorted(unsorted.items(), key=lambda item: item[1])) +def _exclude_by_entity_registry( + ent_reg: entity_registry.EntityRegistry, + entity_id: str, + include_entity_category: bool, +) -> bool: + """Filter out hidden entities and ones with entity category (unless specified).""" + return bool( + (entry := ent_reg.async_get(entity_id)) + and ( + entry.hidden_by is not None + or (not include_entity_category or entry.entity_category is not None) + ) + ) + + def _async_get_matching_entities( - hass: HomeAssistant, domains: list[str] | None = None + hass: HomeAssistant, + domains: list[str] | None = None, + include_entity_category: bool = False, ) -> dict[str, str]: """Fetch all entities or entities in the given domains.""" + ent_reg = entity_registry.async_get(hass) return { state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})" for state in sorted( hass.states.async_all(domains and set(domains)), key=lambda item: item.entity_id, ) + if not _exclude_by_entity_registry( + ent_reg, state.entity_id, include_entity_category + ) } diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 301040e4f88..fc3ef3e2710 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1481,3 +1481,81 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( "include_entities": [], }, } + + +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_include_mode_skips_hidden_entities( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg +): + """Ensure include mode does not offer hidden entities.""" + config_entry = _mock_config_entry_with_options_populated() + await async_init_entry(hass, config_entry) + + hass.states.async_set("media_player.tv", "off") + hass.states.async_set("media_player.sonos", "off") + hass.states.async_set("switch.other", "off") + + sonos_hidden_switch: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "sonos", + "config", + device_id="1234", + hidden_by=RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(sonos_hidden_switch.entity_id, "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "mode": "bridge", + "include_exclude_mode": "exclude", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["media_player", "switch"], + "mode": "bridge", + "include_exclude_mode": "include", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "include" + assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + + # sonos_hidden_switch.entity_id is a hidden entity + # so it should not be selectable since it will always be excluded + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": [sonos_hidden_switch.entity_id]}, + ) + + result4 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": ["media_player.tv", "switch.other"]}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["media_player.tv", "switch.other"], + }, + } From 165e79be8ffae3cac1c6669516212e74e17bf1b3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 1 Apr 2022 06:39:28 -0500 Subject: [PATCH 0072/1224] Update rokuecp to 0.16.0 (#68822) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 433ce6b29d1..3f63a7039c1 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.15.0"], + "requirements": ["rokuecp==0.16.0"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 50017009dd5..9c984afc53b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2063,7 +2063,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.15.0 +rokuecp==0.16.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78db847ddf4..28ca4ba5d09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1333,7 +1333,7 @@ rflink==0.0.62 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.15.0 +rokuecp==0.16.0 # homeassistant.components.roomba roombapy==1.6.5 From be7fc35dfa5c9bcf7d4ec67927ec53f641750c3e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 13:54:03 +0200 Subject: [PATCH 0073/1224] Add EntityFeature enum to Alarm Control Panel (#69044) --- .../alarm_control_panel/__init__.py | 17 +++--- .../components/alarm_control_panel/const.py | 15 ++++++ .../components/manual/alarm_control_panel.py | 19 +++---- .../alarm_control_panel/test_device_action.py | 53 +++++++++++++++---- .../test_device_condition.py | 40 ++++++++++---- 5 files changed, 105 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 082327fdec8..d27091ad535 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -27,13 +27,14 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -from .const import ( +from .const import ( # noqa: F401 SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_CUSTOM_BYPASS, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, SUPPORT_ALARM_ARM_VACATION, SUPPORT_ALARM_TRIGGER, + AlarmControlPanelEntityFeature, ) _LOGGER: Final = logging.getLogger(__name__) @@ -58,7 +59,7 @@ PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" component = hass.data[DOMAIN] = EntityComponent( - logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -70,37 +71,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home", - [SUPPORT_ALARM_ARM_HOME], + [AlarmControlPanelEntityFeature.ARM_HOME], ) component.async_register_entity_service( SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away", - [SUPPORT_ALARM_ARM_AWAY], + [AlarmControlPanelEntityFeature.ARM_AWAY], ) component.async_register_entity_service( SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night", - [SUPPORT_ALARM_ARM_NIGHT], + [AlarmControlPanelEntityFeature.ARM_NIGHT], ) component.async_register_entity_service( SERVICE_ALARM_ARM_VACATION, ALARM_SERVICE_SCHEMA, "async_alarm_arm_vacation", - [SUPPORT_ALARM_ARM_VACATION], + [AlarmControlPanelEntityFeature.ARM_VACATION], ) component.async_register_entity_service( SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, "async_alarm_arm_custom_bypass", - [SUPPORT_ALARM_ARM_CUSTOM_BYPASS], + [AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS], ) component.async_register_entity_service( SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger", - [SUPPORT_ALARM_TRIGGER], + [AlarmControlPanelEntityFeature.TRIGGER], ) return True diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index f3688a27958..c3ed914126e 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -1,7 +1,22 @@ """Provides the constants needed for component.""" +from enum import IntEnum from typing import Final + +class AlarmControlPanelEntityFeature(IntEnum): + """Supported features of the alarm control panel entity.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +# These constants are deprecated as of Home Assistant 2022.5 +# Pleease use the AlarmControlPanelEntityFeature enum instead. SUPPORT_ALARM_ARM_HOME: Final = 1 SUPPORT_ALARM_ARM_AWAY: Final = 2 SUPPORT_ALARM_ARM_NIGHT: Final = 4 diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 8f86f3cdbdf..b9ac0bdcf5c 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -10,12 +10,7 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_CUSTOM_BYPASS, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, - SUPPORT_ALARM_ARM_VACATION, - SUPPORT_ALARM_TRIGGER, + AlarmControlPanelEntityFeature, ) from homeassistant.const import ( CONF_ARMING_TIME, @@ -262,12 +257,12 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): def supported_features(self) -> int: """Return the list of supported features.""" return ( - SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY - | SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_ARM_VACATION - | SUPPORT_ALARM_TRIGGER - | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.TRIGGER + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS ) @property diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 5cbe9f256ba..52e60967131 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -44,16 +44,51 @@ def entity_reg(hass): "set_state,features_reg,features_state,expected_action_types", [ (False, 0, 0, ["disarm"]), - (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["disarm", "arm_away"]), - (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["disarm", "arm_home"]), - (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["disarm", "arm_night"]), - (False, const.SUPPORT_ALARM_TRIGGER, 0, ["disarm", "trigger"]), + ( + False, + const.AlarmControlPanelEntityFeature.ARM_AWAY, + 0, + ["disarm", "arm_away"], + ), + ( + False, + const.AlarmControlPanelEntityFeature.ARM_HOME, + 0, + ["disarm", "arm_home"], + ), + ( + False, + const.AlarmControlPanelEntityFeature.ARM_NIGHT, + 0, + ["disarm", "arm_night"], + ), + (False, const.AlarmControlPanelEntityFeature.TRIGGER, 0, ["disarm", "trigger"]), (True, 0, 0, ["disarm"]), - (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["disarm", "arm_away"]), - (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["disarm", "arm_home"]), - (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["disarm", "arm_night"]), - (True, 0, const.SUPPORT_ALARM_ARM_VACATION, ["disarm", "arm_vacation"]), - (True, 0, const.SUPPORT_ALARM_TRIGGER, ["disarm", "trigger"]), + ( + True, + 0, + const.AlarmControlPanelEntityFeature.ARM_AWAY, + ["disarm", "arm_away"], + ), + ( + True, + 0, + const.AlarmControlPanelEntityFeature.ARM_HOME, + ["disarm", "arm_home"], + ), + ( + True, + 0, + const.AlarmControlPanelEntityFeature.ARM_NIGHT, + ["disarm", "arm_night"], + ), + ( + True, + 0, + const.AlarmControlPanelEntityFeature.ARM_VACATION, + ["disarm", "arm_vacation"], + ), + (True, 0, const.AlarmControlPanelEntityFeature.TRIGGER, ["disarm", "trigger"]), ], ) async def test_get_actions( diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index b44e6ba7e8b..83f9ff79dca 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -49,17 +49,37 @@ def calls(hass): "set_state,features_reg,features_state,expected_condition_types", [ (False, 0, 0, []), - (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["is_armed_away"]), - (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["is_armed_home"]), - (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["is_armed_night"]), - (False, const.SUPPORT_ALARM_ARM_VACATION, 0, ["is_armed_vacation"]), - (False, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, 0, ["is_armed_custom_bypass"]), + (False, const.AlarmControlPanelEntityFeature.ARM_AWAY, 0, ["is_armed_away"]), + (False, const.AlarmControlPanelEntityFeature.ARM_HOME, 0, ["is_armed_home"]), + (False, const.AlarmControlPanelEntityFeature.ARM_NIGHT, 0, ["is_armed_night"]), + ( + False, + const.AlarmControlPanelEntityFeature.ARM_VACATION, + 0, + ["is_armed_vacation"], + ), + ( + False, + const.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + 0, + ["is_armed_custom_bypass"], + ), (True, 0, 0, []), - (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["is_armed_away"]), - (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["is_armed_home"]), - (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["is_armed_night"]), - (True, 0, const.SUPPORT_ALARM_ARM_VACATION, ["is_armed_vacation"]), - (True, 0, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, ["is_armed_custom_bypass"]), + (True, 0, const.AlarmControlPanelEntityFeature.ARM_AWAY, ["is_armed_away"]), + (True, 0, const.AlarmControlPanelEntityFeature.ARM_HOME, ["is_armed_home"]), + (True, 0, const.AlarmControlPanelEntityFeature.ARM_NIGHT, ["is_armed_night"]), + ( + True, + 0, + const.AlarmControlPanelEntityFeature.ARM_VACATION, + ["is_armed_vacation"], + ), + ( + True, + 0, + const.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + ["is_armed_custom_bypass"], + ), ], ) async def test_get_conditions( From 044d71f0164a867605225000a9e18d7b7091476d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 14:18:08 +0200 Subject: [PATCH 0074/1224] Add color mode support to zengge light (#55260) --- homeassistant/components/zengge/light.py | 39 ++++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 0a4392ff855..057c643f409 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -9,11 +9,10 @@ from zengge import zengge from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, - ATTR_WHITE_VALUE, + ATTR_WHITE, + COLOR_MODE_HS, + COLOR_MODE_WHITE, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.const import CONF_DEVICES, CONF_NAME @@ -25,8 +24,6 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -SUPPORT_ZENGGE_LED = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE - DEVICE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -103,20 +100,27 @@ class ZenggeLight(LightEntity): return self._white @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_ZENGGE_LED + def color_mode(self): + """Return the current color mode.""" + if self._white != 0: + return COLOR_MODE_WHITE + return COLOR_MODE_HS + + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return {COLOR_MODE_HS, COLOR_MODE_WHITE} @property def assumed_state(self): """We can report the actual state.""" return False - def set_rgb(self, red, green, blue): + def _set_rgb(self, red, green, blue): """Set the rgb state.""" return self._bulb.set_rgb(red, green, blue) - def set_white(self, white): + def _set_white(self, white): """Set the white state.""" return self._bulb.set_white(white) @@ -126,28 +130,29 @@ class ZenggeLight(LightEntity): self._bulb.on() hs_color = kwargs.get(ATTR_HS_COLOR) - white = kwargs.get(ATTR_WHITE_VALUE) + white = kwargs.get(ATTR_WHITE) brightness = kwargs.get(ATTR_BRIGHTNESS) if white is not None: - self._white = white + # Change the bulb to white + self._brightness = self._white = white self._hs_color = (0, 0) if hs_color is not None: + # Change the bulb to hs self._white = 0 self._hs_color = hs_color if brightness is not None: - self._white = 0 self._brightness = brightness if self._white != 0: - self.set_white(self._white) + self._set_white(self._brightness) else: rgb = color_util.color_hsv_to_RGB( self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 ) - self.set_rgb(*rgb) + self._set_rgb(*rgb) def turn_off(self, **kwargs): """Turn the specified light off.""" @@ -161,4 +166,6 @@ class ZenggeLight(LightEntity): self._hs_color = hsv[:2] self._brightness = (hsv[2] / 100) * 255 self._white = self._bulb.get_white() + if self._white: + self._brightness = self._white self._state = self._bulb.get_on() From c01637130b5307f6baee008bc6111a8a1d3bdf38 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 14:38:34 +0200 Subject: [PATCH 0075/1224] Update zengge codeowners (#69068) --- CODEOWNERS | 1 + homeassistant/components/zengge/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8f0a672fa51..aac6f1986ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1185,6 +1185,7 @@ build.json @home-assistant/supervisor /homeassistant/components/yi/ @bachya /homeassistant/components/youless/ @gjong /tests/components/youless/ @gjong +/homeassistant/components/zengge/ @emontnemery /homeassistant/components/zeroconf/ @bdraco /tests/components/zeroconf/ @bdraco /homeassistant/components/zerproc/ @emlove diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index 98f2ab1de21..cc04876287d 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -3,7 +3,7 @@ "name": "Zengge", "documentation": "https://www.home-assistant.io/integrations/zengge", "requirements": ["zengge==0.2"], - "codeowners": [], + "codeowners": ["@emontnemery"], "iot_class": "local_polling", "loggers": ["zengge"] } From 2c3d9566cb5c51a9c572a5f55fe729165081cde3 Mon Sep 17 00:00:00 2001 From: Billy Stevenson Date: Fri, 1 Apr 2022 14:11:37 +0100 Subject: [PATCH 0076/1224] Add Meater integration (#44929) Co-authored-by: Alexei Chetroi Co-authored-by: Brian Rogers Co-authored-by: Franck Nijhof Co-authored-by: Erik --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/meater/__init__.py | 89 ++++++++++++++ .../components/meater/config_flow.py | 57 +++++++++ homeassistant/components/meater/const.py | 3 + homeassistant/components/meater/manifest.json | 9 ++ homeassistant/components/meater/sensor.py | 112 ++++++++++++++++++ homeassistant/components/meater/strings.json | 18 +++ .../components/meater/translations/en.json | 18 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/meater/__init__.py | 1 + tests/components/meater/test_config_flow.py | 108 +++++++++++++++++ 14 files changed, 427 insertions(+) create mode 100644 homeassistant/components/meater/__init__.py create mode 100644 homeassistant/components/meater/config_flow.py create mode 100644 homeassistant/components/meater/const.py create mode 100644 homeassistant/components/meater/manifest.json create mode 100644 homeassistant/components/meater/sensor.py create mode 100644 homeassistant/components/meater/strings.json create mode 100644 homeassistant/components/meater/translations/en.json create mode 100644 tests/components/meater/__init__.py create mode 100644 tests/components/meater/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 73814938619..0d53cb2ea22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -678,6 +678,9 @@ omit = homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/* + homeassistant/components/meater/__init__.py + homeassistant/components/meater/const.py + homeassistant/components/meater/sensor.py homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py homeassistant/components/melcloud/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index aac6f1986ea..1b9c9bfa69d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -588,6 +588,8 @@ build.json @home-assistant/supervisor /homeassistant/components/matrix/ @tinloaf /homeassistant/components/mazda/ @bdr99 /tests/components/mazda/ @bdr99 +/homeassistant/components/meater/ @Sotolotl +/tests/components/meater/ @Sotolotl /homeassistant/components/media_player/ @home-assistant/core /tests/components/media_player/ @home-assistant/core /homeassistant/components/media_source/ @hunterjm diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py new file mode 100644 index 00000000000..7210e74d45f --- /dev/null +++ b/homeassistant/components/meater/__init__.py @@ -0,0 +1,89 @@ +"""The Meater Temperature Probe integration.""" +from datetime import timedelta +import logging + +import async_timeout +from meater import ( + AuthenticationError, + MeaterApi, + ServiceUnavailableError, + TooManyRequestsError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Meater Temperature Probe from a config entry.""" + # Store an API object to access + session = async_get_clientsession(hass) + meater_api = MeaterApi(session) + + # Add the credentials + try: + _LOGGER.debug("Authenticating with the Meater API") + await meater_api.authenticate( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) + except (ServiceUnavailableError, TooManyRequestsError) as err: + raise ConfigEntryNotReady from err + except AuthenticationError as err: + _LOGGER.error("Unable to authenticate with the Meater API: %s", err) + return False + + async def async_update_data(): + """Fetch data from API endpoint.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + devices = await meater_api.get_all_devices() + except AuthenticationError as err: + raise UpdateFailed("The API call wasn't authenticated") from err + except TooManyRequestsError as err: + raise UpdateFailed( + "Too many requests have been made to the API, rate limiting is in place" + ) from err + + return devices + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="meater_api", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=30), + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault("known_probes", set()) + + hass.data[DOMAIN][entry.entry_id] = { + "api": meater_api, + "coordinator": coordinator, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/meater/config_flow.py b/homeassistant/components/meater/config_flow.py new file mode 100644 index 00000000000..1b1a8a0eca4 --- /dev/null +++ b/homeassistant/components/meater/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for Meater.""" +from meater import AuthenticationError, MeaterApi, ServiceUnavailableError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN + +FLOW_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Meater Config Flow.""" + + async def async_step_user(self, user_input=None): + """Define the login user step.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=FLOW_SCHEMA, + ) + + username: str = user_input[CONF_USERNAME] + await self.async_set_unique_id(username.lower()) + self._abort_if_unique_id_configured() + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + session = aiohttp_client.async_get_clientsession(self.hass) + + api = MeaterApi(session) + errors = {} + + try: + await api.authenticate(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + except AuthenticationError: + errors["base"] = "invalid_auth" + except ServiceUnavailableError: + errors["base"] = "service_unavailable_error" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown_auth_error" + else: + return self.async_create_entry( + title="Meater", + data={"username": username, "password": password}, + ) + + return self.async_show_form( + step_id="user", + data_schema=FLOW_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/meater/const.py b/homeassistant/components/meater/const.py new file mode 100644 index 00000000000..6b40aa18d59 --- /dev/null +++ b/homeassistant/components/meater/const.py @@ -0,0 +1,3 @@ +"""Constants for the Meater Temperature Probe integration.""" + +DOMAIN = "meater" diff --git a/homeassistant/components/meater/manifest.json b/homeassistant/components/meater/manifest.json new file mode 100644 index 00000000000..192a534cd75 --- /dev/null +++ b/homeassistant/components/meater/manifest.json @@ -0,0 +1,9 @@ +{ + "codeowners": ["@Sotolotl"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/meater", + "domain": "meater", + "iot_class": "cloud_polling", + "name": "Meater", + "requirements": ["meater-python==0.0.8"] +} diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py new file mode 100644 index 00000000000..9fb5176aa81 --- /dev/null +++ b/homeassistant/components/meater/sensor.py @@ -0,0 +1,112 @@ +"""The Meater Temperature Probe integration.""" +from enum import Enum + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the entry.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + "coordinator" + ] + + @callback + def async_update_data(): + """Handle updated data from the API endpoint.""" + if not coordinator.last_update_success: + return + + devices = coordinator.data + entities = [] + known_probes: set = hass.data[DOMAIN]["known_probes"] + + # Add entities for temperature probes which we've not yet seen + for dev in devices: + if dev.id in known_probes: + continue + + entities.append( + MeaterProbeTemperature( + coordinator, dev.id, TemperatureMeasurement.Internal + ) + ) + entities.append( + MeaterProbeTemperature( + coordinator, dev.id, TemperatureMeasurement.Ambient + ) + ) + known_probes.add(dev.id) + + async_add_entities(entities) + + return devices + + # Add a subscriber to the coordinator to discover new temperature probes + coordinator.async_add_listener(async_update_data) + + +class MeaterProbeTemperature(SensorEntity, CoordinatorEntity): + """Meater Temperature Sensor Entity.""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = TEMP_CELSIUS + + def __init__(self, coordinator, device_id, temperature_reading_type): + """Initialise the sensor.""" + super().__init__(coordinator) + self._attr_name = f"Meater Probe {temperature_reading_type.name}" + self._attr_device_info = { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, device_id) + }, + "manufacturer": "Apption Labs", + "model": "Meater Probe", + "name": f"Meater Probe {device_id}", + } + self._attr_unique_id = f"{device_id}-{temperature_reading_type}" + + self.device_id = device_id + self.temperature_reading_type = temperature_reading_type + + @property + def native_value(self): + """Return the temperature of the probe.""" + # First find the right probe in the collection + device = None + + for dev in self.coordinator.data: + if dev.id == self.device_id: + device = dev + + if device is None: + return None + + if TemperatureMeasurement.Internal == self.temperature_reading_type: + return device.internal_temperature + + # Not an internal temperature, must be ambient + return device.ambient_temperature + + @property + def available(self): + """Return if entity is available.""" + # See if the device was returned from the API. If not, it's offline + return self.coordinator.last_update_success and any( + self.device_id == device.id for device in self.coordinator.data + ) + + +class TemperatureMeasurement(Enum): + """Enumeration of possible temperature readings from the probe.""" + + Internal = 1 + Ambient = 2 diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json new file mode 100644 index 00000000000..772e6afd080 --- /dev/null +++ b/homeassistant/components/meater/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up your Meater Cloud account.", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown_auth_error": "[%key:common::config_flow::error::unknown%]", + "service_unavailable_error": "The API is currently unavailable, please try again later." + } + } +} diff --git a/homeassistant/components/meater/translations/en.json b/homeassistant/components/meater/translations/en.json new file mode 100644 index 00000000000..3ceb94bcef0 --- /dev/null +++ b/homeassistant/components/meater/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Invalid authentication", + "service_unavailable_error": "The API is currently unavailable, please try again later.", + "unknown_auth_error": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Set up your Meater Cloud account." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 998e88000bf..a7eced345b4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -196,6 +196,7 @@ FLOWS = { "lyric", "mailgun", "mazda", + "meater", "melcloud", "met", "met_eireann", diff --git a/requirements_all.txt b/requirements_all.txt index 9c984afc53b..aef5fcf0fca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -982,6 +982,9 @@ mbddns==0.1.2 # homeassistant.components.minecraft_server mcstatus==6.0.0 +# homeassistant.components.meater +meater-python==0.0.8 + # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28ca4ba5d09..da78199a073 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -660,6 +660,9 @@ mbddns==0.1.2 # homeassistant.components.minecraft_server mcstatus==6.0.0 +# homeassistant.components.meater +meater-python==0.0.8 + # homeassistant.components.meteo_france meteofrance-api==1.0.2 diff --git a/tests/components/meater/__init__.py b/tests/components/meater/__init__.py new file mode 100644 index 00000000000..ef96dafe88c --- /dev/null +++ b/tests/components/meater/__init__.py @@ -0,0 +1 @@ +"""Tests for the Meater integration.""" diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py new file mode 100644 index 00000000000..597b72c354a --- /dev/null +++ b/tests/components/meater/test_config_flow.py @@ -0,0 +1,108 @@ +"""Define tests for the Meater config flow.""" +from unittest.mock import AsyncMock, patch + +from meater import AuthenticationError, ServiceUnavailableError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.meater import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_client(): + """Define a fixture for authentication coroutine.""" + return AsyncMock(return_value=None) + + +@pytest.fixture +def mock_meater(mock_client): + """Mock the meater library.""" + with patch("homeassistant.components.meater.MeaterApi.authenticate") as mock_: + mock_.side_effect = mock_client + yield mock_ + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} + + MockConfigEntry(domain=DOMAIN, unique_id="user@host.com", data=conf).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("mock_client", [AsyncMock(side_effect=Exception)]) +async def test_unknown_auth_error(hass, mock_meater): + """Test that an invalid API/App Key throws an error.""" + conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["errors"] == {"base": "unknown_auth_error"} + + +@pytest.mark.parametrize("mock_client", [AsyncMock(side_effect=AuthenticationError)]) +async def test_invalid_credentials(hass, mock_meater): + """Test that an invalid API/App Key throws an error.""" + conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize( + "mock_client", [AsyncMock(side_effect=ServiceUnavailableError)] +) +async def test_service_unavailable(hass, mock_meater): + """Test that an invalid API/App Key throws an error.""" + conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["errors"] == {"base": "service_unavailable_error"} + + +async def test_user_flow(hass, mock_meater): + """Test that the user flow works.""" + conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.meater.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(result["flow_id"], conf) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "password123", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "password123", + } From cbd3d7c0373a1820d57935a24474e765b2f6d67e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 15:58:08 +0200 Subject: [PATCH 0077/1224] Add bluepy as a dependency for zengge (#69067) Co-authored-by: Franck Nijhof --- homeassistant/components/zengge/manifest.json | 2 +- requirements_all.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index cc04876287d..79ae849dd36 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -2,7 +2,7 @@ "domain": "zengge", "name": "Zengge", "documentation": "https://www.home-assistant.io/integrations/zengge", - "requirements": ["zengge==0.2"], + "requirements": ["bluepy==1.3.0", "zengge==0.2"], "codeowners": ["@emontnemery"], "iot_class": "local_polling", "loggers": ["zengge"] diff --git a/requirements_all.txt b/requirements_all.txt index aef5fcf0fca..655031a667e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,6 +407,7 @@ blockchain==1.4.4 # homeassistant.components.decora # homeassistant.components.miflora +# homeassistant.components.zengge # bluepy==1.3.0 # homeassistant.components.bond From 2c82befc78297267289fd8d5e9abff20df8d8d04 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 1 Apr 2022 07:28:29 -0700 Subject: [PATCH 0078/1224] Enable select platform in Overkiz integration (#68995) --- homeassistant/components/overkiz/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 119f7a32262..8488103a238 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -28,6 +28,7 @@ PLATFORMS: list[Platform] = [ Platform.LOCK, Platform.NUMBER, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, From 9b21a48048adb753e2085bdf7d33d4cb735607fb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 17:11:31 +0200 Subject: [PATCH 0079/1224] Mend incorrectly imported MQTT config entries (#68987) Co-authored-by: Paulus Schoutsen --- homeassistant/components/mqtt/__init__.py | 74 +++++++++++++++++++---- tests/components/mqtt/test_init.py | 53 +++++++++++++++- 2 files changed, 114 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 22d99d1b7be..7c16da7f2aa 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -131,12 +131,15 @@ DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_TLS_PROTOCOL = "auto" DEFAULT_VALUES = { - CONF_PORT: DEFAULT_PORT, - CONF_WILL_MESSAGE: DEFAULT_WILL, CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, } +MANDATORY_DEFAULT_VALUES = (CONF_PORT,) + ATTR_TOPIC_TEMPLATE = "topic_template" ATTR_PAYLOAD_TEMPLATE = "payload_template" @@ -203,9 +206,7 @@ CONFIG_SCHEMA_BASE = vol.Schema( CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG ): cv.isfile, vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): vol.Any( - "auto", "1.0", "1.1", "1.2" - ), + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) ), @@ -220,6 +221,17 @@ CONFIG_SCHEMA_BASE = vol.Schema( } ) +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -602,6 +614,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DATA_MQTT_CONFIG] = conf if not bool(hass.config_entries.async_entries(DOMAIN)): + # Create an import flow if the user has yaml configured entities etc. + # but no broker configuration. Note: The intention is not for this to + # import broker configuration from YAML because that has been deprecated. hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, @@ -612,18 +627,53 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _merge_config(entry, conf): - """Merge configuration.yaml config with config entry.""" - # Base config on default values +def _merge_basic_config( + hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any] +) -> None: + """Merge basic options in configuration.yaml config with config entry. + + This mends incomplete migration from old version of HA Core. + """ + + entry_updated = False + entry_config = {**entry.data} + for key in DEPRECATED_CONFIG_KEYS: + if key in yaml_config and key not in entry_config: + entry_config[key] = yaml_config[key] + entry_updated = True + + for key in MANDATORY_DEFAULT_VALUES: + if key not in entry_config: + entry_config[key] = DEFAULT_VALUES[key] + entry_updated = True + + if entry_updated: + hass.config_entries.async_update_entry(entry, data=entry_config) + + +def _merge_extended_config(entry, conf): + """Merge advanced options in configuration.yaml config with config entry.""" + # Add default values conf = {**DEFAULT_VALUES, **conf} return {**conf, **entry.data} async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - # If user didn't have configuration.yaml config, generate defaults + # Merge basic configuration, and add missing defaults for basic options + _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) + + # Bail out if broker setting is missing + if CONF_BROKER not in entry.data: + _LOGGER.error("MQTT broker is not configured, please configure it") + return False + + # If user doesn't have configuration.yaml config, generate default values + # for options not in config entry data if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) + + # User has configuration.yaml config, warn about config entry overrides elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() override = {k: entry.data[k] for k in shared_keys} @@ -635,8 +685,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: override, ) - # Merge the configuration values from configuration.yaml - conf = _merge_config(entry, conf) + # Merge advanced configuration values from configuration.yaml + conf = _merge_extended_config(entry, conf) hass.data[DATA_MQTT] = MQTT( hass, @@ -870,7 +920,7 @@ class MQTT: if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - self.conf = _merge_config(entry, conf) + self.conf = _merge_extended_config(entry, conf) await self.async_disconnect() self.init_client() await self.async_connect() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 87eddd4421f..03874ed331e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -23,7 +23,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.core as ha -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, template from homeassistant.helpers.entity import Entity @@ -1620,6 +1620,57 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo assert device_entry is not None +async def test_update_incomplete_entry( + hass: HomeAssistant, device_reg, mqtt_client_mock, caplog +): + """Test if the MQTT component loads when config entry data is incomplete.""" + data = ( + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/sensor",' + ' "unique_id": "unique" }' + ) + + # Config entry data is incomplete + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={"port": 1234}) + entry.add_to_hass(hass) + # Mqtt present in yaml config + config = {"broker": "yaml_broker"} + await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + + # Config entry data should now be updated + assert entry.data == { + "port": 1234, + "broker": "yaml_broker", + } + # Warnings about broker deprecated, but not about other keys with default values + assert ( + "The 'broker' option is deprecated, please remove it from your configuration" + in caplog.text + ) + assert ( + "Deprecated configuration settings found in configuration.yaml. These settings " + "from your configuration entry will override: {'broker': 'yaml_broker'}" + in caplog.text + ) + + # Discover a device to verify the entry was setup correctly + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + assert device_entry is not None + + +async def test_fail_no_broker(hass, device_reg, mqtt_client_mock, caplog): + """Test if the MQTT component loads when broker configuration is missing.""" + # Config entry data is incomplete + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={}) + entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(entry.entry_id) + assert "MQTT broker is not configured, please configure it" in caplog.text + + @pytest.mark.no_fail_on_log_exception async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): """Test exception raised by message handler.""" From 87100c2517af268a9fb738befe29da25e6f256bb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 17:22:19 +0200 Subject: [PATCH 0080/1224] Drop deprecated support for unit_of_measurement from sensor (#69061) --- homeassistant/components/sensor/__init__.py | 29 --------------------- tests/components/sensor/test_init.py | 11 +------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index b9cb3d94796..56d4ae884fe 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -5,7 +5,6 @@ from collections.abc import Callable, Mapping from contextlib import suppress from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone -import inspect import logging from math import floor, log10 from typing import Any, Final, cast, final @@ -253,27 +252,6 @@ class SensorEntityDescription(EntityDescription): state_class: SensorStateClass | str | None = None unit_of_measurement: None = None # Type override, use native_unit_of_measurement - def __post_init__(self) -> None: - """Post initialisation processing.""" - if self.unit_of_measurement: - caller = inspect.stack()[2] # type: ignore[unreachable] - module = inspect.getmodule(caller[0]) - if "custom_components" in module.__file__: - report_issue = "report it to the custom component author." - else: - report_issue = ( - "create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - _LOGGER.warning( - "%s is setting 'unit_of_measurement' on an instance of " - "SensorEntityDescription, this is not valid and will be unsupported " - "from Home Assistant 2021.11. Please %s", - module.__name__, - report_issue, - ) - self.native_unit_of_measurement = self.unit_of_measurement - class SensorEntity(Entity): """Base class for sensor entities.""" @@ -387,13 +365,6 @@ class SensorEntity(Entity): if self._sensor_option_unit_of_measurement: return self._sensor_option_unit_of_measurement - # Support for _attr_unit_of_measurement will be removed in Home Assistant 2021.11 - if ( - hasattr(self, "_attr_unit_of_measurement") - and self._attr_unit_of_measurement is not None - ): - return self._attr_unit_of_measurement # type: ignore[unreachable] - native_unit_of_measurement = self.native_unit_of_measurement if native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT): diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 950c737375b..544e85b04ef 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -4,7 +4,7 @@ from datetime import date, datetime, timezone import pytest from pytest import approx -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PRESSURE_HPA, @@ -116,15 +116,6 @@ async def test_deprecated_last_reset( assert "last_reset" not in state.attributes -async def test_deprecated_unit_of_measurement(hass, caplog, enable_custom_integrations): - """Test warning on deprecated unit_of_measurement.""" - SensorEntityDescription("catsensor", unit_of_measurement="cats") - assert ( - "tests.components.sensor.test_init is setting 'unit_of_measurement' on an " - "instance of SensorEntityDescription" - ) in caplog.text - - async def test_datetime_conversion(hass, caplog, enable_custom_integrations): """Test conversion of datetime.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc) From bda997efe954263c8032d4cf8f9a5ff43f527e7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 17:28:50 +0200 Subject: [PATCH 0081/1224] Fix utility_meter startup (#69064) --- homeassistant/components/utility_meter/sensor.py | 10 ++++++---- tests/components/utility_meter/test_sensor.py | 8 +++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 92656fd0769..8df009a2a04 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_NAME, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, - EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -33,6 +32,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.template import is_number from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -426,6 +426,10 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): ) tariff_entity_state = self.hass.states.get(self._tariff_entity) + if not tariff_entity_state: + # The utility meter is not yet added + return + self._change_status(tariff_entity_state.state) return @@ -439,9 +443,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self.hass, [self._sensor_source_id], self.async_reading ) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_source_tracking - ) + async_at_start(self.hass, async_source_tracking) @property def name(self): diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 2ee2f0b1c74..63dc0523e6d 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -37,7 +37,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import State +from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -418,6 +418,9 @@ async def test_device_class(hass, yaml_config, config_entry_configs): ) async def test_restore_state(hass, yaml_config, config_entry_config): """Test utility sensor restore state.""" + # Home assistant is not runnit yet + hass.state = CoreState.not_running + last_reset = "2020-12-21T00:00:00.013073+00:00" mock_restore_cache( hass, @@ -668,6 +671,9 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo ) async def test_delta_values(hass, yaml_config, config_entry_config, caplog): """Test utility meter "delta_values" mode.""" + # Home assistant is not runnit yet + hass.state = CoreState.not_running + now = dt_util.utcnow() with alter_time(now): if yaml_config: From 78e4d7e1ca8f49068d8f63f6c80bb3048f5ad8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 1 Apr 2022 17:31:39 +0200 Subject: [PATCH 0082/1224] Add auto_update property to supervisor and addon update entities (#69055) --- homeassistant/components/hassio/__init__.py | 26 +++++++++++++++ homeassistant/components/hassio/const.py | 1 + homeassistant/components/hassio/update.py | 7 ++++ tests/components/hassio/test_binary_sensor.py | 8 +++++ tests/components/hassio/test_init.py | 8 +++++ tests/components/hassio/test_sensor.py | 8 +++++ tests/components/hassio/test_update.py | 33 ++++++++++++++----- 7 files changed, 83 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8a69f553025..acc676bdf9f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -51,6 +51,7 @@ from .auth import async_setup_auth_view from .const import ( ATTR_ADDON, ATTR_ADDONS, + ATTR_AUTO_UPDATE, ATTR_CHANGELOG, ATTR_DISCOVERY, ATTR_FOLDERS, @@ -98,6 +99,7 @@ DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" +DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) @@ -422,6 +424,16 @@ def get_supervisor_info(hass): return hass.data.get(DATA_SUPERVISOR_INFO) +@callback +@bind_hass +def get_addons_info(hass): + """Return Addons info. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_INFO) + + @callback @bind_hass def get_addons_stats(hass): @@ -607,6 +619,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: changelog = await hassio.get_addon_changelog(slug) return (slug, changelog) + async def update_addon_info(slug): + """Return the info for an add-on.""" + info = await hassio.get_addon_info(slug) + return (slug, info) + async def update_info_data(now): """Update last available supervisor information.""" @@ -641,6 +658,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: *[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons] ) ) + hass.data[DATA_ADDONS_INFO] = dict( + await asyncio.gather( + *[update_addon_info(addon[ATTR_SLUG]) for addon in addons] + ) + ) if ADDONS_COORDINATOR in hass.data: await hass.data[ADDONS_COORDINATOR].async_refresh() @@ -845,6 +867,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" new_data = {} supervisor_info = get_supervisor_info(self.hass) + addons_info = get_addons_info(self.hass) addons_stats = get_addons_stats(self.hass) addons_changelogs = get_addons_changelogs(self.hass) store_data = get_store(self.hass) @@ -858,6 +881,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): addon[ATTR_SLUG]: { **addon, **((addons_stats or {}).get(addon[ATTR_SLUG], {})), + ATTR_AUTO_UPDATE: addons_info.get(addon[ATTR_SLUG], {}).get( + ATTR_AUTO_UPDATE, False + ), ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), ATTR_REPOSITORY: repositories.get( addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 7f62748b835..8c27fdebb17 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -39,6 +39,7 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" +ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 41539aa21e0..d2953a0f801 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -24,6 +24,7 @@ from . import ( async_update_supervisor, ) from .const import ( + ATTR_AUTO_UPDATE, ATTR_CHANGELOG, ATTR_VERSION, ATTR_VERSION_LATEST, @@ -99,6 +100,11 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): """Return the add-on data.""" return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] + @property + def auto_update(self): + """Return true if auto-update is enabled for the add-on.""" + return self._addon_data[ATTR_AUTO_UPDATE] + @property def title(self) -> str | None: """Return the title of the update.""" @@ -210,6 +216,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Update entity to handle updates for the Home Assistant Supervisor.""" + _attr_auto_update = True _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "Home Assistant Supervisor" diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index e48f8a6a481..ef114b90771 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -115,7 +115,15 @@ def mock_all(aioclient_mock, request): }, ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 30776fd5b17..1a50d9c3036 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -139,7 +139,15 @@ def mock_all(aioclient_mock, request): }, ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index df309e94360..e6a35c4744d 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -108,7 +108,15 @@ def mock_all(aioclient_mock, request): }, ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 8442f01abf3..f9b7c201ba3 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -121,23 +121,37 @@ def mock_all(aioclient_mock, request): }, ) aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", + json={"result": "ok", "data": {"auto_update": True}}, + ) aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/addons/test2/info", + json={"result": "ok", "data": {"auto_update": False}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) @pytest.mark.parametrize( - "entity_id,expected", + "entity_id,expected_state, auto_update", [ - ("update.home_assistant_operating_system_update", "on"), - ("update.home_assistant_supervisor_update", "on"), - ("update.home_assistant_core_update", "on"), - ("update.test_update", "on"), - ("update.test2_update", "off"), + ("update.home_assistant_operating_system_update", "on", False), + ("update.home_assistant_supervisor_update", "on", True), + ("update.home_assistant_core_update", "on", False), + ("update.test_update", "on", True), + ("update.test2_update", "off", False), ], ) -async def test_update_entities(hass, entity_id, expected, aioclient_mock): +async def test_update_entities( + hass, + entity_id, + expected_state, + auto_update, + aioclient_mock, +): """Test update entities.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) @@ -153,7 +167,10 @@ async def test_update_entities(hass, entity_id, expected, aioclient_mock): # Verify that the entity have the expected state. state = hass.states.get(entity_id) - assert state.state == expected + assert state.state == expected_state + + # Verify that the auto_update attribute is correct + assert state.attributes["auto_update"] is auto_update async def test_update_addon(hass, aioclient_mock): From 4f4f7e40e37939f34c2e91bc8217f76688472adb Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 1 Apr 2022 08:48:14 -0700 Subject: [PATCH 0083/1224] Bump pyoverkiz to 1.3.14 in Overkiz integration (#69084) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 53f2a7227bd..3409c06be26 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz (by Somfy)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.3.13"], + "requirements": ["pyoverkiz==1.3.14"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 655031a667e..6609a4e23d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1705,7 +1705,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.13 +pyoverkiz==1.3.14 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da78199a073..8202075728e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1142,7 +1142,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.13 +pyoverkiz==1.3.14 # homeassistant.components.openweathermap pyowm==3.2.0 From 8cf6ac281ee6ad4122fa0836fad297d2da50cc9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Apr 2022 05:49:21 -1000 Subject: [PATCH 0084/1224] Convert statistics to use history api for database access (#68411) --- homeassistant/components/statistics/sensor.py | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index d632e9710cf..aaca8a98290 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -12,9 +12,7 @@ from typing import Any, Literal, cast import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import StateAttributes, States -from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, @@ -474,37 +472,29 @@ class StatisticsSensor(SensorEntity): def _fetch_states_from_database(self) -> list[State]: """Fetch the states from the database.""" _LOGGER.debug("%s: initializing values from the database", self.entity_id) - states = [] - - with session_scope(hass=self.hass) as session: - query = session.query(States, StateAttributes).filter( - States.entity_id == self._source_entity_id.lower() + lower_entity_id = self._source_entity_id.lower() + if self._samples_max_age is not None: + start_date = ( + dt_util.utcnow() - self._samples_max_age - timedelta(microseconds=1) ) - - if self._samples_max_age is not None: - records_older_then = dt_util.utcnow() - self._samples_max_age - _LOGGER.debug( - "%s: retrieve records not older then %s", - self.entity_id, - records_older_then, - ) - query = query.filter(States.last_updated >= records_older_then) - else: - _LOGGER.debug("%s: retrieving all records", self.entity_id) - - query = query.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id + _LOGGER.debug( + "%s: retrieve records not older then %s", + self.entity_id, + start_date, ) - query = query.order_by(States.last_updated.desc()).limit( - self._samples_max_buffer_size - ) - if results := execute(query, to_native=False, validate_entity_ids=False): - for state, attributes in results: - native = state.to_native() - if not native.attributes: - native.attributes = attributes.to_native() - states.append(native) - return states + else: + start_date = datetime.fromtimestamp(0, tz=dt_util.UTC) + _LOGGER.debug("%s: retrieving all records", self.entity_id) + entity_states = history.state_changes_during_period( + self.hass, + start_date, + entity_id=lower_entity_id, + descending=True, + limit=self._samples_max_buffer_size, + include_start_time_state=False, + ) + # Need to cast since minimal responses is not passed in + return cast(list[State], entity_states.get(lower_entity_id, [])) async def _initialize_from_database(self) -> None: """Initialize the list of states from the database. From 7f1b90a51cf06ce5d37949f0053b7e2f9c82e8ef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 18:18:13 +0200 Subject: [PATCH 0085/1224] Migrate crownstone light to color_mode (#69081) --- homeassistant/components/crownstone/light.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/crownstone/light.py b/homeassistant/components/crownstone/light.py index ff647b2fc84..de35ad3d508 100644 --- a/homeassistant/components/crownstone/light.py +++ b/homeassistant/components/crownstone/light.py @@ -11,7 +11,8 @@ from crownstone_uart import CrownstoneUart from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -98,11 +99,16 @@ class CrownstoneEntity(CrownstoneBaseEntity, LightEntity): return crownstone_state_to_hass(self.device.state) > 0 @property - def supported_features(self) -> int: - """Return the supported features of this Crownstone.""" + def color_mode(self) -> str: + """Return the color mode of the light.""" if self.device.abilities.get(DIMMING_ABILITY).is_enabled: - return SUPPORT_BRIGHTNESS - return 0 + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF + + @property + def supported_color_modes(self) -> set[str] | None: + """Flag supported color modes.""" + return {self.color_mode} async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" From 93571c2d01cc88739b9dcf64e998e964a1ba27aa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 18:38:03 +0200 Subject: [PATCH 0086/1224] Add EntityFeature enum to Camera (#69072) --- homeassistant/components/camera/__init__.py | 22 ++++++++++++++++----- homeassistant/components/demo/camera.py | 4 ++-- tests/components/camera/test_init.py | 4 ++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f3258681b52..c9045f7975d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -8,6 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta +from enum import IntEnum from functools import partial import hashlib import logging @@ -88,7 +89,16 @@ STATE_RECORDING: Final = "recording" STATE_STREAMING: Final = "streaming" STATE_IDLE: Final = "idle" -# Bitfield of features supported by the camera entity + +class CameraEntityFeature(IntEnum): + """Supported features of the camera entity.""" + + ON_OFF = 1 + STREAM = 2 + + +# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. +# Pleease use the CameraEntityFeature enum instead. SUPPORT_ON_OFF: Final = 1 SUPPORT_STREAM: Final = 2 @@ -499,7 +509,7 @@ class Camera(Entity): """ if hasattr(self, "_attr_frontend_stream_type"): return self._attr_frontend_stream_type - if not self.supported_features & SUPPORT_STREAM: + if not self.supported_features & CameraEntityFeature.STREAM: return None if self._rtsp_to_webrtc: return STREAM_TYPE_WEB_RTC @@ -535,7 +545,8 @@ class Camera(Entity): async def stream_source(self) -> str | None: """Return the source of the stream. - This is used by cameras with SUPPORT_STREAM and STREAM_TYPE_HLS. + This is used by cameras with CameraEntityFeature.STREAM + and STREAM_TYPE_HLS. """ # pylint: disable=no-self-use return None @@ -543,7 +554,8 @@ class Camera(Entity): async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: """Handle the WebRTC offer and return an answer. - This is used by cameras with SUPPORT_STREAM and STREAM_TYPE_WEB_RTC. + This is used by cameras with CameraEntityFeature.STREAM + and STREAM_TYPE_WEB_RTC. Integrations can override with a native WebRTC implementation. """ @@ -682,7 +694,7 @@ class Camera(Entity): async def _async_use_rtsp_to_webrtc(self) -> bool: """Determine if a WebRTC provider can be used for the camera.""" - if not self.supported_features & SUPPORT_STREAM: + if not self.supported_features & CameraEntityFeature.STREAM: return False if DATA_RTSP_TO_WEB_RTC not in self.hass.data: return False diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index c1bf54d4629..25026bce11b 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from homeassistant.components.camera import SUPPORT_ON_OFF, Camera +from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -39,7 +39,7 @@ class DemoCamera(Camera): _attr_is_streaming = True _attr_motion_detection_enabled = False - _attr_supported_features = SUPPORT_ON_OFF + _attr_supported_features = CameraEntityFeature.ON_OFF def __init__(self, name, content_type): """Initialize demo camera component.""" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index e72941ef488..1b30facf1de 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -58,7 +58,7 @@ async def mock_stream_source_fixture(): return_value=STREAM_SOURCE, ) as mock_stream_source, patch( "homeassistant.components.camera.Camera.supported_features", - return_value=camera.SUPPORT_STREAM, + return_value=camera.CameraEntityFeature.STREAM, ): yield mock_stream_source @@ -71,7 +71,7 @@ async def mock_hls_stream_source_fixture(): return_value=HLS_STREAM_SOURCE, ) as mock_hls_stream_source, patch( "homeassistant.components.camera.Camera.supported_features", - return_value=camera.SUPPORT_STREAM, + return_value=camera.CameraEntityFeature.STREAM, ): yield mock_hls_stream_source From 330c9310670ef114f4f7e9366a86aeb6d87214bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 18:38:21 +0200 Subject: [PATCH 0087/1224] Add EntityFeature enum to Cover (#69088) --- homeassistant/components/cover/__init__.py | 65 ++++++++++++++++------ homeassistant/components/demo/cover.py | 17 ++---- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 44573c0a6da..aecca5a4029 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta +from enum import IntEnum import functools as ft import logging from typing import Any, final @@ -79,6 +80,22 @@ DEVICE_CLASS_SHADE = CoverDeviceClass.SHADE.value DEVICE_CLASS_SHUTTER = CoverDeviceClass.SHUTTER.value DEVICE_CLASS_WINDOW = CoverDeviceClass.WINDOW.value + +class CoverEntityFeature(IntEnum): + """Supported features of the cover entity.""" + + OPEN = 1 + CLOSE = 2 + SET_POSITION = 4 + STOP = 8 + OPEN_TILT = 16 + CLOSE_TILT = 32 + STOP_TILT = 64 + SET_TILT_POSITION = 128 + + +# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. +# Please use the CoverEntityFeature enum instead. SUPPORT_OPEN = 1 SUPPORT_CLOSE = 2 SUPPORT_SET_POSITION = 4 @@ -109,11 +126,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_OPEN_COVER, {}, "async_open_cover", [SUPPORT_OPEN] + SERVICE_OPEN_COVER, {}, "async_open_cover", [CoverEntityFeature.OPEN] ) component.async_register_entity_service( - SERVICE_CLOSE_COVER, {}, "async_close_cover", [SUPPORT_CLOSE] + SERVICE_CLOSE_COVER, {}, "async_close_cover", [CoverEntityFeature.CLOSE] ) component.async_register_entity_service( @@ -124,27 +141,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) }, "async_set_cover_position", - [SUPPORT_SET_POSITION], + [CoverEntityFeature.SET_POSITION], ) component.async_register_entity_service( - SERVICE_STOP_COVER, {}, "async_stop_cover", [SUPPORT_STOP] + SERVICE_STOP_COVER, {}, "async_stop_cover", [CoverEntityFeature.STOP] ) component.async_register_entity_service( - SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_OPEN | SUPPORT_CLOSE] + SERVICE_TOGGLE, + {}, + "async_toggle", + [CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE], ) component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt", [SUPPORT_OPEN_TILT] + SERVICE_OPEN_COVER_TILT, + {}, + "async_open_cover_tilt", + [CoverEntityFeature.OPEN_TILT], ) component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt", [SUPPORT_CLOSE_TILT] + SERVICE_CLOSE_COVER_TILT, + {}, + "async_close_cover_tilt", + [CoverEntityFeature.CLOSE_TILT], ) component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt", [SUPPORT_STOP_TILT] + SERVICE_STOP_COVER_TILT, + {}, + "async_stop_cover_tilt", + [CoverEntityFeature.STOP_TILT], ) component.async_register_entity_service( @@ -155,14 +184,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) }, "async_set_cover_tilt_position", - [SUPPORT_SET_TILT_POSITION], + [CoverEntityFeature.SET_TILT_POSITION], ) component.async_register_entity_service( SERVICE_TOGGLE_COVER_TILT, {}, "async_toggle_tilt", - [SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT], + [CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT], ) return True @@ -262,17 +291,19 @@ class CoverEntity(Entity): if self._attr_supported_features is not None: return self._attr_supported_features - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) if self.current_cover_position is not None: - supported_features |= SUPPORT_SET_POSITION + supported_features |= CoverEntityFeature.SET_POSITION if self.current_cover_tilt_position is not None: supported_features |= ( - SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT - | SUPPORT_SET_TILT_POSITION + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION ) return supported_features @@ -395,7 +426,7 @@ class CoverEntity(Entity): await self.async_close_cover_tilt(**kwargs) def _get_toggle_function(self, fns): - if SUPPORT_STOP | self.supported_features and ( + if CoverEntityFeature.STOP | self.supported_features and ( self.is_closing or self.is_opening ): return fns["stop"] diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index dab85964639..9a1ea6239ee 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -4,14 +4,9 @@ from __future__ import annotations from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - SUPPORT_CLOSE, - SUPPORT_CLOSE_TILT, - SUPPORT_OPEN, - SUPPORT_OPEN_TILT, - SUPPORT_SET_TILT_POSITION, - SUPPORT_STOP_TILT, CoverDeviceClass, CoverEntity, + CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -40,7 +35,7 @@ async def async_setup_platform( "cover_4", "Garage Door", device_class=CoverDeviceClass.GARAGE, - supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE), + supported_features=(CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE), ), DemoCover( hass, @@ -48,10 +43,10 @@ async def async_setup_platform( "Pergola Roof", tilt_position=60, supported_features=( - SUPPORT_OPEN_TILT - | SUPPORT_STOP_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_SET_TILT_POSITION + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION ), ), ] From 4c7e1fe0609a0c676bb4adacf0c1b3f19566904b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Apr 2022 18:40:43 +0200 Subject: [PATCH 0088/1224] Cleanup ENTITY_CATEGORIES_SCHEMA (#66549) Co-authored-by: Paulus Schoutsen Co-authored-by: epenet --- homeassistant/components/knx/schema.py | 26 +++++++++---------- .../components/mobile_app/webhook.py | 4 +-- homeassistant/components/mqtt/mixins.py | 4 +-- homeassistant/helpers/entity.py | 15 +++-------- tests/helpers/test_entity.py | 5 +++- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 555fcfc575b..5c5b05db62d 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -31,7 +31,7 @@ from homeassistant.const import ( Platform, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import validate_entity_category +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from .const import ( CONF_INVERT, @@ -262,7 +262,7 @@ class BinarySensorSchema(KNXPlatformSchema): ), vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_RESET_AFTER): cv.positive_float, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) @@ -298,7 +298,7 @@ class ButtonSchema(KNXPlatformSchema): vol.Exclusive( CONF_TYPE, "length_or_type", msg=length_or_type_msg ): object, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), vol.Any( @@ -442,7 +442,7 @@ class ClimateSchema(KNXPlatformSchema): ): vol.In(HVAC_MODES), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) @@ -497,7 +497,7 @@ class CoverSchema(KNXPlatformSchema): vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) @@ -560,7 +560,7 @@ class FanSchema(KNXPlatformSchema): vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator, vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MAX_STEP): cv.byte, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -664,7 +664,7 @@ class LightSchema(KNXPlatformSchema): vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), vol.Any( @@ -744,7 +744,7 @@ class NumberSchema(KNXPlatformSchema): vol.Optional(CONF_MAX): vol.Coerce(float), vol.Optional(CONF_MIN): vol.Coerce(float), vol.Optional(CONF_STEP): cv.positive_float, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), number_limit_sub_validator, @@ -766,7 +766,7 @@ class SceneSchema(KNXPlatformSchema): vol.Required(CONF_SCENE_NUMBER): vol.All( vol.Coerce(int), vol.Range(min=1, max=64) ), - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -797,7 +797,7 @@ class SelectSchema(KNXPlatformSchema): ], vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), select_options_sub_validator, @@ -822,7 +822,7 @@ class SensorSchema(KNXPlatformSchema): vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Required(CONF_TYPE): sensor_type_validator, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -843,7 +843,7 @@ class SwitchSchema(KNXPlatformSchema): vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -890,7 +890,7 @@ class WeatherSchema(KNXPlatformSchema): vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 860b8ef7b53..eb93aba59cb 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -45,7 +45,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import validate_entity_category +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from homeassistant.util.decorator import Registry from .const import ( @@ -446,7 +446,7 @@ def _validate_state_class_sensor(value: dict): vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( None, bool, str, int, float ), - vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES), }, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 09efb384196..bf3431c5324 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -37,11 +37,11 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import ( + ENTITY_CATEGORIES_SCHEMA, DeviceInfo, Entity, EntityCategory, async_generate_entity_id, - validate_entity_category, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service @@ -212,7 +212,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, - vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f6869787f5b..2b5604832d5 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -12,7 +12,7 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any, Literal, TypedDict, final +from typing import Any, Final, Literal, TypedDict, final import voluptuous as vol @@ -28,7 +28,6 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, - ENTITY_CATEGORIES, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -65,15 +64,6 @@ SOURCE_PLATFORM_CONFIG = "platform_config" FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 -def validate_entity_category(value: Any | None) -> EntityCategory: - """Validate entity category configuration.""" - value = vol.In(ENTITY_CATEGORIES)(value) - return EntityCategory(value) - - -ENTITY_CATEGORIES_SCHEMA = validate_entity_category - - @callback @bind_hass def entity_sources(hass: HomeAssistant) -> dict[str, dict[str, str]]: @@ -214,6 +204,9 @@ class EntityCategory(StrEnum): SYSTEM = "system" +ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory) + + class EntityPlatformState(Enum): """The platform state of an entity.""" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9130de04e0f..5bd04742195 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -927,5 +927,8 @@ def test_entity_category_schema(value, expected): def test_entity_category_schema_error(value): """Test entity category schema.""" schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) - with pytest.raises(vol.Invalid): + with pytest.raises( + vol.Invalid, + match=r"expected EntityCategory or one of 'config', 'diagnostic', 'system'", + ): schema(value) From 8fc55b71c5153580508446d478adfb450c76ea41 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 18:41:49 +0200 Subject: [PATCH 0089/1224] Add EntityFeature enum to Climate (#69077) --- homeassistant/components/climate/__init__.py | 58 ++++++++++--------- homeassistant/components/climate/const.py | 17 ++++++ homeassistant/components/demo/climate.py | 28 ++++----- .../components/climate/test_device_action.py | 14 ++++- .../climate/test_device_condition.py | 14 ++++- 5 files changed, 86 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index b076e0db01a..91b4db1fc33 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util.temperature import convert as convert_temperature -from .const import ( +from .const import ( # noqa: F401 ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, @@ -74,6 +74,7 @@ from .const import ( SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + ClimateEntityFeature, ) DEFAULT_MIN_TEMP = 7 @@ -122,37 +123,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_SET_PRESET_MODE, {vol.Required(ATTR_PRESET_MODE): cv.string}, "async_set_preset_mode", - [SUPPORT_PRESET_MODE], + [ClimateEntityFeature.PRESET_MODE], ) component.async_register_entity_service( SERVICE_SET_AUX_HEAT, {vol.Required(ATTR_AUX_HEAT): cv.boolean}, async_service_aux_heat, - [SUPPORT_AUX_HEAT], + [ClimateEntityFeature.AUX_HEAT], ) component.async_register_entity_service( SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set, - [SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE], + [ + ClimateEntityFeature.TARGET_TEMPERATURE, + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, + ], ) component.async_register_entity_service( SERVICE_SET_HUMIDITY, {vol.Required(ATTR_HUMIDITY): vol.Coerce(int)}, "async_set_humidity", - [SUPPORT_TARGET_HUMIDITY], + [ClimateEntityFeature.TARGET_HUMIDITY], ) component.async_register_entity_service( SERVICE_SET_FAN_MODE, {vol.Required(ATTR_FAN_MODE): cv.string}, "async_set_fan_mode", - [SUPPORT_FAN_MODE], + [ClimateEntityFeature.FAN_MODE], ) component.async_register_entity_service( SERVICE_SET_SWING_MODE, {vol.Required(ATTR_SWING_MODE): cv.string}, "async_set_swing_mode", - [SUPPORT_SWING_MODE], + [ClimateEntityFeature.SWING_MODE], ) return True @@ -235,17 +239,17 @@ class ClimateEntity(Entity): if self.target_temperature_step: data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step - if supported_features & SUPPORT_TARGET_HUMIDITY: + if supported_features & ClimateEntityFeature.TARGET_HUMIDITY: data[ATTR_MIN_HUMIDITY] = self.min_humidity data[ATTR_MAX_HUMIDITY] = self.max_humidity - if supported_features & SUPPORT_FAN_MODE: + if supported_features & ClimateEntityFeature.FAN_MODE: data[ATTR_FAN_MODES] = self.fan_modes - if supported_features & SUPPORT_PRESET_MODE: + if supported_features & ClimateEntityFeature.PRESET_MODE: data[ATTR_PRESET_MODES] = self.preset_modes - if supported_features & SUPPORT_SWING_MODE: + if supported_features & ClimateEntityFeature.SWING_MODE: data[ATTR_SWING_MODES] = self.swing_modes return data @@ -264,7 +268,7 @@ class ClimateEntity(Entity): ), } - if supported_features & SUPPORT_TARGET_TEMPERATURE: + if supported_features & ClimateEntityFeature.TARGET_TEMPERATURE: data[ATTR_TEMPERATURE] = show_temp( self.hass, self.target_temperature, @@ -272,7 +276,7 @@ class ClimateEntity(Entity): self.precision, ) - if supported_features & SUPPORT_TARGET_TEMPERATURE_RANGE: + if supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: data[ATTR_TARGET_TEMP_HIGH] = show_temp( self.hass, self.target_temperature_high, @@ -289,22 +293,22 @@ class ClimateEntity(Entity): if self.current_humidity is not None: data[ATTR_CURRENT_HUMIDITY] = self.current_humidity - if supported_features & SUPPORT_TARGET_HUMIDITY: + if supported_features & ClimateEntityFeature.TARGET_HUMIDITY: data[ATTR_HUMIDITY] = self.target_humidity - if supported_features & SUPPORT_FAN_MODE: + if supported_features & ClimateEntityFeature.FAN_MODE: data[ATTR_FAN_MODE] = self.fan_mode if self.hvac_action: data[ATTR_HVAC_ACTION] = self.hvac_action - if supported_features & SUPPORT_PRESET_MODE: + if supported_features & ClimateEntityFeature.PRESET_MODE: data[ATTR_PRESET_MODE] = self.preset_mode - if supported_features & SUPPORT_SWING_MODE: + if supported_features & ClimateEntityFeature.SWING_MODE: data[ATTR_SWING_MODE] = self.swing_mode - if supported_features & SUPPORT_AUX_HEAT: + if supported_features & ClimateEntityFeature.AUX_HEAT: data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF return data @@ -367,7 +371,7 @@ class ClimateEntity(Entity): def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach. - Requires SUPPORT_TARGET_TEMPERATURE_RANGE. + Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. """ return self._attr_target_temperature_high @@ -375,7 +379,7 @@ class ClimateEntity(Entity): def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach. - Requires SUPPORT_TARGET_TEMPERATURE_RANGE. + Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. """ return self._attr_target_temperature_low @@ -383,7 +387,7 @@ class ClimateEntity(Entity): def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. - Requires SUPPORT_PRESET_MODE. + Requires ClimateEntityFeature.PRESET_MODE. """ return self._attr_preset_mode @@ -391,7 +395,7 @@ class ClimateEntity(Entity): def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. - Requires SUPPORT_PRESET_MODE. + Requires ClimateEntityFeature.PRESET_MODE. """ return self._attr_preset_modes @@ -399,7 +403,7 @@ class ClimateEntity(Entity): def is_aux_heat(self) -> bool | None: """Return true if aux heater. - Requires SUPPORT_AUX_HEAT. + Requires ClimateEntityFeature.AUX_HEAT. """ return self._attr_is_aux_heat @@ -407,7 +411,7 @@ class ClimateEntity(Entity): def fan_mode(self) -> str | None: """Return the fan setting. - Requires SUPPORT_FAN_MODE. + Requires ClimateEntityFeature.FAN_MODE. """ return self._attr_fan_mode @@ -415,7 +419,7 @@ class ClimateEntity(Entity): def fan_modes(self) -> list[str] | None: """Return the list of available fan modes. - Requires SUPPORT_FAN_MODE. + Requires ClimateEntityFeature.FAN_MODE. """ return self._attr_fan_modes @@ -423,7 +427,7 @@ class ClimateEntity(Entity): def swing_mode(self) -> str | None: """Return the swing setting. - Requires SUPPORT_SWING_MODE. + Requires ClimateEntityFeature.SWING_MODE. """ return self._attr_swing_mode @@ -431,7 +435,7 @@ class ClimateEntity(Entity): def swing_modes(self) -> list[str] | None: """Return the list of available swing modes. - Requires SUPPORT_SWING_MODE. + Requires ClimateEntityFeature.SWING_MODE. """ return self._attr_swing_modes diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 773ee5920da..5ee2424e7f7 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -1,5 +1,7 @@ """Provides the constants needed for component.""" +from enum import IntEnum + # All activity disabled / Device is off/standby HVAC_MODE_OFF = "off" @@ -133,6 +135,21 @@ SERVICE_SET_HVAC_MODE = "set_hvac_mode" SERVICE_SET_SWING_MODE = "set_swing_mode" SERVICE_SET_TEMPERATURE = "set_temperature" + +class ClimateEntityFeature(IntEnum): + """Supported features of the climate entity.""" + + TARGET_TEMPERATURE = 1 + TARGET_TEMPERATURE_RANGE = 2 + TARGET_HUMIDITY = 4 + FAN_MODE = 8 + PRESET_MODE = 16 + SWING_MODE = 32 + AUX_HEAT = 64 + + +# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. +# Pleease use the ClimateEntityFeature enum instead. SUPPORT_TARGET_TEMPERATURE = 1 SUPPORT_TARGET_TEMPERATURE_RANGE = 2 SUPPORT_TARGET_HUMIDITY = 4 diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index eb1c38d90bb..750354a567a 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -13,13 +13,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, - SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, + ClimateEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -138,19 +132,25 @@ class DemoClimate(ClimateEntity): self._name = name self._support_flags = SUPPORT_FLAGS if target_temperature is not None: - self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE + self._support_flags = ( + self._support_flags | ClimateEntityFeature.TARGET_TEMPERATURE + ) if preset is not None: - self._support_flags = self._support_flags | SUPPORT_PRESET_MODE + self._support_flags = self._support_flags | ClimateEntityFeature.PRESET_MODE if fan_mode is not None: - self._support_flags = self._support_flags | SUPPORT_FAN_MODE + self._support_flags = self._support_flags | ClimateEntityFeature.FAN_MODE if target_humidity is not None: - self._support_flags = self._support_flags | SUPPORT_TARGET_HUMIDITY + self._support_flags = ( + self._support_flags | ClimateEntityFeature.TARGET_HUMIDITY + ) if swing_mode is not None: - self._support_flags = self._support_flags | SUPPORT_SWING_MODE + self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE if aux is not None: - self._support_flags = self._support_flags | SUPPORT_AUX_HEAT + self._support_flags = self._support_flags | ClimateEntityFeature.AUX_HEAT if HVAC_MODE_HEAT_COOL in hvac_modes or HVAC_MODE_AUTO in hvac_modes: - self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE_RANGE + self._support_flags = ( + self._support_flags | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) self._target_temperature = target_temperature self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 4f926e1b094..cd9bc0b1baf 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -35,9 +35,19 @@ def entity_reg(hass): "set_state,features_reg,features_state,expected_action_types", [ (False, 0, 0, ["set_hvac_mode"]), - (False, const.SUPPORT_PRESET_MODE, 0, ["set_hvac_mode", "set_preset_mode"]), + ( + False, + const.ClimateEntityFeature.PRESET_MODE, + 0, + ["set_hvac_mode", "set_preset_mode"], + ), (True, 0, 0, ["set_hvac_mode"]), - (True, 0, const.SUPPORT_PRESET_MODE, ["set_hvac_mode", "set_preset_mode"]), + ( + True, + 0, + const.ClimateEntityFeature.PRESET_MODE, + ["set_hvac_mode", "set_preset_mode"], + ), ], ) async def test_get_actions( diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 65c1e17048b..e76073f1e25 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -41,9 +41,19 @@ def calls(hass): "set_state,features_reg,features_state,expected_condition_types", [ (False, 0, 0, ["is_hvac_mode"]), - (False, const.SUPPORT_PRESET_MODE, 0, ["is_hvac_mode", "is_preset_mode"]), + ( + False, + const.ClimateEntityFeature.PRESET_MODE, + 0, + ["is_hvac_mode", "is_preset_mode"], + ), (True, 0, 0, ["is_hvac_mode"]), - (True, 0, const.SUPPORT_PRESET_MODE, ["is_hvac_mode", "is_preset_mode"]), + ( + True, + 0, + const.ClimateEntityFeature.PRESET_MODE, + ["is_hvac_mode", "is_preset_mode"], + ), ], ) async def test_get_conditions( From 325a260cfd98fa6fbf1b237fd37f6dbb12f7687d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 1 Apr 2022 10:26:34 -0700 Subject: [PATCH 0090/1224] Fix Protexial alarm in Overkiz integration (#68996) --- .../components/overkiz/alarm_control_panel.py | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py index f6bdb6eef0e..4e62547bec5 100644 --- a/homeassistant/components/overkiz/alarm_control_panel.py +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -22,6 +22,7 @@ from homeassistant.components.alarm_control_panel.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, @@ -54,15 +55,15 @@ class OverkizAlarmDescription( """Class to describe an Overkiz alarm control panel.""" alarm_disarm: str | None = None - alarm_disarm_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_disarm_args: OverkizStateType | list[OverkizStateType] = [] alarm_arm_home: str | None = None - alarm_arm_home_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_home_args: OverkizStateType | list[OverkizStateType] = [] alarm_arm_night: str | None = None - alarm_arm_night_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_night_args: OverkizStateType | list[OverkizStateType] = [] alarm_arm_away: str | None = None - alarm_arm_away_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_away_args: OverkizStateType | list[OverkizStateType] = [] alarm_trigger: str | None = None - alarm_trigger_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_trigger_args: OverkizStateType | list[OverkizStateType] = [] MAP_INTERNAL_STATUS_STATE: dict[str, str] = { @@ -91,23 +92,25 @@ def _state_tsk_alarm_controller(select_state: Callable[[str], OverkizStateType]) ] +MAP_CORE_ACTIVE_ZONES: dict[str, str] = { + OverkizCommandParam.A: STATE_ALARM_ARMED_HOME, + f"{OverkizCommandParam.A},{OverkizCommandParam.B}": STATE_ALARM_ARMED_NIGHT, + f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}": STATE_ALARM_ARMED_AWAY, +} + + def _state_stateful_alarm_controller( select_state: Callable[[str], OverkizStateType] ) -> str: """Return the state of the device.""" - if state := cast(list, select_state(OverkizState.CORE_ACTIVE_ZONES)): - if [ - OverkizCommandParam.A, - OverkizCommandParam.B, - OverkizCommandParam.C, - ] in state: - return STATE_ALARM_ARMED_AWAY + if state := cast(str, select_state(OverkizState.CORE_ACTIVE_ZONES)): + # The Stateful Alarm Controller has 3 zones with the following options: + # (A, B, C, A,B, B,C, A,C, A,B,C). Since it is not possible to map this to AlarmControlPanel entity, + # only the most important zones are mapped, other zones can only be disarmed. + if state in MAP_CORE_ACTIVE_ZONES: + return MAP_CORE_ACTIVE_ZONES[state] - if [OverkizCommandParam.A, OverkizCommandParam.B] in state: - return STATE_ALARM_ARMED_NIGHT - - if OverkizCommandParam.A in state: - return STATE_ALARM_ARMED_HOME + return STATE_ALARM_ARMED_CUSTOM_BYPASS return STATE_ALARM_DISARMED @@ -181,17 +184,13 @@ ALARM_DESCRIPTIONS: list[OverkizAlarmDescription] = [ SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT ), fn_state=_state_stateful_alarm_controller, - alarm_disarm=OverkizCommand.DISARM, + alarm_disarm=OverkizCommand.ALARM_OFF, alarm_arm_home=OverkizCommand.ALARM_ZONE_ON, - alarm_arm_home_args=[OverkizCommandParam.A], + alarm_arm_home_args=OverkizCommandParam.A, alarm_arm_night=OverkizCommand.ALARM_ZONE_ON, - alarm_arm_night_args=[OverkizCommandParam.A, OverkizCommandParam.B], + alarm_arm_night_args=f"{OverkizCommandParam.A}, {OverkizCommandParam.B}", alarm_arm_away=OverkizCommand.ALARM_ZONE_ON, - alarm_arm_away_args=[ - OverkizCommandParam.A, - OverkizCommandParam.B, - OverkizCommandParam.C, - ], + alarm_arm_away_args=f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}", ), # MyFoxAlarmController OverkizAlarmDescription( From 02dbd617b9eae25bb5ae5d7562f27fc0a62e893f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 20:10:52 +0200 Subject: [PATCH 0091/1224] Add EntityFeature enum to Humidifier (#69092) --- homeassistant/components/demo/humidifier.py | 4 ++-- homeassistant/components/humidifier/__init__.py | 11 ++++++----- homeassistant/components/humidifier/const.py | 11 +++++++++++ homeassistant/components/humidifier/device_action.py | 2 +- .../components/humidifier/device_condition.py | 2 +- homeassistant/components/humidifier/intent.py | 4 ++-- tests/components/humidifier/test_device_action.py | 4 ++-- tests/components/humidifier/test_device_condition.py | 4 ++-- 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index 0b366e90efe..c04c44cd8c5 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity -from homeassistant.components.humidifier.const import SUPPORT_MODES +from homeassistant.components.humidifier.const import HumidifierEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -71,7 +71,7 @@ class DemoHumidifier(HumidifierEntity): self._attr_supported_features = SUPPORT_FLAGS if mode is not None: self._attr_supported_features = ( - self._attr_supported_features | SUPPORT_MODES + self._attr_supported_features | HumidifierEntityFeature.MODES ) self._attr_target_humidity = target_humidity self._attr_mode = mode diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 8eda2589417..b5f02a6a5d3 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -41,6 +41,7 @@ from .const import ( # noqa: F401 SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, SUPPORT_MODES, + HumidifierEntityFeature, ) _LOGGER = logging.getLogger(__name__) @@ -88,7 +89,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_SET_MODE, {vol.Required(ATTR_MODE): cv.string}, "async_set_mode", - [SUPPORT_MODES], + [HumidifierEntityFeature.MODES], ) component.async_register_entity_service( SERVICE_SET_HUMIDITY, @@ -142,7 +143,7 @@ class HumidifierEntity(ToggleEntity): ATTR_MAX_HUMIDITY: self.max_humidity, } - if supported_features & SUPPORT_MODES: + if supported_features & HumidifierEntityFeature.MODES: data[ATTR_AVAILABLE_MODES] = self.available_modes return data @@ -166,7 +167,7 @@ class HumidifierEntity(ToggleEntity): if self.target_humidity is not None: data[ATTR_HUMIDITY] = self.target_humidity - if supported_features & SUPPORT_MODES: + if supported_features & HumidifierEntityFeature.MODES: data[ATTR_MODE] = self.mode return data @@ -180,7 +181,7 @@ class HumidifierEntity(ToggleEntity): def mode(self) -> str | None: """Return the current mode, e.g., home, auto, baby. - Requires SUPPORT_MODES. + Requires HumidifierEntityFeature.MODES. """ return self._attr_mode @@ -188,7 +189,7 @@ class HumidifierEntity(ToggleEntity): def available_modes(self) -> list[str] | None: """Return a list of available modes. - Requires SUPPORT_MODES. + Requires HumidifierEntityFeature.MODES. """ return self._attr_available_modes diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index a3df2756af9..03f89f5489a 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -1,4 +1,6 @@ """Provides the constants needed for component.""" +from enum import IntEnum + MODE_NORMAL = "normal" MODE_ECO = "eco" MODE_AWAY = "away" @@ -27,4 +29,13 @@ DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier" SERVICE_SET_MODE = "set_mode" SERVICE_SET_HUMIDITY = "set_humidity" + +class HumidifierEntityFeature(IntEnum): + """Supported features of the alarm control panel entity.""" + + MODES = 1 + + +# The SUPPORT_MODES constant is deprecated as of Home Assistant 2022.5. +# Please use the HumidifierEntityFeature enum instead. SUPPORT_MODES = 1 diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index 5ff79bdf2be..db51ab34baa 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -66,7 +66,7 @@ async def async_get_actions( } actions.append({**base_action, CONF_TYPE: "set_humidity"}) - if supported_features & const.SUPPORT_MODES: + if supported_features & const.HumidifierEntityFeature.MODES: actions.append({**base_action, CONF_TYPE: "set_mode"}) return actions diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index 323d199cfc6..c58c247d569 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -51,7 +51,7 @@ async def async_get_conditions( supported_features = get_supported_features(hass, entry.entity_id) - if supported_features & const.SUPPORT_MODES: + if supported_features & const.HumidifierEntityFeature.MODES: conditions.append( { CONF_CONDITION: "device", diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index d9ecafbc537..aeeb18cceec 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -13,7 +13,7 @@ from . import ( SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, SERVICE_TURN_ON, - SUPPORT_MODES, + HumidifierEntityFeature, ) INTENT_HUMIDITY = "HassHumidifierSetpoint" @@ -90,7 +90,7 @@ class SetModeHandler(intent.IntentHandler): service_data = {ATTR_ENTITY_ID: state.entity_id} - intent.async_test_feature(state, SUPPORT_MODES, "modes") + intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") mode = slots["mode"]["value"] if mode not in state.attributes.get(ATTR_AVAILABLE_MODES, []): diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index f8391e2509b..fee8afc7a72 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -36,9 +36,9 @@ def entity_reg(hass): "set_state,features_reg,features_state,expected_action_types", [ (False, 0, 0, []), - (False, const.SUPPORT_MODES, 0, ["set_mode"]), + (False, const.HumidifierEntityFeature.MODES, 0, ["set_mode"]), (True, 0, 0, []), - (True, 0, const.SUPPORT_MODES, ["set_mode"]), + (True, 0, const.HumidifierEntityFeature.MODES, ["set_mode"]), ], ) async def test_get_actions( diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index aed1079b915..9c9b83a79d6 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -42,9 +42,9 @@ def calls(hass): "set_state,features_reg,features_state,expected_condition_types", [ (False, 0, 0, []), - (False, const.SUPPORT_MODES, 0, ["is_mode"]), + (False, const.HumidifierEntityFeature.MODES, 0, ["is_mode"]), (True, 0, 0, []), - (True, 0, const.SUPPORT_MODES, ["is_mode"]), + (True, 0, const.HumidifierEntityFeature.MODES, ["is_mode"]), ], ) async def test_get_conditions( From c31e788439624da784c2809e1a507cc93ec887c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 20:11:17 +0200 Subject: [PATCH 0092/1224] Rename current_version to installed_version in Update platform (#69093) --- homeassistant/components/demo/update.py | 16 ++--- homeassistant/components/hassio/update.py | 17 +++-- homeassistant/components/pi_hole/update.py | 14 ++-- .../components/synology_dsm/update.py | 6 +- homeassistant/components/update/__init__.py | 29 ++++---- homeassistant/components/update/const.py | 2 +- .../components/update/significant_change.py | 4 +- homeassistant/components/wled/update.py | 4 +- tests/components/demo/test_update.py | 12 ++-- tests/components/pi_hole/test_update.py | 12 ++-- tests/components/update/test_init.py | 70 +++++++++---------- tests/components/update/test_recorder.py | 4 +- .../update/test_significant_change.py | 6 +- tests/components/wled/test_update.py | 14 ++-- .../custom_components/test/update.py | 28 ++++---- 15 files changed, 121 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py index e0f5814c63d..648ad59bb55 100644 --- a/homeassistant/components/demo/update.py +++ b/homeassistant/components/demo/update.py @@ -31,7 +31,7 @@ async def async_setup_platform( unique_id="update_no_install", name="Demo Update No Install", title="Awesomesoft Inc.", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", release_summary="Awesome update, fixing everything!", release_url="https://www.example.com/release/1.0.1", @@ -41,14 +41,14 @@ async def async_setup_platform( unique_id="update_2_date", name="Demo No Update", title="AdGuard Home", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.0", ), DemoUpdate( unique_id="update_addon", name="Demo add-on", title="AdGuard Home", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", release_summary="Awesome update, fixing everything!", release_url="https://www.example.com/release/1.0.1", @@ -57,7 +57,7 @@ async def async_setup_platform( unique_id="update_light_bulb", name="Demo Living Room Bulb Update", title="Philips Lamps Firmware", - current_version="1.93.3", + installed_version="1.93.3", latest_version="1.94.2", release_summary="Added support for effects", release_url="https://www.example.com/release/1.93.3", @@ -67,7 +67,7 @@ async def async_setup_platform( unique_id="update_support_progress", name="Demo Update with Progress", title="Philips Lamps Firmware", - current_version="1.93.3", + installed_version="1.93.3", latest_version="1.94.2", support_progress=True, release_summary="Added support for effects", @@ -104,7 +104,7 @@ class DemoUpdate(UpdateEntity): unique_id: str, name: str, title: str | None, - current_version: str | None, + installed_version: str | None, latest_version: str | None, release_summary: str | None = None, release_url: str | None = None, @@ -114,7 +114,7 @@ class DemoUpdate(UpdateEntity): device_class: UpdateDeviceClass | None = None, ) -> None: """Initialize the Demo select entity.""" - self._attr_current_version = current_version + self._attr_installed_version = installed_version self._attr_device_class = device_class self._attr_latest_version = latest_version self._attr_name = name or DEVICE_DEFAULT_NAME @@ -149,7 +149,7 @@ class DemoUpdate(UpdateEntity): await _fake_install() self._attr_in_progress = False - self._attr_current_version = ( + self._attr_installed_version = ( version if version is not None else self.latest_version ) self.async_write_ha_state() diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index d2953a0f801..8af3d88088a 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -116,8 +116,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): return self._addon_data[ATTR_VERSION_LATEST] @property - def current_version(self) -> str | None: - """Version currently in use.""" + def installed_version(self) -> str | None: + """Version installed and in use.""" return self._addon_data[ATTR_VERSION] @property @@ -139,9 +139,12 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): if (notes := self._addon_data[ATTR_CHANGELOG]) is None: return None - if f"# {self.latest_version}" in notes and f"# {self.current_version}" in notes: + if ( + f"# {self.latest_version}" in notes + and f"# {self.installed_version}" in notes + ): # Split the release notes to only what is between the versions if we can - new_notes = notes.split(f"# {self.current_version}")[0] + new_notes = notes.split(f"# {self.installed_version}")[0] if f"# {self.latest_version}" in new_notes: # Make sure the latest version is still there. # This can be False if the order of the release notes are not correct @@ -182,7 +185,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST] @property - def current_version(self) -> str: + def installed_version(self) -> str: """Return native value of entity.""" return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION] @@ -226,7 +229,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST] @property - def current_version(self) -> str: + def installed_version(self) -> str: """Return native value of entity.""" return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION] @@ -271,7 +274,7 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity): return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST] @property - def current_version(self) -> str: + def installed_version(self) -> str: """Return native value of entity.""" return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION] diff --git a/homeassistant/components/pi_hole/update.py b/homeassistant/components/pi_hole/update.py index 14e6761f7c5..6bb424f15b1 100644 --- a/homeassistant/components/pi_hole/update.py +++ b/homeassistant/components/pi_hole/update.py @@ -22,7 +22,7 @@ from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN class PiHoleUpdateEntityDescription(UpdateEntityDescription): """Describes PiHole update entity.""" - current_version: Callable[[dict], str | None] = lambda api: None + installed_version: Callable[[dict], str | None] = lambda api: None latest_version: Callable[[dict], str | None] = lambda api: None release_base_url: str | None = None title: str | None = None @@ -34,7 +34,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( name="Core Update Available", title="Pi-hole Core", entity_category=EntityCategory.DIAGNOSTIC, - current_version=lambda versions: versions.get("core_current"), + installed_version=lambda versions: versions.get("core_current"), latest_version=lambda versions: versions.get("core_latest"), release_base_url="https://github.com/pi-hole/pi-hole/releases/tag", ), @@ -43,7 +43,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( name="Web Update Available", title="Pi-hole Web interface", entity_category=EntityCategory.DIAGNOSTIC, - current_version=lambda versions: versions.get("web_current"), + installed_version=lambda versions: versions.get("web_current"), latest_version=lambda versions: versions.get("web_latest"), release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag", ), @@ -52,7 +52,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( name="FTL Update Available", title="Pi-hole FTL DNS", entity_category=EntityCategory.DIAGNOSTIC, - current_version=lambda versions: versions.get("FTL_current"), + installed_version=lambda versions: versions.get("FTL_current"), latest_version=lambda versions: versions.get("FTL_latest"), release_base_url="https://github.com/pi-hole/FTL/releases/tag", ), @@ -100,10 +100,10 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity): self._attr_title = description.title @property - def current_version(self) -> str | None: - """Version currently in use.""" + def installed_version(self) -> str | None: + """Version installed and in use.""" if isinstance(self.api.versions, dict): - return self.entity_description.current_version(self.api.versions) + return self.entity_description.installed_version(self.api.versions) return None @property diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index a4a958b8c1a..836468d6f50 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -55,15 +55,15 @@ class SynoDSMUpdateEntity(SynologyDSMBaseEntity, UpdateEntity): _attr_title = "Synology DSM" @property - def current_version(self) -> str | None: - """Version currently in use.""" + def installed_version(self) -> str | None: + """Version installed and in use.""" return self._api.information.version_string # type: ignore[no-any-return] @property def latest_version(self) -> str | None: """Latest version available for install.""" if not self._api.upgrade.update_available: - return self.current_version + return self.installed_version return self._api.upgrade.available_version # type: ignore[no-any-return] @property diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 6b30f45b023..6d4b36e3411 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -28,8 +28,8 @@ from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_AUTO_UPDATE, ATTR_BACKUP, - ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, @@ -117,7 +117,8 @@ async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None """Service call wrapper to validate the call.""" # If version is not specified, but no update is available. if (version := service_call.data.get(ATTR_VERSION)) is None and ( - entity.current_version == entity.latest_version or entity.latest_version is None + entity.installed_version == entity.latest_version + or entity.latest_version is None ): raise HomeAssistantError(f"No update available for {entity.name}") @@ -165,7 +166,7 @@ class UpdateEntity(RestoreEntity): entity_description: UpdateEntityDescription _attr_auto_update: bool = False - _attr_current_version: str | None = None + _attr_installed_version: str | None = None _attr_device_class: UpdateDeviceClass | str | None _attr_in_progress: bool | int = False _attr_latest_version: str | None = None @@ -183,9 +184,9 @@ class UpdateEntity(RestoreEntity): return self._attr_auto_update @property - def current_version(self) -> str | None: - """Version currently in use.""" - return self._attr_current_version + def installed_version(self) -> str | None: + """Version installed and in use.""" + return self._attr_installed_version @property def device_class(self) -> UpdateDeviceClass | str | None: @@ -256,7 +257,7 @@ class UpdateEntity(RestoreEntity): """Skip the current offered version to update.""" if (latest_version := self.latest_version) is None: raise HomeAssistantError(f"Cannot skip an unknown version for {self.name}") - if self.current_version == latest_version: + if self.installed_version == latest_version: raise HomeAssistantError(f"No update available to skip for {self.name}") self.__skipped_version = latest_version self.async_write_ha_state() @@ -305,7 +306,7 @@ class UpdateEntity(RestoreEntity): @final def state(self) -> str | None: """Return the entity state.""" - if (current_version := self.current_version) is None or ( + if (installed_version := self.installed_version) is None or ( latest_version := self.latest_version ) is None: return None @@ -314,11 +315,11 @@ class UpdateEntity(RestoreEntity): return STATE_OFF try: - newer = AwesomeVersion(latest_version) > current_version + newer = AwesomeVersion(latest_version) > installed_version return STATE_ON if newer else STATE_OFF except AwesomeVersionCompareException: # Can't compare versions, fallback to exact match - return STATE_OFF if latest_version == current_version else STATE_ON + return STATE_OFF if latest_version == installed_version else STATE_ON @final @property @@ -334,17 +335,17 @@ class UpdateEntity(RestoreEntity): else: in_progress = self.__in_progress - # Clear skipped version in case it matches the current version or - # the latest version diverged. + # Clear skipped version in case it matches the current installed + # version or the latest version diverged. if ( - self.__skipped_version == self.current_version + self.__skipped_version == self.installed_version or self.__skipped_version != self.latest_version ): self.__skipped_version = None return { ATTR_AUTO_UPDATE: self.auto_update, - ATTR_CURRENT_VERSION: self.current_version, + ATTR_INSTALLED_VERSION: self.installed_version, ATTR_IN_PROGRESS: in_progress, ATTR_LATEST_VERSION: self.latest_version, ATTR_RELEASE_SUMMARY: release_summary, diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py index 916d2cbaceb..aec3c183a63 100644 --- a/homeassistant/components/update/const.py +++ b/homeassistant/components/update/const.py @@ -22,7 +22,7 @@ SERVICE_SKIP: Final = "skip" ATTR_AUTO_UPDATE: Final = "auto_update" ATTR_BACKUP: Final = "backup" -ATTR_CURRENT_VERSION: Final = "current_version" +ATTR_INSTALLED_VERSION: Final = "installed_version" ATTR_IN_PROGRESS: Final = "in_progress" ATTR_LATEST_VERSION: Final = "latest_version" ATTR_RELEASE_SUMMARY: Final = "release_summary" diff --git a/homeassistant/components/update/significant_change.py b/homeassistant/components/update/significant_change.py index 400734f2e43..8b37d227a1f 100644 --- a/homeassistant/components/update/significant_change.py +++ b/homeassistant/components/update/significant_change.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.core import HomeAssistant, callback -from .const import ATTR_CURRENT_VERSION, ATTR_LATEST_VERSION +from .const import ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION @callback @@ -21,7 +21,7 @@ def async_check_significant_change( if old_state != new_state: return True - if old_attrs.get(ATTR_CURRENT_VERSION) != new_attrs.get(ATTR_CURRENT_VERSION): + if old_attrs.get(ATTR_INSTALLED_VERSION) != new_attrs.get(ATTR_INSTALLED_VERSION): return True if old_attrs.get(ATTR_LATEST_VERSION) != new_attrs.get(ATTR_LATEST_VERSION): diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index 098f2ca3831..f0fc532b3b3 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -44,8 +44,8 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity): self._attr_unique_id = coordinator.data.info.mac_address @property - def current_version(self) -> str | None: - """Version currently in use.""" + def installed_version(self) -> str | None: + """Version currently installed and in use.""" if (version := self.coordinator.data.info.version) is None: return None return str(version) diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py index 37e0ea903d1..ebee402049c 100644 --- a/tests/components/demo/test_update.py +++ b/tests/components/demo/test_update.py @@ -5,8 +5,8 @@ import pytest from homeassistant.components.update import DOMAIN, SERVICE_INSTALL, UpdateDeviceClass from homeassistant.components.update.const import ( - ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, @@ -31,7 +31,7 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state assert state.state == STATE_ON assert state.attributes[ATTR_TITLE] == "Awesomesoft Inc." - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert ( state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!" @@ -42,7 +42,7 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state assert state.state == STATE_OFF assert state.attributes[ATTR_TITLE] == "AdGuard Home" - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" assert state.attributes[ATTR_RELEASE_SUMMARY] is None assert state.attributes[ATTR_RELEASE_URL] is None @@ -51,7 +51,7 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state assert state.state == STATE_ON assert state.attributes[ATTR_TITLE] == "AdGuard Home" - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert ( state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!" @@ -62,7 +62,7 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state assert state.state == STATE_ON assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware" - assert state.attributes[ATTR_CURRENT_VERSION] == "1.93.3" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.93.3" assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2" assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects" assert ( @@ -74,7 +74,7 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state assert state.state == STATE_ON assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware" - assert state.attributes[ATTR_CURRENT_VERSION] == "1.93.3" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.93.3" assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2" assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects" assert ( diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py index 375104a9ce0..8da4ec263d5 100644 --- a/tests/components/pi_hole/test_update.py +++ b/tests/components/pi_hole/test_update.py @@ -20,7 +20,7 @@ async def test_update(hass): state = hass.states.get("update.pi_hole_core_update_available") assert state.name == "Pi-Hole Core Update Available" assert state.state == STATE_ON - assert state.attributes["current_version"] == "v5.5" + assert state.attributes["installed_version"] == "v5.5" assert state.attributes["latest_version"] == "v5.6" assert ( state.attributes["release_url"] @@ -30,7 +30,7 @@ async def test_update(hass): state = hass.states.get("update.pi_hole_ftl_update_available") assert state.name == "Pi-Hole FTL Update Available" assert state.state == STATE_ON - assert state.attributes["current_version"] == "v5.10" + assert state.attributes["installed_version"] == "v5.10" assert state.attributes["latest_version"] == "v5.11" assert ( state.attributes["release_url"] @@ -40,7 +40,7 @@ async def test_update(hass): state = hass.states.get("update.pi_hole_web_update_available") assert state.name == "Pi-Hole Web Update Available" assert state.state == STATE_ON - assert state.attributes["current_version"] == "v5.7" + assert state.attributes["installed_version"] == "v5.7" assert state.attributes["latest_version"] == "v5.8" assert ( state.attributes["release_url"] @@ -61,20 +61,20 @@ async def test_update_no_versions(hass): state = hass.states.get("update.pi_hole_core_update_available") assert state.name == "Pi-Hole Core Update Available" assert state.state == STATE_UNKNOWN - assert state.attributes["current_version"] is None + assert state.attributes["installed_version"] is None assert state.attributes["latest_version"] is None assert state.attributes["release_url"] is None state = hass.states.get("update.pi_hole_ftl_update_available") assert state.name == "Pi-Hole FTL Update Available" assert state.state == STATE_UNKNOWN - assert state.attributes["current_version"] is None + assert state.attributes["installed_version"] is None assert state.attributes["latest_version"] is None assert state.attributes["release_url"] is None state = hass.states.get("update.pi_hole_web_update_available") assert state.name == "Pi-Hole Web Update Available" assert state.state == STATE_UNKNOWN - assert state.attributes["current_version"] is None + assert state.attributes["installed_version"] is None assert state.attributes["latest_version"] is None assert state.attributes["release_url"] is None diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index b8db7f24f4a..3a40b51cca2 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -17,8 +17,8 @@ from homeassistant.components.update import ( ) from homeassistant.components.update.const import ( ATTR_AUTO_UPDATE, - ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, @@ -51,14 +51,14 @@ async def test_update(hass: HomeAssistant) -> None: update = MockUpdateEntity() update.hass = hass - update._attr_current_version = "1.0.0" + update._attr_installed_version = "1.0.0" update._attr_latest_version = "1.0.1" update._attr_release_summary = "Summary" update._attr_release_url = "https://example.com" update._attr_title = "Title" assert update.entity_category is EntityCategory.DIAGNOSTIC - assert update.current_version == "1.0.0" + assert update.installed_version == "1.0.0" assert update.latest_version == "1.0.1" assert update.release_summary == "Summary" assert update.release_url == "https://example.com" @@ -67,7 +67,7 @@ async def test_update(hass: HomeAssistant) -> None: assert update.state == STATE_ON assert update.state_attributes == { ATTR_AUTO_UPDATE: False, - ATTR_CURRENT_VERSION: "1.0.0", + ATTR_INSTALLED_VERSION: "1.0.0", ATTR_IN_PROGRESS: False, ATTR_LATEST_VERSION: "1.0.1", ATTR_RELEASE_SUMMARY: "Summary", @@ -77,27 +77,27 @@ async def test_update(hass: HomeAssistant) -> None: } # Test no update available - update._attr_current_version = "1.0.0" + update._attr_installed_version = "1.0.0" update._attr_latest_version = "1.0.0" assert update.state is STATE_OFF - # Test state becomes unknown if current version is unknown - update._attr_current_version = None + # Test state becomes unknown if installed version is unknown + update._attr_installed_version = None update._attr_latest_version = "1.0.0" assert update.state is None # Test state becomes unknown if latest version is unknown - update._attr_current_version = "1.0.0" + update._attr_installed_version = "1.0.0" update._attr_latest_version = None assert update.state is None # Test no update if new version is not an update - update._attr_current_version = "1.0.0" + update._attr_installed_version = "1.0.0" update._attr_latest_version = "0.9.0" assert update.state is STATE_OFF # Test update if new version is not considered a valid version - update._attr_current_version = "1.0.0" + update._attr_installed_version = "1.0.0" update._attr_latest_version = "awesome_update" assert update.state is STATE_ON @@ -159,7 +159,7 @@ async def test_entity_with_no_install( state = hass.states.get("update.update_no_install") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" # Should not be able to install as the entity doesn't support that @@ -175,7 +175,7 @@ async def test_entity_with_no_install( state = hass.states.get("update.update_no_install") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] is None @@ -190,7 +190,7 @@ async def test_entity_with_no_install( state = hass.states.get("update.update_no_install") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" @@ -210,7 +210,7 @@ async def test_entity_with_no_updates( state = hass.states.get("update.no_update") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" # Should not be able to skip when there is no update available @@ -259,7 +259,7 @@ async def test_entity_with_auto_update( state = hass.states.get("update.update_with_auto_update") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] is None @@ -300,7 +300,7 @@ async def test_entity_with_updates_available( state = hass.states.get("update.update_available") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] is None @@ -316,7 +316,7 @@ async def test_entity_with_updates_available( state = hass.states.get("update.update_available") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" @@ -332,7 +332,7 @@ async def test_entity_with_updates_available( state = hass.states.get("update.update_available") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.1" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.1" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] is None assert "Installed latest update" in caplog.text @@ -353,7 +353,7 @@ async def test_entity_with_unknown_version( state = hass.states.get("update.update_unknown") assert state assert state.state == STATE_UNKNOWN - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] is None assert state.attributes[ATTR_SKIPPED_VERSION] is None @@ -391,7 +391,7 @@ async def test_entity_with_specific_version( state = hass.states.get("update.update_specific_version") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" # Update to a specific version @@ -406,7 +406,7 @@ async def test_entity_with_specific_version( state = hass.states.get("update.update_specific_version") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "0.9.9" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.9.9" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" assert "Installed update with version: 0.9.9" in caplog.text @@ -421,7 +421,7 @@ async def test_entity_with_specific_version( state = hass.states.get("update.update_specific_version") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" assert "Installed latest update" in caplog.text @@ -455,7 +455,7 @@ async def test_entity_with_backup_support( state = hass.states.get("update.update_backup") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" # Without a backup @@ -472,7 +472,7 @@ async def test_entity_with_backup_support( state = hass.states.get("update.update_backup") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.1" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.1" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert "Creating backup before installing update" not in caplog.text assert "Installed latest update" in caplog.text @@ -493,7 +493,7 @@ async def test_entity_with_backup_support( state = hass.states.get("update.update_backup") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "0.9.8" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.9.8" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert "Creating backup before installing update" in caplog.text assert "Installed update with version: 0.9.8" in caplog.text @@ -514,7 +514,7 @@ async def test_entity_already_in_progress( state = hass.states.get("update.update_already_in_progress") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_IN_PROGRESS] == 50 @@ -559,14 +559,14 @@ async def test_entity_without_progress_support( assert len(events) == 2 assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False - assert events[0].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[0].data.get("old_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert events[0].data.get("new_state").attributes[ATTR_IN_PROGRESS] is True - assert events[0].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[0].data.get("new_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert events[1].data.get("old_state").attributes[ATTR_IN_PROGRESS] is True - assert events[1].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[1].data.get("old_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert events[1].data.get("new_state").attributes[ATTR_IN_PROGRESS] is False - assert events[1].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.1" + assert events[1].data.get("new_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.1" async def test_entity_without_progress_support_raising( @@ -602,14 +602,14 @@ async def test_entity_without_progress_support_raising( assert len(events) == 2 assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False - assert events[0].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[0].data.get("old_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert events[0].data.get("new_state").attributes[ATTR_IN_PROGRESS] is True - assert events[0].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[0].data.get("new_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert events[1].data.get("old_state").attributes[ATTR_IN_PROGRESS] is True - assert events[1].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[1].data.get("old_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert events[1].data.get("new_state").attributes[ATTR_IN_PROGRESS] is False - assert events[1].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[1].data.get("new_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" async def test_restore_state( @@ -638,7 +638,7 @@ async def test_restore_state( state = hass.states.get("update.update_available") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_INSTALLED_VERSION] == "1.0.0" assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" diff --git a/tests/components/update/test_recorder.py b/tests/components/update/test_recorder.py index 17ab7445b4b..6d4ec567182 100644 --- a/tests/components/update/test_recorder.py +++ b/tests/components/update/test_recorder.py @@ -6,8 +6,8 @@ from datetime import timedelta from homeassistant.components.recorder.models import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.update.const import ( - ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, ATTR_RELEASE_SUMMARY, DOMAIN, ) @@ -52,4 +52,4 @@ async def test_exclude_attributes( for state in states: assert ATTR_IN_PROGRESS not in state.attributes assert ATTR_RELEASE_SUMMARY not in state.attributes - assert ATTR_CURRENT_VERSION in state.attributes + assert ATTR_INSTALLED_VERSION in state.attributes diff --git a/tests/components/update/test_significant_change.py b/tests/components/update/test_significant_change.py index 699d3e60f57..35e601f4789 100644 --- a/tests/components/update/test_significant_change.py +++ b/tests/components/update/test_significant_change.py @@ -1,7 +1,7 @@ """Test the update significant change platform.""" from homeassistant.components.update.const import ( - ATTR_CURRENT_VERSION, ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, @@ -23,7 +23,7 @@ async def test_significant_change(hass: HomeAssistant) -> None: assert not async_check_significant_change(hass, STATE_ON, {}, STATE_ON, {}) attrs = { - ATTR_CURRENT_VERSION: "1.0.0", + ATTR_INSTALLED_VERSION: "1.0.0", ATTR_IN_PROGRESS: False, ATTR_LATEST_VERSION: "1.0.1", ATTR_RELEASE_SUMMARY: "Fixes!", @@ -38,7 +38,7 @@ async def test_significant_change(hass: HomeAssistant) -> None: STATE_ON, attrs, STATE_ON, - attrs.copy() | {ATTR_CURRENT_VERSION: "1.0.1"}, + attrs.copy() | {ATTR_INSTALLED_VERSION: "1.0.1"}, ) assert async_check_significant_change( diff --git a/tests/components/wled/test_update.py b/tests/components/wled/test_update.py index fea6180048e..452aeba6a80 100644 --- a/tests/components/wled/test_update.py +++ b/tests/components/wled/test_update.py @@ -11,7 +11,7 @@ from homeassistant.components.update import ( UpdateEntityFeature, ) from homeassistant.components.update.const import ( - ATTR_CURRENT_VERSION, + ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_SUMMARY, ATTR_RELEASE_URL, @@ -46,7 +46,7 @@ async def test_update_available( assert state assert state.attributes.get(ATTR_DEVICE_CLASS) == UpdateDeviceClass.FIRMWARE assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.5" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.8.5" assert state.attributes[ATTR_LATEST_VERSION] == "0.12.0" assert state.attributes[ATTR_RELEASE_SUMMARY] is None assert ( @@ -79,7 +79,7 @@ async def test_update_information_available( assert state assert state.attributes.get(ATTR_DEVICE_CLASS) == UpdateDeviceClass.FIRMWARE assert state.state == STATE_UNKNOWN - assert state.attributes[ATTR_CURRENT_VERSION] is None + assert state.attributes[ATTR_INSTALLED_VERSION] is None assert state.attributes[ATTR_LATEST_VERSION] is None assert state.attributes[ATTR_RELEASE_SUMMARY] is None assert state.attributes[ATTR_RELEASE_URL] is None @@ -110,7 +110,7 @@ async def test_no_update_available( assert state assert state.state == STATE_OFF assert state.attributes.get(ATTR_DEVICE_CLASS) == UpdateDeviceClass.FIRMWARE - assert state.attributes[ATTR_CURRENT_VERSION] == "0.12.0-b2" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.12.0-b2" assert state.attributes[ATTR_LATEST_VERSION] == "0.12.0-b2" assert state.attributes[ATTR_RELEASE_SUMMARY] is None assert ( @@ -169,7 +169,7 @@ async def test_update_stay_stable( state = hass.states.get("update.wled_rgb_light_firmware") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.5" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.8.5" assert state.attributes[ATTR_LATEST_VERSION] == "0.12.0" await hass.services.async_call( @@ -198,7 +198,7 @@ async def test_update_beta_to_stable( state = hass.states.get("update.wled_rgbw_light_firmware") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.6b4" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.8.6b4" assert state.attributes[ATTR_LATEST_VERSION] == "0.8.6" await hass.services.async_call( @@ -226,7 +226,7 @@ async def test_update_stay_beta( state = hass.states.get("update.wled_rgb_light_firmware") assert state assert state.state == STATE_ON - assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.6b1" + assert state.attributes[ATTR_INSTALLED_VERSION] == "0.8.6b1" assert state.attributes[ATTR_LATEST_VERSION] == "0.8.6b2" await hass.services.async_call( diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py index fc5ee31246e..1840f076407 100644 --- a/tests/testing_config/custom_components/test/update.py +++ b/tests/testing_config/custom_components/test/update.py @@ -26,9 +26,9 @@ class MockUpdateEntity(MockEntity, UpdateEntity): return self._handle("auto_update") @property - def current_version(self) -> str | None: - """Version currently in use.""" - return self._handle("current_version") + def installed_version(self) -> str | None: + """Version currently installed and in use.""" + return self._handle("installed_version") @property def in_progress(self) -> bool | int | None: @@ -61,10 +61,10 @@ class MockUpdateEntity(MockEntity, UpdateEntity): _LOGGER.info("Creating backup before installing update") if version is not None: - self._values["current_version"] = version + self._values["installed_version"] = version _LOGGER.info(f"Installed update with version: {version}") else: - self._values["current_version"] = self.latest_version + self._values["installed_version"] = self.latest_version _LOGGER.info("Installed latest update") def release_notes(self) -> str | None: @@ -83,28 +83,28 @@ def init(empty=False): MockUpdateEntity( name="No Update", unique_id="no_update", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.0", supported_features=UpdateEntityFeature.INSTALL, ), MockUpdateEntity( name="Update Available", unique_id="update_available", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", supported_features=UpdateEntityFeature.INSTALL, ), MockUpdateEntity( name="Update Unknown", unique_id="update_unknown", - current_version="1.0.0", + installed_version="1.0.0", latest_version=None, supported_features=UpdateEntityFeature.INSTALL, ), MockUpdateEntity( name="Update Specific Version", unique_id="update_specific_version", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.0", supported_features=UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION, @@ -112,7 +112,7 @@ def init(empty=False): MockUpdateEntity( name="Update Backup", unique_id="update_backup", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", supported_features=UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION @@ -121,7 +121,7 @@ def init(empty=False): MockUpdateEntity( name="Update Already in Progress", unique_id="update_already_in_progres", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", in_progress=50, supported_features=UpdateEntityFeature.INSTALL @@ -130,20 +130,20 @@ def init(empty=False): MockUpdateEntity( name="Update No Install", unique_id="no_install", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", ), MockUpdateEntity( name="Update with release notes", unique_id="with_release_notes", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", supported_features=UpdateEntityFeature.RELEASE_NOTES, ), MockUpdateEntity( name="Update with auto update", unique_id="with_auto_update", - current_version="1.0.0", + installed_version="1.0.0", latest_version="1.0.1", auto_update=True, supported_features=UpdateEntityFeature.INSTALL, From cb437449ddb160b8154917237fc09e195923c303 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 1 Apr 2022 20:26:18 +0200 Subject: [PATCH 0093/1224] Update frontend to 20220401.0 (#69095) --- 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 06b46f97fb7..43bea9503c8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220330.0"], + "requirements": ["home-assistant-frontend==20220401.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 02979392ade..4cc7fc74737 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220330.0 +home-assistant-frontend==20220401.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 6609a4e23d4..1494341eba0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -812,7 +812,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220330.0 +home-assistant-frontend==20220401.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8202075728e..33bf00e9e8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220330.0 +home-assistant-frontend==20220401.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 4f49939bc0310ed190d30dbe9fc4f02281c3f27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Apr 2022 20:51:20 +0200 Subject: [PATCH 0094/1224] Remove deprecated DEVICE_CLASS_* and STATE_CLASS_* from Airzone (#69096) --- .../components/airzone/binary_sensor.py | 9 ++++----- homeassistant/components/airzone/sensor.py | 18 +++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index 1d0c76906e8..79877d1cbbd 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -15,8 +15,7 @@ from aioairzone.const import ( ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_RUNNING, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -39,12 +38,12 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription): BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( AirzoneBinarySensorEntityDescription( - device_class=DEVICE_CLASS_RUNNING, + device_class=BinarySensorDeviceClass.RUNNING, key=AZD_AIR_DEMAND, name="Air Demand", ), AirzoneBinarySensorEntityDescription( - device_class=DEVICE_CLASS_RUNNING, + device_class=BinarySensorDeviceClass.RUNNING, key=AZD_FLOOR_DEMAND, name="Floor Demand", ), @@ -52,7 +51,7 @@ BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( attributes={ "errors": AZD_ERRORS, }, - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, key=AZD_PROBLEMS, name="Problem", diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index 931e74a495d..b40f306bf02 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -6,17 +6,13 @@ from typing import Any, Final from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_TEMP_UNIT, AZD_ZONES from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,18 +22,18 @@ from .coordinator import AirzoneUpdateCoordinator SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, key=AZD_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, key=AZD_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 9902ecb4176a2bacc379dbcce4d1ce511e3b97c1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 1 Apr 2022 20:53:09 +0200 Subject: [PATCH 0095/1224] Remove use of HVAC_MODE_OFF in plugwise climate, it's not implemented (#69094) --- homeassistant/components/plugwise/climate.py | 5 ++--- tests/components/plugwise/test_climate.py | 4 ---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 12a203b6896..1e4b972e4d0 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -12,7 +12,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -64,7 +63,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self._attr_preset_modes = list(presets) # Determine hvac modes and current hvac mode - self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._attr_hvac_modes = [HVAC_MODE_HEAT] if self.coordinator.data.gateway.get("cooling_present"): self._attr_hvac_modes.append(HVAC_MODE_COOL) if self.device.get("available_schedules") != ["None"]: @@ -90,7 +89,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): def hvac_mode(self) -> str: """Return HVAC operation ie. heat, cool mode.""" if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes: - return HVAC_MODE_OFF + return HVAC_MODE_HEAT return mode @property diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index a52e4a955a6..a695906c0af 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -9,7 +9,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_OFF, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -26,7 +25,6 @@ async def test_adam_climate_entity_attributes( assert state assert state.attributes["hvac_modes"] == [ HVAC_MODE_HEAT, - HVAC_MODE_OFF, HVAC_MODE_AUTO, ] @@ -47,7 +45,6 @@ async def test_adam_climate_entity_attributes( assert state.attributes["hvac_modes"] == [ HVAC_MODE_HEAT, - HVAC_MODE_OFF, HVAC_MODE_AUTO, ] @@ -161,7 +158,6 @@ async def test_anna_climate_entity_attributes( assert state.state == HVAC_MODE_HEAT assert state.attributes["hvac_modes"] == [ HVAC_MODE_HEAT, - HVAC_MODE_OFF, HVAC_MODE_COOL, ] assert "no_frost" in state.attributes["preset_modes"] From 853923c30a3ca65b417eea168eb7692772cb19d8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Apr 2022 20:53:38 +0200 Subject: [PATCH 0096/1224] Add EntityFeature enum to Fan (#69091) --- homeassistant/components/demo/fan.py | 20 +++++------ homeassistant/components/fan/__init__.py | 44 +++++++++++++++--------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 8fcc6a810ed..06b19e90090 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,13 +1,7 @@ """Demo fan platform that has a fake fan.""" from __future__ import annotations -from homeassistant.components.fan import ( - SUPPORT_DIRECTION, - SUPPORT_OSCILLATE, - SUPPORT_PRESET_MODE, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,8 +12,10 @@ PRESET_MODE_SMART = "smart" PRESET_MODE_SLEEP = "sleep" PRESET_MODE_ON = "on" -FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION -LIMITED_SUPPORT = SUPPORT_SET_SPEED +FULL_SUPPORT = ( + FanEntityFeature.SET_SPEED | FanEntityFeature.OSCILLATE | FanEntityFeature.DIRECTION +) +LIMITED_SUPPORT = FanEntityFeature.SET_SPEED async def async_setup_platform( @@ -78,7 +74,7 @@ async def async_setup_platform( hass, "fan5", "Preset Only Limited Fan", - SUPPORT_PRESET_MODE, + FanEntityFeature.PRESET_MODE, [ PRESET_MODE_AUTO, PRESET_MODE_SMART, @@ -120,9 +116,9 @@ class BaseDemoFan(FanEntity): self._oscillating: bool | None = None self._direction: str | None = None self._name = name - if supported_features & SUPPORT_OSCILLATE: + if supported_features & FanEntityFeature.OSCILLATE: self._oscillating = False - if supported_features & SUPPORT_DIRECTION: + if supported_features & FanEntityFeature.DIRECTION: self._direction = "forward" @property diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 89de0cabb10..5eb3bedabd5 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta +from enum import IntEnum import functools as ft import logging import math @@ -39,7 +40,18 @@ SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" -# Bitfield of features supported by the fan entity + +class FanEntityFeature(IntEnum): + """Supported features of the fan entity.""" + + SET_SPEED = 1 + OSCILLATE = 2 + DIRECTION = 4 + PRESET_MODE = 8 + + +# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. +# Please use the FanEntityFeature enum instead. SUPPORT_SET_SPEED = 1 SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 @@ -103,7 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) }, "async_increase_speed", - [SUPPORT_SET_SPEED], + [FanEntityFeature.SET_SPEED], ) component.async_register_entity_service( SERVICE_DECREASE_SPEED, @@ -113,19 +125,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) }, "async_decrease_speed", - [SUPPORT_SET_SPEED], + [FanEntityFeature.SET_SPEED], ) component.async_register_entity_service( SERVICE_OSCILLATE, {vol.Required(ATTR_OSCILLATING): cv.boolean}, "async_oscillate", - [SUPPORT_OSCILLATE], + [FanEntityFeature.OSCILLATE], ) component.async_register_entity_service( SERVICE_SET_DIRECTION, {vol.Optional(ATTR_DIRECTION): cv.string}, "async_set_direction", - [SUPPORT_DIRECTION], + [FanEntityFeature.DIRECTION], ) component.async_register_entity_service( SERVICE_SET_PERCENTAGE, @@ -135,13 +147,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) }, "async_set_percentage", - [SUPPORT_SET_SPEED], + [FanEntityFeature.SET_SPEED], ) component.async_register_entity_service( SERVICE_SET_PRESET_MODE, {vol.Required(ATTR_PRESET_MODE): cv.string}, "async_set_preset_mode", - [SUPPORT_SET_SPEED, SUPPORT_PRESET_MODE], + [FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE], ) return True @@ -314,8 +326,8 @@ class FanEntity(ToggleEntity): attrs = {} if ( - self.supported_features & SUPPORT_SET_SPEED - or self.supported_features & SUPPORT_PRESET_MODE + self.supported_features & FanEntityFeature.SET_SPEED + or self.supported_features & FanEntityFeature.PRESET_MODE ): attrs[ATTR_PRESET_MODES] = self.preset_modes @@ -328,19 +340,19 @@ class FanEntity(ToggleEntity): data: dict[str, float | str | None] = {} supported_features = self.supported_features - if supported_features & SUPPORT_DIRECTION: + if supported_features & FanEntityFeature.DIRECTION: data[ATTR_DIRECTION] = self.current_direction - if supported_features & SUPPORT_OSCILLATE: + if supported_features & FanEntityFeature.OSCILLATE: data[ATTR_OSCILLATING] = self.oscillating - if supported_features & SUPPORT_SET_SPEED: + if supported_features & FanEntityFeature.SET_SPEED: data[ATTR_PERCENTAGE] = self.percentage data[ATTR_PERCENTAGE_STEP] = self.percentage_step if ( - supported_features & SUPPORT_PRESET_MODE - or supported_features & SUPPORT_SET_SPEED + supported_features & FanEntityFeature.PRESET_MODE + or supported_features & FanEntityFeature.SET_SPEED ): data[ATTR_PRESET_MODE] = self.preset_mode @@ -355,7 +367,7 @@ class FanEntity(ToggleEntity): def preset_mode(self) -> str | None: """Return the current preset mode, e.g., auto, smart, interval, favorite. - Requires SUPPORT_SET_SPEED. + Requires FanEntityFeature.SET_SPEED. """ if hasattr(self, "_attr_preset_mode"): return self._attr_preset_mode @@ -365,7 +377,7 @@ class FanEntity(ToggleEntity): def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. - Requires SUPPORT_SET_SPEED. + Requires FanEntityFeature.SET_SPEED. """ if hasattr(self, "_attr_preset_modes"): return self._attr_preset_modes From 4b5996c5ed76ffc1046445485bcd643f05f41c17 Mon Sep 17 00:00:00 2001 From: Patrik Lindgren <21142447+ggravlingen@users.noreply.github.com> Date: Fri, 1 Apr 2022 23:26:35 +0200 Subject: [PATCH 0097/1224] Drop support for Tradfri groups and YAML configuration (#68033) * Drop support for Tradfri groups * Remove context * Remove async_setup * Mark removed * Pass generator expression --- homeassistant/components/tradfri/__init__.py | 86 +------------ homeassistant/components/tradfri/const.py | 8 +- .../components/tradfri/coordinator.py | 45 ------- .../components/tradfri/diagnostics.py | 3 +- homeassistant/components/tradfri/light.py | 78 +----------- tests/components/tradfri/common.py | 1 - tests/components/tradfri/test_config_flow.py | 120 ------------------ tests/components/tradfri/test_diagnostics.py | 7 - tests/components/tradfri/test_init.py | 2 - tests/components/tradfri/test_light.py | 89 ------------- 10 files changed, 8 insertions(+), 431 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 0054b1d7bff..26637f500ac 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -9,10 +9,7 @@ from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory from pytradfri.command import Command from pytradfri.device import Device -from pytradfri.group import Group -import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -24,82 +21,30 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_TRADFRI_GATEWAY, ATTR_TRADFRI_GATEWAY_MODEL, ATTR_TRADFRI_MANUFACTURER, - CONF_ALLOW_TRADFRI_GROUPS, CONF_GATEWAY_ID, CONF_IDENTITY, - CONF_IMPORT_GROUPS, CONF_KEY, COORDINATOR, COORDINATOR_LIST, - DEFAULT_ALLOW_TRADFRI_GROUPS, DOMAIN, - GROUPS_LIST, KEY_API, PLATFORMS, SIGNAL_GW, TIMEOUT_API, ) -from .coordinator import ( - TradfriDeviceDataUpdateCoordinator, - TradfriGroupDataUpdateCoordinator, -) +from .coordinator import TradfriDeviceDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) FACTORY = "tradfri_factory" LISTENERS = "tradfri_listeners" -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - vol.All( - cv.deprecated(CONF_HOST), - cv.deprecated( - CONF_ALLOW_TRADFRI_GROUPS, - ), - { - vol.Optional(CONF_HOST): cv.string, - vol.Optional( - CONF_ALLOW_TRADFRI_GROUPS, default=DEFAULT_ALLOW_TRADFRI_GROUPS - ): cv.boolean, - }, - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Tradfri component.""" - if (conf := config.get(DOMAIN)) is None: - return True - - configured_hosts = [ - entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN) - ] - - host = conf.get(CONF_HOST) - import_groups = conf[CONF_ALLOW_TRADFRI_GROUPS] - - if host is None or host in configured_hosts: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: host, CONF_IMPORT_GROUPS: import_groups}, - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry( @@ -127,7 +72,6 @@ async def async_setup_entry( api = factory.request gateway = Gateway() - groups: list[Group] = [] try: gateway_info = await api(gateway.get_gateway_info(), timeout=TIMEOUT_API) @@ -136,19 +80,6 @@ async def async_setup_entry( ) devices: list[Device] = await api(devices_commands, timeout=TIMEOUT_API) - if entry.data[CONF_IMPORT_GROUPS]: - # Note: we should update this page when deprecating: - # https://www.home-assistant.io/integrations/tradfri/ - _LOGGER.warning( - "Importing of Tradfri groups has been deprecated due to stability issues " - "and will be removed in Home Assistant core 2022.5" - ) - # No need to load groups if the user hasn't requested it - groups_commands: Command = await api( - gateway.get_groups(), timeout=TIMEOUT_API - ) - groups = await api(groups_commands, timeout=TIMEOUT_API) - except RequestError as exc: await factory.shutdown() raise ConfigEntryNotReady from exc @@ -172,7 +103,6 @@ async def async_setup_entry( CONF_GATEWAY_ID: gateway, KEY_API: api, COORDINATOR_LIST: [], - GROUPS_LIST: [], } for device in devices: @@ -186,18 +116,6 @@ async def async_setup_entry( ) coordinator_data[COORDINATOR_LIST].append(coordinator) - for group in groups: - group_coordinator = TradfriGroupDataUpdateCoordinator( - hass=hass, api=api, group=group - ) - await group_coordinator.async_config_entry_first_refresh() - entry.async_on_unload( - async_dispatcher_connect( - hass, SIGNAL_GW, group_coordinator.set_hub_available - ) - ) - coordinator_data[GROUPS_LIST].append(group_coordinator) - tradfri_data[COORDINATOR] = coordinator_data async def async_keep_alive(now: datetime) -> None: diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 3d68ebbaee0..d3f04f4e2c4 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,7 +1,7 @@ """Consts used by Tradfri.""" from typing import Final -from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION +from homeassistant.components.light import SUPPORT_TRANSITION from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import CONF_HOST, Platform, @@ -16,19 +16,16 @@ ATTR_TRADFRI_GATEWAY_MODEL = "E1526" ATTR_TRADFRI_MANUFACTURER = "IKEA of Sweden" ATTR_TRANSITION_TIME = "transition_time" ATTR_MODEL = "model" -CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" CONF_IDENTITY = "identity" CONF_IMPORT_GROUPS = "import_groups" CONF_GATEWAY_ID = "gateway_id" CONF_KEY = "key" -DEFAULT_ALLOW_TRADFRI_GROUPS = False + DOMAIN = "tradfri" KEY_API = "tradfri_api" DEVICES = "tradfri_devices" -GROUPS = "tradfri_groups" SIGNAL_GW = "tradfri.gw_status" KEY_SECURITY_CODE = "security_code" -SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION PLATFORMS = [ Platform.COVER, @@ -44,6 +41,5 @@ SCAN_INTERVAL = 60 # Interval for updating the coordinator COORDINATOR = "coordinator" COORDINATOR_LIST = "coordinator_list" -GROUPS_LIST = "groups_list" ATTR_FILTER_LIFE_REMAINING: Final = "filter_life_remaining" diff --git a/homeassistant/components/tradfri/coordinator.py b/homeassistant/components/tradfri/coordinator.py index 5a516e8f46e..8aed96d344f 100644 --- a/homeassistant/components/tradfri/coordinator.py +++ b/homeassistant/components/tradfri/coordinator.py @@ -9,7 +9,6 @@ from typing import Any from pytradfri.command import Command from pytradfri.device import Device from pytradfri.error import RequestError -from pytradfri.group import Group from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -95,47 +94,3 @@ class TradfriDeviceDataUpdateCoordinator(DataUpdateCoordinator[Device]): self.update_interval = timedelta(seconds=SCAN_INTERVAL) return self.device - - -class TradfriGroupDataUpdateCoordinator(DataUpdateCoordinator[Group]): - """Coordinator to manage data for a specific Tradfri group.""" - - def __init__( - self, - hass: HomeAssistant, - *, - api: Callable[[Command | list[Command]], Any], - group: Group, - ) -> None: - """Initialize group coordinator.""" - self.api = api - self.group = group - self._exception: Exception | None = None - - super().__init__( - hass, - _LOGGER, - name=f"Update coordinator for {group}", - update_interval=timedelta(seconds=SCAN_INTERVAL), - ) - - async def set_hub_available(self, available: bool) -> None: - """Set status of hub.""" - if available != self.last_update_success: - if not available: - self.last_update_success = False - await self.async_request_refresh() - - async def _async_update_data(self) -> Group: - """Fetch data from the gateway for a specific group.""" - self.update_interval = timedelta(seconds=SCAN_INTERVAL) # Reset update interval - cmd = self.group.update() - try: - await self.api(cmd) - except RequestError as exc: - self.update_interval = timedelta( - seconds=5 - ) # Change interval so we get a swift refresh - raise UpdateFailed("Unable to update group coordinator") from exc - - return self.group diff --git a/homeassistant/components/tradfri/diagnostics.py b/homeassistant/components/tradfri/diagnostics.py index 81f20c4a46a..c415632faa4 100644 --- a/homeassistant/components/tradfri/diagnostics.py +++ b/homeassistant/components/tradfri/diagnostics.py @@ -7,7 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, GROUPS_LIST +from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN async def async_get_config_entry_diagnostics( @@ -32,5 +32,4 @@ async def async_get_config_entry_diagnostics( return { "gateway_version": device.sw_version, "device_data": sorted(device_data), - "no_of_groups": len(coordinator_data[GROUPS_LIST]), } diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index bae626017e7..506343d3d4d 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -5,7 +5,6 @@ from collections.abc import Callable from typing import Any, cast from pytradfri.command import Command -from pytradfri.group import Group from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -20,7 +19,6 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util from .base_class import TradfriBaseEntity @@ -30,19 +28,13 @@ from .const import ( ATTR_SAT, ATTR_TRANSITION_TIME, CONF_GATEWAY_ID, - CONF_IMPORT_GROUPS, COORDINATOR, COORDINATOR_LIST, DOMAIN, - GROUPS_LIST, KEY_API, - SUPPORTED_GROUP_FEATURES, SUPPORTED_LIGHT_FEATURES, ) -from .coordinator import ( - TradfriDeviceDataUpdateCoordinator, - TradfriGroupDataUpdateCoordinator, -) +from .coordinator import TradfriDeviceDataUpdateCoordinator async def async_setup_entry( @@ -55,7 +47,7 @@ async def async_setup_entry( coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] api = coordinator_data[KEY_API] - entities: list = [ + async_add_entities( TradfriLight( device_coordinator, api, @@ -63,71 +55,7 @@ async def async_setup_entry( ) for device_coordinator in coordinator_data[COORDINATOR_LIST] if device_coordinator.device.has_light_control - ] - - if config_entry.data[CONF_IMPORT_GROUPS] and ( - group_coordinators := coordinator_data[GROUPS_LIST] - ): - entities.extend( - [ - TradfriGroup(group_coordinator, api, gateway_id) - for group_coordinator in group_coordinators - ] - ) - - async_add_entities(entities) - - -class TradfriGroup(CoordinatorEntity[TradfriGroupDataUpdateCoordinator], LightEntity): - """The platform class for light groups required by hass.""" - - _attr_supported_features = SUPPORTED_GROUP_FEATURES - - def __init__( - self, - group_coordinator: TradfriGroupDataUpdateCoordinator, - api: Callable[[Command | list[Command]], Any], - gateway_id: str, - ) -> None: - """Initialize a Group.""" - super().__init__(coordinator=group_coordinator) - - self._group: Group = self.coordinator.data - - self._api = api - self._attr_unique_id = f"group-{gateway_id}-{self._group.id}" - - @property - def is_on(self) -> bool: - """Return true if group lights are on.""" - return cast(bool, self._group.state) - - @property - def brightness(self) -> int | None: - """Return the brightness of the group lights.""" - return cast(int, self._group.dimmer) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Instruct the group lights to turn off.""" - await self._api(self._group.set_state(0)) - - await self.coordinator.async_request_refresh() - - async def async_turn_on(self, **kwargs: Any) -> None: - """Instruct the group lights to turn on, or dim.""" - keys = {} - if ATTR_TRANSITION in kwargs: - keys["transition_time"] = int(kwargs[ATTR_TRANSITION]) * 10 - - if ATTR_BRIGHTNESS in kwargs: - if kwargs[ATTR_BRIGHTNESS] == 255: - kwargs[ATTR_BRIGHTNESS] = 254 - - await self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) - else: - await self._api(self._group.set_state(1)) - - await self.coordinator.async_request_refresh() + ) class TradfriLight(TradfriBaseEntity, LightEntity): diff --git a/tests/components/tradfri/common.py b/tests/components/tradfri/common.py index feeb60ab7c9..81e21524eb0 100644 --- a/tests/components/tradfri/common.py +++ b/tests/components/tradfri/common.py @@ -14,7 +14,6 @@ async def setup_integration(hass): "host": "mock-host", "identity": "mock-identity", "key": "mock-key", - "import_groups": True, "gateway_id": GATEWAY_ID, }, ) diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 90fce929f58..17b5530fe81 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -130,126 +130,6 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): } -async def test_import_connection(hass, mock_auth, mock_entry_setup): - """Test a connection via import.""" - mock_auth.side_effect = lambda hass, host, code: { - "host": host, - "gateway_id": "bla", - "identity": "mock-iden", - "key": "mock-key", - } - - flow = await hass.config_entries.flow.async_init( - "tradfri", - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": "123.123.123.123", "import_groups": True}, - ) - - result = await hass.config_entries.flow.async_configure( - flow["flow_id"], {"security_code": "abcd"} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].data == { - "host": "123.123.123.123", - "gateway_id": "bla", - "identity": "mock-iden", - "key": "mock-key", - "import_groups": True, - } - - assert len(mock_entry_setup.mock_calls) == 1 - - -async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): - """Test a connection via import and no groups allowed.""" - mock_auth.side_effect = lambda hass, host, code: { - "host": host, - "gateway_id": "bla", - "identity": "mock-iden", - "key": "mock-key", - } - - flow = await hass.config_entries.flow.async_init( - "tradfri", - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": "123.123.123.123", "import_groups": False}, - ) - - result = await hass.config_entries.flow.async_configure( - flow["flow_id"], {"security_code": "abcd"} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].data == { - "host": "123.123.123.123", - "gateway_id": "bla", - "identity": "mock-iden", - "key": "mock-key", - "import_groups": False, - } - - assert len(mock_entry_setup.mock_calls) == 1 - - -async def test_import_connection_legacy(hass, mock_gateway_info, mock_entry_setup): - """Test a connection via import.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: { - "host": host, - "identity": identity, - "key": key, - "gateway_id": "mock-gateway", - } - - result = await hass.config_entries.flow.async_init( - "tradfri", - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": "123.123.123.123", "key": "mock-key", "import_groups": True}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].data == { - "host": "123.123.123.123", - "gateway_id": "mock-gateway", - "identity": "homeassistant", - "key": "mock-key", - "import_groups": True, - } - - assert len(mock_gateway_info.mock_calls) == 1 - assert len(mock_entry_setup.mock_calls) == 1 - - -async def test_import_connection_legacy_no_groups( - hass, mock_gateway_info, mock_entry_setup -): - """Test a connection via legacy import and no groups allowed.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: { - "host": host, - "identity": identity, - "key": key, - "gateway_id": "mock-gateway", - } - - result = await hass.config_entries.flow.async_init( - "tradfri", - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": "123.123.123.123", "key": "mock-key", "import_groups": False}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].data == { - "host": "123.123.123.123", - "gateway_id": "mock-gateway", - "identity": "homeassistant", - "key": "mock-key", - "import_groups": False, - } - - assert len(mock_gateway_info.mock_calls) == 1 - assert len(mock_entry_setup.mock_calls) == 1 - - async def test_discovery_duplicate_aborted(hass): """Test a duplicate discovery host aborts and updates existing entry.""" entry = MockConfigEntry( diff --git a/tests/components/tradfri/test_diagnostics.py b/tests/components/tradfri/test_diagnostics.py index d76e80a8b9c..c950f4cba9f 100644 --- a/tests/components/tradfri/test_diagnostics.py +++ b/tests/components/tradfri/test_diagnostics.py @@ -7,7 +7,6 @@ from homeassistant.core import HomeAssistant from .common import setup_integration from .test_fan import mock_fan -from .test_light import mock_group from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -30,11 +29,6 @@ async def test_diagnostics( ) ) - mock_gateway.mock_groups.append( - # Add a group - mock_group(test_state={"state": True, "dimmer": 100}) - ) - init_integration = await setup_integration(hass) result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) @@ -42,4 +36,3 @@ async def test_diagnostics( assert isinstance(result, dict) assert result["gateway_version"] == "1.2.1234" assert len(result["device_data"]) == 1 - assert result["no_of_groups"] == 1 diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 2a26391c43f..9d8cf60b2bf 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -17,7 +17,6 @@ async def test_entry_setup_unload(hass, mock_api_factory): tradfri.CONF_HOST: "mock-host", tradfri.CONF_IDENTITY: "mock-identity", tradfri.CONF_KEY: "mock-key", - tradfri.CONF_IMPORT_GROUPS: True, tradfri.CONF_GATEWAY_ID: GATEWAY_ID, }, ) @@ -59,7 +58,6 @@ async def test_remove_stale_devices(hass, mock_api_factory): tradfri.CONF_HOST: "mock-host", tradfri.CONF_IDENTITY: "mock-identity", tradfri.CONF_KEY: "mock-key", - tradfri.CONF_IMPORT_GROUPS: True, tradfri.CONF_GATEWAY_ID: GATEWAY_ID, }, ) diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 1ed24d7b080..8e51e37576e 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -305,92 +305,3 @@ async def test_turn_off(hass, mock_gateway, mock_api_factory): # Check that the state is correct. states = hass.states.get("light.tradfri_light_0") assert states.state == "off" - - -def mock_group(test_state=None, group_number=0): - """Mock a Tradfri group.""" - if test_state is None: - test_state = {} - default_state = {"state": False, "dimmer": 0} - - state = {**default_state, **test_state} - - _mock_group = Mock(member_ids=[], observe=Mock(), **state) - _mock_group.name = f"tradfri_group_{group_number}" - _mock_group.id = group_number - return _mock_group - - -async def test_group(hass, mock_gateway, mock_api_factory): - """Test that groups are correctly added.""" - mock_gateway.mock_groups.append(mock_group()) - state = {"state": True, "dimmer": 100} - mock_gateway.mock_groups.append(mock_group(state, 1)) - await setup_integration(hass) - - group = hass.states.get("light.tradfri_group_mock_gateway_id_0") - assert group is not None - assert group.state == "off" - - group = hass.states.get("light.tradfri_group_mock_gateway_id_1") - assert group is not None - assert group.state == "on" - assert group.attributes["brightness"] == 100 - - -async def test_group_turn_on(hass, mock_gateway, mock_api_factory): - """Test turning on a group.""" - group = mock_group() - group2 = mock_group(group_number=1) - group3 = mock_group(group_number=2) - mock_gateway.mock_groups.append(group) - mock_gateway.mock_groups.append(group2) - mock_gateway.mock_groups.append(group3) - await setup_integration(hass) - - # Use the turn_off service call to change the light state. - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.tradfri_group_mock_gateway_id_0"}, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.tradfri_group_mock_gateway_id_1", "brightness": 100}, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.tradfri_group_mock_gateway_id_2", - "brightness": 100, - "transition": 1, - }, - blocking=True, - ) - await hass.async_block_till_done() - - group.set_state.assert_called_with(1) - group2.set_dimmer.assert_called_with(100) - group3.set_dimmer.assert_called_with(100, transition_time=10) - - -async def test_group_turn_off(hass, mock_gateway, mock_api_factory): - """Test turning off a group.""" - group = mock_group({"state": True}) - mock_gateway.mock_groups.append(group) - await setup_integration(hass) - - # Use the turn_off service call to change the light state. - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.tradfri_group_mock_gateway_id_0"}, - blocking=True, - ) - await hass.async_block_till_done() - - group.set_state.assert_called_with(0) From ae9c2df691c7502ba2a8044f52cca2ca3f9d2a95 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Apr 2022 23:34:36 +0200 Subject: [PATCH 0098/1224] Return unsubscribe callback from start.async_at_start (#69083) * Return unsubscribe callback from start.async_at_start * Update calling code --- homeassistant/components/group/__init__.py | 4 ++-- homeassistant/components/here_travel_time/sensor.py | 2 +- homeassistant/components/statistics/sensor.py | 2 +- homeassistant/helpers/start.py | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 7b895f9c1ce..9627ad86734 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -454,7 +454,7 @@ class GroupEntity(Entity): self.async_update_group_state() self.async_write_ha_state() - start.async_at_start(self.hass, _update_at_start) + self.async_on_remove(start.async_at_start(self.hass, _update_at_start)) @callback def async_defer_or_update_ha_state(self) -> None: @@ -689,7 +689,7 @@ class Group(Entity): async def async_added_to_hass(self): """Handle addition to Home Assistant.""" - start.async_at_start(self.hass, self._async_start) + self.async_on_remove(start.async_at_start(self.hass, self._async_start)) async def async_will_remove_from_hass(self): """Handle removal from Home Assistant.""" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 8fdd3df5fc1..a0449f7b5c0 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -247,7 +247,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): async def _update_at_start(_): await self.async_update() - async_at_start(self.hass, _update_at_start) + self.async_on_remove(async_at_start(self.hass, _update_at_start)) @property def native_value(self) -> str | None: diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index aaca8a98290..ed2352657f4 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -302,7 +302,7 @@ class StatisticsSensor(SensorEntity): if "recorder" in self.hass.config.components: self.hass.async_create_task(self._initialize_from_database()) - async_at_start(self.hass, async_stats_sensor_startup) + self.async_on_remove(async_at_start(self.hass, async_stats_sensor_startup)) def _add_state_to_queue(self, new_state: State) -> None: """Add the state to the queue.""" diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 4560119a685..7f919f5351d 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -4,13 +4,13 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import Event, HassJob, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback @callback def async_at_start( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable[None] | None] -) -> None: +) -> CALLBACK_TYPE: """Execute something when Home Assistant is started. Will execute it now if Home Assistant is already started. @@ -18,10 +18,10 @@ def async_at_start( at_start_job = HassJob(at_start_cb) if hass.is_running: hass.async_run_hass_job(at_start_job, hass) - return + return lambda: None async def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" hass.async_run_hass_job(at_start_job, hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) From ee4eebea7d0921305f234b371794226f519133ac Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 2 Apr 2022 09:14:56 +0200 Subject: [PATCH 0099/1224] Allow lowercase none for effect value in Hue lights (#69111) --- homeassistant/components/hue/v2/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 5b6bb4ed82c..af3dfa80ffc 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -194,7 +194,7 @@ class HueLight(HueBaseEntity, LightEntity): brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) effect = effect_str = kwargs.get(ATTR_EFFECT) - if effect_str == EFFECT_NONE: + if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()): effect = EffectStatus.NO_EFFECT elif effect_str is not None: # work out if we got a regular effect or timed effect From 15fc7349edbf20f2dd459f6b998fb2facf84594c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 2 Apr 2022 03:36:56 -0400 Subject: [PATCH 0100/1224] Fix unit prefixes for derivative and integration (#69109) --- homeassistant/components/derivative/config_flow.py | 5 +++-- homeassistant/components/integration/config_flow.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index fe6b99c3eca..6249e9fd1cc 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -37,8 +37,9 @@ UNIT_PREFIXES = [ {"value": "m", "label": "m (milli)"}, {"value": "k", "label": "k (kilo)"}, {"value": "M", "label": "M (mega)"}, - {"value": "G", "label": "T (tera)"}, - {"value": "T", "label": "P (peta)"}, + {"value": "G", "label": "G (giga)"}, + {"value": "T", "label": "T (tera)"}, + {"value": "P", "label": "P (peta)"}, ] TIME_UNITS = [ {"value": TIME_SECONDS, "label": "Seconds"}, diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index c220327e983..cd4f8a11fbf 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -37,8 +37,9 @@ UNIT_PREFIXES = [ {"value": "none", "label": "none"}, {"value": "k", "label": "k (kilo)"}, {"value": "M", "label": "M (mega)"}, - {"value": "G", "label": "T (tera)"}, - {"value": "T", "label": "P (peta)"}, + {"value": "G", "label": "G (giga)"}, + {"value": "T", "label": "T (tera)"}, + {"value": "P", "label": "P (peta)"}, ] TIME_UNITS = [ {"value": TIME_SECONDS, "label": "s (seconds)"}, From ebfba783e0b39920840998d8b4e39841016184b3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Apr 2022 09:38:10 +0200 Subject: [PATCH 0101/1224] Bump num of conflicts in pip check (#69112) --- script/pip_check | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/pip_check b/script/pip_check index f29ea5a5dd0..6f48ce15bb1 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=6 +DEPENDENCY_CONFLICTS=7 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 5f897874cbf5036bf61813aae24f6b4737458e7e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 2 Apr 2022 03:42:19 -0400 Subject: [PATCH 0102/1224] Fix kodi log spamming (#69113) --- homeassistant/components/kodi/media_player.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index a07ee14137a..2de2c277189 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -321,7 +321,6 @@ class KodiEntity(MediaPlayerEntity): self._app_properties = {} self._media_position_updated_at = None self._media_position = None - self._connect_error = False @property def _kodi_is_off(self): @@ -447,8 +446,9 @@ class KodiEntity(MediaPlayerEntity): except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): if not self._connect_error: self._connect_error = True - _LOGGER.error("Unable to connect to Kodi via websocket") + _LOGGER.warning("Unable to connect to Kodi via websocket") await self._clear_connection(False) + self._connect_error = False async def _ping(self): try: @@ -456,8 +456,9 @@ class KodiEntity(MediaPlayerEntity): except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): if not self._connect_error: self._connect_error = True - _LOGGER.error("Unable to ping Kodi via websocket") + _LOGGER.warning("Unable to ping Kodi via websocket") await self._clear_connection() + self._connect_error = False async def _async_connect_websocket_if_disconnected(self, *_): """Reconnect the websocket if it fails.""" From 538c8160f36746c4efef836fec393aa4e74ce83e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 2 Apr 2022 09:44:05 +0200 Subject: [PATCH 0103/1224] Adjust check for orphaned Hue device entries for grouped lights (#69110) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/v2/device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 64bdcc7a4f2..c3deee40023 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -81,6 +81,10 @@ async def async_setup_devices(bridge: "HueBridge"): dev_reg, entry.entry_id ): if device not in known_devices: + # handle case where a virtual device was created for a Hue group + hue_dev_id = next(x[1] for x in device.identifiers if x[0] == DOMAIN) + if hue_dev_id in api.groups: + continue dev_reg.async_remove_device(device.id) # add listener for updates on Hue devices controller From 912923f55debfbb548313776339b13d87f4dd17d Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sat, 2 Apr 2022 03:44:40 -0400 Subject: [PATCH 0104/1224] Environment Canada: Fix for when temperature is zero (#69101) --- .../components/environment_canada/weather.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 4d226261b94..1b81750fb47 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -80,12 +80,16 @@ class ECWeather(CoordinatorEntity, WeatherEntity): @property def temperature(self): """Return the temperature.""" - if self.ec_data.conditions.get("temperature", {}).get("value"): - return float(self.ec_data.conditions["temperature"]["value"]) - if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get( - "temperature" + if ( + temperature := self.ec_data.conditions.get("temperature", {}).get("value") + ) is not None: + return float(temperature) + if ( + self.ec_data.hourly_forecasts + and (temperature := self.ec_data.hourly_forecasts[0].get("temperature")) + is not None ): - return float(self.ec_data.hourly_forecasts[0]["temperature"]) + return float(temperature) return None @property From aa969d5ae830faec98e918a7a2e6d99c81d825e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 2 Apr 2022 10:01:49 +0200 Subject: [PATCH 0105/1224] Add missing typing to Airzone tests (#69097) --- tests/components/airzone/test_binary_sensor.py | 3 ++- tests/components/airzone/test_climate.py | 11 ++++++----- tests/components/airzone/test_config_flow.py | 7 ++++--- tests/components/airzone/test_coordinator.py | 2 +- tests/components/airzone/test_init.py | 3 ++- tests/components/airzone/test_sensor.py | 4 +++- tests/components/airzone/util.py | 2 +- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/components/airzone/test_binary_sensor.py b/tests/components/airzone/test_binary_sensor.py index ee3a8324ea4..13582a3c724 100644 --- a/tests/components/airzone/test_binary_sensor.py +++ b/tests/components/airzone/test_binary_sensor.py @@ -1,11 +1,12 @@ """The sensor tests for the Airzone platform.""" from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from .util import async_init_integration -async def test_airzone_create_binary_sensors(hass): +async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: """Test creation of binary sensors.""" await async_init_integration(hass) diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index b06bb1f046f..107f0c32297 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -37,12 +37,13 @@ from homeassistant.components.climate.const import ( SERVICE_SET_TEMPERATURE, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from .util import async_init_integration -async def test_airzone_create_climates(hass): +async def test_airzone_create_climates(hass: HomeAssistant) -> None: """Test creation of climates.""" await async_init_integration(hass) @@ -133,7 +134,7 @@ async def test_airzone_create_climates(hass): assert state.attributes.get(ATTR_TEMPERATURE) == 19.1 -async def test_airzone_climate_set_hvac_mode(hass): +async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: """Test setting the HVAC mode.""" await async_init_integration(hass) @@ -192,7 +193,7 @@ async def test_airzone_climate_set_hvac_mode(hass): assert state.state == HVAC_MODE_OFF -async def test_airzone_climate_set_hvac_slave_error(hass): +async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None: """Test setting the HVAC mode for a slave zone.""" HVAC_MOCK = { @@ -225,7 +226,7 @@ async def test_airzone_climate_set_hvac_slave_error(hass): assert state.state == HVAC_MODE_OFF -async def test_airzone_climate_set_temp(hass): +async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: """Test setting the target temperature.""" HVAC_MOCK = { @@ -258,7 +259,7 @@ async def test_airzone_climate_set_temp(hass): assert state.attributes.get(ATTR_TEMPERATURE) == 20.5 -async def test_airzone_climate_set_temp_error(hass): +async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: """Test error when setting the target temperature.""" await async_init_integration(hass) diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 08eb35ef52b..8ffe10167ea 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -8,13 +8,14 @@ from homeassistant import data_entry_flow from homeassistant.components.airzone.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from .util import CONFIG, HVAC_MOCK from tests.common import MockConfigEntry -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test that the form is served with valid input.""" with patch( @@ -56,7 +57,7 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_duplicated_id(hass): +async def test_form_duplicated_id(hass: HomeAssistant) -> None: """Test setting up duplicated entry.""" entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) @@ -74,7 +75,7 @@ async def test_form_duplicated_id(hass): assert result["reason"] == "already_configured" -async def test_connection_error(hass): +async def test_connection_error(hass: HomeAssistant): """Test connection to host error.""" with patch( diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py index 00ef0616b3e..179e94355c0 100644 --- a/tests/components/airzone/test_coordinator.py +++ b/tests/components/airzone/test_coordinator.py @@ -15,7 +15,7 @@ from .util import CONFIG, HVAC_MOCK from tests.common import MockConfigEntry, async_fire_time_changed -async def test_coordinator_client_connector_error(hass: HomeAssistant): +async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: """Test ClientConnectorError on coordinator update.""" entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py index 30e3ce37d6f..ce08a17ec6c 100644 --- a/tests/components/airzone/test_init.py +++ b/tests/components/airzone/test_init.py @@ -4,13 +4,14 @@ from unittest.mock import patch from homeassistant.components.airzone.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant from .util import CONFIG, HVAC_MOCK from tests.common import MockConfigEntry -async def test_unload_entry(hass): +async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload.""" config_entry = MockConfigEntry( diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index fc03d8a3301..c68be2abbab 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -1,9 +1,11 @@ """The sensor tests for the Airzone platform.""" +from homeassistant.core import HomeAssistant + from .util import async_init_integration -async def test_airzone_create_sensors(hass): +async def test_airzone_create_sensors(hass: HomeAssistant) -> None: """Test creation of sensors.""" await async_init_integration(hass) diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 2f7afb068b3..f533870550d 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -150,7 +150,7 @@ HVAC_MOCK = { async def async_init_integration( hass: HomeAssistant, -): +) -> None: """Set up the Airzone integration in Home Assistant.""" entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) From 66e9b263a8dad9c268040368c25ddda38ada9b1c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 2 Apr 2022 10:03:19 +0200 Subject: [PATCH 0106/1224] Migrate bond light to color_mode (#69078) --- homeassistant/components/bond/light.py | 12 ++++++--- tests/components/bond/test_light.py | 35 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index b5728ec47d4..ecf93a26090 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -10,7 +10,8 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -126,7 +127,8 @@ async def async_setup_entry( class BondBaseLight(BondEntity, LightEntity): """Representation of a Bond light.""" - _attr_supported_features = 0 + _attr_color_mode = COLOR_MODE_ONOFF + _attr_supported_color_modes = {COLOR_MODE_ONOFF} async def async_set_brightness_belief(self, brightness: int) -> None: """Set the belief state of the light.""" @@ -170,7 +172,8 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) if device.supports_set_brightness(): - self._attr_supported_features = SUPPORT_BRIGHTNESS + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} def _apply_state(self, state: dict) -> None: self._attr_is_on = state.get("light") == 1 @@ -267,7 +270,8 @@ class BondUpLight(BondBaseLight, BondEntity, LightEntity): class BondFireplace(BondEntity, LightEntity): """Representation of a Bond-controlled fireplace.""" - _attr_supported_features = SUPPORT_BRIGHTNESS + _attr_color_mode = COLOR_MODE_BRIGHTNESS + _attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} def _apply_state(self, state: dict) -> None: power = state.get("power") diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 695a98a927e..1e8b50be073 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -18,8 +18,11 @@ from homeassistant.components.bond.light import ( ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, DOMAIN as LIGHT_DOMAIN, - SUPPORT_BRIGHTNESS, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -723,7 +726,20 @@ async def test_brightness_support(hass: core.HomeAssistant): ) state = hass.states.get("light.name_1") - assert state.attributes[ATTR_SUPPORTED_FEATURES] & SUPPORT_BRIGHTNESS + assert state.state == "off" + assert ATTR_COLOR_MODE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_BRIGHTNESS] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + with patch_bond_device_state(return_value={"light": 1, "brightness": 50}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + state = hass.states.get("light.name_1") + assert state.state == "on" + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_BRIGHTNESS + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_BRIGHTNESS] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 async def test_brightness_not_supported(hass: core.HomeAssistant): @@ -736,7 +752,20 @@ async def test_brightness_not_supported(hass: core.HomeAssistant): ) state = hass.states.get("light.name_1") - assert not state.attributes[ATTR_SUPPORTED_FEATURES] & SUPPORT_BRIGHTNESS + assert state.state == "off" + assert ATTR_COLOR_MODE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_ONOFF] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + with patch_bond_device_state(return_value={"light": 1}): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + state = hass.states.get("light.name_1") + assert state.state == "on" + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_ONOFF + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_ONOFF] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 async def test_turn_on_light_with_brightness(hass: core.HomeAssistant): From 88ae7de4fba2dea152aae6efd406689fd0de99ae Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 2 Apr 2022 04:23:22 -0400 Subject: [PATCH 0107/1224] Bump ZHA dependency zigpy-deconz from 0.14.0 to 0.15.0 (#69099) --- 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 6d47535b765..fcf6a126963 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,7 +8,7 @@ "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.69", - "zigpy-deconz==0.14.0", + "zigpy-deconz==0.15.0", "zigpy==0.44.1", "zigpy-xbee==0.14.0", "zigpy-zigate==0.8.0", diff --git a/requirements_all.txt b/requirements_all.txt index 1494341eba0..02baa3eefc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2482,7 +2482,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.14.0 +zigpy-deconz==0.15.0 # homeassistant.components.zha zigpy-xbee==0.14.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33bf00e9e8b..4bbd29028ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1601,7 +1601,7 @@ zeroconf==0.38.4 zha-quirks==0.0.69 # homeassistant.components.zha -zigpy-deconz==0.14.0 +zigpy-deconz==0.15.0 # homeassistant.components.zha zigpy-xbee==0.14.0 From 9a1f5ca16f7860764be4f2be000ccedb24393d20 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Apr 2022 10:43:50 +0200 Subject: [PATCH 0108/1224] Update wled to 0.13.2 (#69116) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 6b72bbf905c..668950b9326 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.13.1"], + "requirements": ["wled==0.13.2"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 02baa3eefc5..79115c6988e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2423,7 +2423,7 @@ wirelesstagpy==0.8.1 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.1 +wled==0.13.2 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bbd29028ca..e10973325d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1563,7 +1563,7 @@ wiffi==1.1.0 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.1 +wled==0.13.2 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 1563420de85eb8fa12e161def3b263b3bc8981c9 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sat, 2 Apr 2022 04:44:16 -0400 Subject: [PATCH 0109/1224] Bump asyncsleepiq to 1.2.3 (#69104) --- .../components/sleepiq/manifest.json | 2 +- homeassistant/components/sleepiq/select.py | 15 ++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sleepiq/conftest.py | 21 ++++++++++--------- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 16881506cb8..cadfe126ecf 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.2.1"], + "requirements": ["asyncsleepiq==1.2.3"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py index 711f228d7f2..0cbf1671e2b 100644 --- a/homeassistant/components/sleepiq/select.py +++ b/homeassistant/components/sleepiq/select.py @@ -1,7 +1,7 @@ """Support for SleepIQ foundation preset selection.""" from __future__ import annotations -from asyncsleepiq import BED_PRESETS, SleepIQBed, SleepIQPreset +from asyncsleepiq import BED_PRESETS, Side, SleepIQBed, SleepIQPreset from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -39,14 +39,11 @@ class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): """Initialize the select entity.""" self.preset = preset - if preset.side: - self._attr_name = ( - f"SleepNumber {bed.name} Foundation Preset {preset.side_full}" - ) - self._attr_unique_id = f"{bed.id}_preset_{preset.side}" - else: - self._attr_name = f"SleepNumber {bed.name} Foundation Preset" - self._attr_unique_id = f"{bed.id}_preset" + self._attr_name = f"SleepNumber {bed.name} Foundation Preset" + self._attr_unique_id = f"{bed.id}_preset" + if preset.side != Side.NONE: + self._attr_name += f" {preset.side_full}" + self._attr_unique_id += f"_{preset.side}" super().__init__(coordinator, bed) self._async_update_attrs() diff --git a/requirements_all.txt b/requirements_all.txt index 79115c6988e..abcd7325601 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ async-upnp-client==0.27.0 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.2.1 +asyncsleepiq==1.2.3 # homeassistant.components.aten_pe atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e10973325d0..6e7bc79fd0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ arcam-fmj==0.12.0 async-upnp-client==0.27.0 # homeassistant.components.sleepiq -asyncsleepiq==1.2.1 +asyncsleepiq==1.2.3 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 4408f41035b..23c42ee7f66 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -5,6 +5,7 @@ from collections.abc import Generator from unittest.mock import MagicMock, create_autospec, patch from asyncsleepiq import ( + Side, SleepIQActuator, SleepIQBed, SleepIQFoundation, @@ -52,14 +53,14 @@ def mock_bed() -> MagicMock: sleeper_r = create_autospec(SleepIQSleeper) bed.sleepers = [sleeper_l, sleeper_r] - sleeper_l.side = "L" + sleeper_l.side = Side.LEFT sleeper_l.name = SLEEPER_L_NAME sleeper_l.in_bed = True sleeper_l.sleep_number = 40 sleeper_l.pressure = 1000 sleeper_l.sleeper_id = SLEEPER_L_ID - sleeper_r.side = "R" + sleeper_r.side = Side.RIGHT sleeper_r.name = SLEEPER_R_NAME sleeper_r.in_bed = False sleeper_r.sleep_number = 80 @@ -91,13 +92,13 @@ def mock_asyncsleepiq_single_foundation( actuator_f = create_autospec(SleepIQActuator) mock_bed.foundation.actuators = [actuator_h, actuator_f] - actuator_h.side = "R" + actuator_h.side = Side.NONE actuator_h.side_full = "Right" actuator_h.actuator = "H" actuator_h.actuator_full = "Head" actuator_h.position = 60 - actuator_f.side = None + actuator_f.side = Side.NONE actuator_f.actuator = "F" actuator_f.actuator_full = "Foot" actuator_f.position = 10 @@ -106,8 +107,8 @@ def mock_asyncsleepiq_single_foundation( mock_bed.foundation.presets = [preset] preset.preset = PRESET_R_STATE - preset.side = None - preset.side_full = None + preset.side = Side.NONE + preset.side_full = "Right" yield client @@ -123,13 +124,13 @@ def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock, None, None]: actuator_f = create_autospec(SleepIQActuator) mock_bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f] - actuator_h_r.side = "R" + actuator_h_r.side = Side.RIGHT actuator_h_r.side_full = "Right" actuator_h_r.actuator = "H" actuator_h_r.actuator_full = "Head" actuator_h_r.position = 60 - actuator_h_l.side = "L" + actuator_h_l.side = Side.LEFT actuator_h_l.side_full = "Left" actuator_h_l.actuator = "H" actuator_h_l.actuator_full = "Head" @@ -145,11 +146,11 @@ def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock, None, None]: mock_bed.foundation.presets = [preset_l, preset_r] preset_l.preset = PRESET_L_STATE - preset_l.side = "L" + preset_l.side = Side.LEFT preset_l.side_full = "Left" preset_r.preset = PRESET_R_STATE - preset_r.side = "R" + preset_r.side = Side.RIGHT preset_r.side_full = "Right" yield client From c3354dcaae5f125909981519f75c71c2d8603119 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sat, 2 Apr 2022 09:54:19 +0100 Subject: [PATCH 0110/1224] Add image test cases to generic (#69040) --- tests/components/generic/sample1_animate.png | Bin 0 -> 259461 bytes .../generic/sample2_jpeg_odd_header.jpg | Bin 0 -> 79018 bytes .../generic/sample3_jpeg_odd_header.jpg | Bin 0 -> 176350 bytes .../generic/sample4_K5-60mileAnim-320x240.gif | Bin 0 -> 1140935 bytes tests/components/generic/test_config_flow.py | 25 ++++++++++++++++++ 5 files changed, 25 insertions(+) create mode 100644 tests/components/generic/sample1_animate.png create mode 100644 tests/components/generic/sample2_jpeg_odd_header.jpg create mode 100644 tests/components/generic/sample3_jpeg_odd_header.jpg create mode 100644 tests/components/generic/sample4_K5-60mileAnim-320x240.gif diff --git a/tests/components/generic/sample1_animate.png b/tests/components/generic/sample1_animate.png new file mode 100644 index 0000000000000000000000000000000000000000..d59a744f735de26c22c011306c7f90d460792a87 GIT binary patch literal 259461 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 lOoSzPX4z|8rf%XWE%32a)nNHJ|AtDF5& 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

X^=uNlXM zj|YiF&+(Hncx&~NPif|@Re66lc?ccfAfB)P`%-}i3tRHLM~#c#zCXp1IE8(R%BRUY zHtznpMs4exMOv>_f{j$QH;4(o`ct*!`_bnoRI+v~%9^{OfBBtdlCRTLwkP}x*~2?E zgID{xv+{Q7$zPTw$i2RBZ=-DT$M>;?6Qw+FTRhDAbJ+d_A1k|2^0y}o;#P2{uhE*E zr}f0Sa)*(Q{OuDvv|m3iaBE57b1%gQr#7xgDzs}i@MAD>68Pdc|FhL}V{2)3l{_Vn2kE_4 zT`vA#k}Lzn_xrl57~9Klj6I|mp;bI1_MwM-i-kUm)AB7|#kp?R^va%WY@M{s=e$YS z(SQBE%X>3EZ8*DWw)OYo*J=07xlJ9XeDT!&V%cA+@jU;X=qSZxK;;*bC_1@%))7Gk|F7bOz-)elUau5dZ%M8)f{ z$aN2bes6aA7r8w$TqsOne%x^HGGkn$qm)JMte?&RhIb*lCB~jcO5SzX%}UpI{lBI4yV{C#PA&cRuX0z@^6xV@KV31|YQJ87_-viC5!gj#LxZncl~1ey_?gzbbXeych(Br|K9ySU{8HX#$3Nx{-2ZBb-T5dHx*Uw{;z$&IAE!;RlQZ!-g2Eo{+36K7EQWu zc0~I^W`wo0o8E47%Wcblm*xFQ|GezDw!pk!zZc5y>0A8$Wd55yMSHeyu3_@EUiW?X zna9?v!xa`S`gQ90o||uLviJYkI=$fJ3#+60>?Mbn7lyE~Fev_HVUS~BV9;Rz0p<^k z?I4;#m4Shgfq_9tNJxSRAPH6@i3+4-C1unl)#r_jqWgW4rUhlrjmnw_McMPC;i!B~TGjeYtf+c@CIr>3&+J)~v3)_( zjk&H5S|eVT27Zk9`0nTMO-12{h|p(Q>91Pqzin;)B_#aI$^BPW{;$6AZ(GOTuHHWr zCjXf-?eC(+|JJShw{z#ey?g&2I`r@O@qedJ|2uQ$-??-DZr=QN`!)jIx%2zZov(NA zzPW$@$%6+E;pia>c>46|&!0d4VSpb$|4m)|nbrOSquFzR zJ^vW?{bM-zpW)PhhRgpMuE5Z{{|rco;~xV%hm6OD1qYisgtcN$Y*=`>T|n7uj>pDD zN4q7Av+kVOxcGR#f^(OQXVH_Llb=g0(>b|m>FMbP$*1N>PM+zTW*E56Wyj=Y=NUEX zxpXB;G>*=-*M~U zeLFhS>AIb~Rn5{xy|1tB|F3o@N|dDC#U0`8kZW6cjTiY?k6vEupEs8{j-HdKY(ExT)a zJZts4kL|BB7EH2wvA(o-m)6=vpQ1uDwq_oi6SJO2MM0BUbj1S)J~N{UEdoI=7-P09 zow9KI^t>C2?FxJ$#~oCcbtrY~9^-i2W6IaE*qHe(#}fxK3w*MZ@Y%|t#c0A z+)Fksp5*1FIVI`p9nFe8p;1NWHnpYSuRHQ8Yt=f(i{^5xH!l0G6~9()_sWeseZPIa z^gGmd>-w<$;iVV9%DyeW{4w+F{Df(bj=y0zYiG@{<&v6(gBo*)^mm3Eei07~AJ%c@ z{quS}>Gwm1=krdVW_Y=3wbJg29WU1Pn1mTOJpRn*uzs&S--0s?Kb4FR7|xb|nLo+@ zS??!@;Gj-}X_0&f3=Z$*dGO(IikFPx(f>RFzmGB}tl2o1x!}!5h7Szy(-$<|swr9+ zD7NYogSPqkKc{<+$-F#c;=Nbrlf~=Uh~-BbkFUOVsEPk;+{&Qxk}LMr?ekZAPX6}y z$Kk2zT4}ke>50GZ?e?lG+PTi_#gC~Ir{?J}&pJ0pHx{cf|*KP}_2(TD>DDXB3+*ML>Y|K$mk`(f|pEJ?1 zgtNtk*>R$S-3c$jU7tLaTkbse>2RuwJm=2zapFFMb{^%@b@BhW7CN+8ZfH95p@b`W zhC|fzU3$DOKK3#U{L3}ZW-%4|ZkJA)U9{Y~@yf~Bl|mfzeyQxSH~pxfZp~r6!NPUg zPM$!WX7$g{c5c$W72Mtb@21?>D&y7r?r7DOP4TOI@p!-0s`U$gNK47*m!9|#`(hz; z%L^yLSLa=XN)|1&T5+lEs_AB3ZN)y32@W^5ZEHNRwz2r|ldTpjb-8jMEH$|6pqu7( zdDgX(DNFuHcerhFY%H0eAe-})J@Kbk#YrJ|rkQh|uoYQ~CgsKd*1E(!^NC(U#Me~^ zH0Fk7&N!oEIe8}Y1x3dakF%Sij61d~C%wDYb>!a8&^ZdsGf#A|Yn0#kx~}4=w)MAD z;bnW?RCdlneFa#Qr{YeSg@-%EnrC z8~z^uMull9%YVK2tM){8Vd5>JD{UA8yc- zl~K30_KeKW4Ec4f7Ka;4{M$pDl0#lTujw=CQ1OoPy!Y?hc7c>k@6O+s9?O~Uv|_!( z#?F2v#7%tHdYQCR*>r(uJ=5uTzP<~N{NepkZ{40S+xPztPMB#Wy^U|pyV|rGb|>G= zMPC&pK0kB#boze3=EgZCYgacg`#jhywPQnMNl5?V-cDbct_PXNKe|IV_&IvUw-g?$NW!dz3oM$r*U2jiydOe#(X}N9v<#Of+ z>pw8azHk)Yb)mgXF5$7J|G~14cl(}kN-!^uaPwMrssHJgea|1gto?oa=)J{f{m zlM%N0<)ztHjT*-7Y!mgj)z8TNrKfVfs%^D}y;^uN(~Ub-uHQegRZr02x^|auUP#i` z<0V|~Qrm3LeBZy^ZsM_^xv#>+Zy5H-99^rComd%-2cr2YAv#gWu(t<#&3=zY=}U|%P|tOvfoDd8zCfc;MWe)wMyVZ*GB+CK zel#jbG%1~E&^KsOt7y`g(WJGbNvERGGoVRVqFITf*(jsQa{`O$jYiIlM&lXHb}yRQ zC0d*;T3jMpTu(If{%H8s-r~8V+5SgMfJCe8jQXI6*073JQ;+6|ipGK@_2?5#Q5CJ6 z9!&}oZSf~s?S62HXSC$pXv_Q2#(biMkE7B0MGN1LhN6nLsvYeT9<4PQt!Y2nBP2TF zchojzH2?eA-W-w9*izBar@>Yr(K*SY)5)Su+@taL^p(Q8LbsYUx-iSB(fnilS8+i%fx^lzebpUZ z`%l!K%;-}&(WRNuKH)@P!;Ic95&i5D?P49ByI!<>(CB}2qvOJhe#IAkOfwrN6Z zbhBqR{gr5b?a}kqp!J)@M3K%0!4>`gw)e7jPLQ6_DgATe@);9eTC^V7F+p9T@rgz= zzh&1?g^8j+CmGEAS8pxRFo&a0A+t+qXPai_WShz+%a{FXm3;vxnjKFz>Hh3CoH<3m zbK>9S6Kz*ckb5!F-*PH*X4n4@T)9e3_Lg1IE1PcrV2YJ&KC@#I*UBkrGpGD`G1bg- zs-na+8I7qSFQ@rOPO)Fnx5}cq=x2M$%SNfrzEVl`$d?mNz2M2Z(Ue{}L-ghZ^_Np} zZcb~HoDi~eI@8PPH9yp@&SoEhifnMowBebI#Tq-9Khb zllExL=A13{V|KpgY}?2=Z!&tC{++08x!JJu=Co~|vr=}>KEu&rKchWi<)r^B=DhLj z{4rzV?uyQHnsb{trrXS%_vK{6t(P7D19+DR%!%rpAETkp7E>fGmgrV2 zsPR}FRMn!hYq2|5>!0J(9HV-oXEkMMEYSTiTcc|!@2|#>KYLaisJz`;4 z*ObUp(^6;6&FpHjU)A)EV`+p`tLTcwnjABBot*t|-iu{zSrZGnT1%ta>SipLf4Rhe z=L(}ATuUCTD6CvAbYg}~R?`#9rhnhLqN`Rc;+o$%Yf;^*)@e}_CREM6EvcY4t9b@j zThNcD2Rl|uotVwCt6_I!8{5i`)|JZ_ORX{DTz=SdO~B9Al~EHmMy-{Dq}UD*2;4o>+~uclUJ@y z&YH7Zt9!$$PL<9^52+bVTf+Bt2FxQTW$eFFA4Fjm1$N z8(z+8+#@+FGir*0_XZ!0P1+Wd#j4k(@7O3EwMjc-efiA>f!m7`tTsN0TZE5n@^eAH+&*{k?SyNheH5BdM z_ETZQE3IjlXDm>$S~%}kgW$|aqE$XREH}T7zAUGZQw6ckNN&-loveK0TuGqV~35 zo_l}XZoJOD`1K97a|e3v@0_&CqxHe<#^jxo`l^>4&0KA_ds|Y$qWtXj8L##|%Gxip zV&bRYyS|)mdNXVPm!13NI<`+*&U1d&#-c?$+MF9%=PWrs^8nYL6%%e9kkHuPa%;V| z=bq4rl_D_*KV&bn-F+~WV|T)fLt$P=5^o)x)9V*~GUkV z=44Kv(r@y&y<8)&K_rHxB!Y@pTnz2r*jlJtakZF1_Eo1GGA@ee_BVI0DmXDm`tB6*&d%d&ns@HFB5H9-rFZ7+Gba*W&JKIqel_DP zcSTL-4ds_Qjm2krxZWPon0qwx#A>^q4Z4x%BzL!3Se$K;zVbDqUG>Mji4jXJW_5pA z(|_^jR!8ac`+QC|$X?arI2X@xQ6XXm^WVt_HFhKg@aj8omHgpUqEA!9oht@|}*0%lobK^iHf5P#Ny&t%8 zbQ>?-Ix~Cj-Cr+fn^*3gFL}dE`&u&30;amw`?{UWqR;FAsFuW5Q~SAX%HXT{UJ#DODm?WsFli?-*?T3mVANoraNXWw7j z!)CMF*nU39Juypu->&=K_r!NzF5~T$$T=xgx7cvUJ*9V@8)D~j%5=z9@8tHle~RO# zbbTeL`udB)P({jBG@I&|+{+_`qv&1cs%p14WeYw~^gukzjX z$Gul}oOt+T)sDb>4};^TuGeWc*|GAN^mB(FvkULeKR;_iKCeQ~-ID<|n>lJvq`X^O z*!6hr-8Fxr7If|DGq7Ia-|@UwuZ`d0O08eRTF$vu`}Ta)Y2H-#!gj`s*4#ageCyIA zuK(ofiGIm*&US~DlY;B7p8ZzyuG=2hn8Uy10>|X`?X&()JDz>5V_)L}xmRp9$M2Ny zo6oy?Rb5MW#_d&+XST}iZyDKEA;S#LZOvD7YitGRabI-QSC z{_WbwbFb#-a*r7o&t|@G(Yqvd|D%J~ff<_9D^@={OB(gF7xGYzOrwbR>ds``;SR8?rn)!$ZL7K-foST{?j*i z@A=iQQ{CTq;p~>ft5+8;YGXQm|0@Iki3jgLXdMst-1+$B5r6OIOI1G{bnjYM&AA!% zcE_w&rSYB8=U&yEYrFko&zjqdymsvR(DPAz&cWvVx7TLZf16kDf2U9F#TynUj{RQu zr|g>WXYJG7{%aG~{m7K#zx?v2^uL?u7wFFlh`C&3|M#};)}{6>{&L%<_B`30`^Ad? z_rw14-?)El-M^Ue(w(DxP3nKv9Pj1%`e1&)?*EvKzv2}&3{LZw%gs0^d+nQkBf~_M zliY?ZM`c`Q1Uz(V8i5dc?xMvD^ywxiHbGQ146T=NCcCeCO)T{Sor&a(HNb=gzLr z@%w7NPMIh)`DEnvnk8%Yb!8PMUR!n5p)7Q^i^`!13eVMKW-NISvd`zT(VyLhk9{X3 zz4^J1$X zpP#2&AHL$}!WT_9+x_h9XJiC^^uBJ~E?@t+_~%z%_VrZ(R@a-OKfgZzeD#_|Roy!4 z*H~%jrXO46ZM9cu(m8`W4BqGVJ$>PP&i0eps}=TFwlN#J2-e@au=~A}_xjy!el>4w zdiG`eZxT{%KI0^vf8@eW>6x!yE%q@|d@5h@NKt5>BL9-^?f-SOHf-HoG5f`a);8sH z28a8ElNZR8=X7~>n%Hn0>rmP=Nq>WojG?dT=GlVk63J&c_s)oW^UqJ+BHycP$vw?Fys+jfe5b@&uZH92MRy}mR5bSbM|VmWQxF=eCD+GUmu4~5K zHN@S(yk+v4-%W=vtu&2q4c%ycmuIcNjjlJR&600hLJdR?zcxKHD@0{$L~_6FiJ7fG zR%;16b;XFttxsF6E|;!k+C62mn(m7z@2ROT=2zsc)?0Y%)vYc?U0c^_nq|^lB{ml6 zZ`TLx66iLKuZ>+~?iVL`*=I%6MefgM6S(^>o{O!%`to|2e4W<4R+Vjr^RwlDsZYM$ z(Xvf2$S5;qf6i*F$%;AByL_!)?TFpB|D{*eLGO;jD)D=la&klWcYJ@k^LgXre@l$R ze|+=dyMCdKce9f9(QJv(_Q2d5iMIUncG`8V-sf++t|R29eCn!)r%q%&X8g2OdG61T ztL}L)t;?NiuRG=HAui{PkTv~WUtTS`vg&(W_50GR=?A}tzA|nuP0h>f4RD(5zp&%! z*1by`dwo8g=D0TD)$WFMn{+ws$O;z80kCx`Loj2i^)SjOa{M&mriFEGWS@fdTD*wTPkfvFC zmKn;wdcOPsf8FubU;9o}EK6GLe$yv*->2i(?c(Noq^2DIrqjQkN&07z`YzwXdrH#n zD|DPzd0lY$cgkI8X6?#~?ZuaSo?TMpU^`bS9wNNH?WBbUF8hR)g2fI@AR zQ0|XP$5&oyHAqsJE-_(ZXUiI0`IOUK|27m&kS;Uq;0`=yua~Cjzw@oU;pf{eMbkI$ zIZ&uF;Y-Z(B`t>S7qrvecPcJDWZ@T#BU3JHPLZ({H?}fL&GY8; ze0gL6569IFRzDMx>IMEyv2T^R)RxLPd%M7+Z8|%Btk&H0=Sg%s8gWQFf9n^45`|5k z%Xm7b+-C`t7Hm6pI%9dk$`aMRBI$>+#JXoaoAk)TZl!B~3zt*aF8=JU9X=CUiWD7L zRv2<-PTeB8Op80{W;ov`X{XXBxdMhKt#3yPS%tmOIDK36{=3fe%NRK4$fsToJe8qi z*f~i}xhKl%_mOi=z9xM|N1pX93qGi_)@y#aVu(&DD4DOfIXd`U*$2l9z}d6s=liEakaaYhu%u4_iGG^>V{)9^RO` zUBe_>KAR)FrfPcs*A;>@SFrKzHu|SmtyeN%;o?#kt3ZEgvD?im58IYZ=q{YYaq}GSHIx4v9CA+Fp7P+@^!WS5@18C2 zQg87zTcbMXjmKi``G40L%<`E!&G^>w{Z4t8Ey6e6*m}=>g?(3wQVM719W5vCCY`B@ zzvpBm6nP(e^+Rlm!RGv~(9Bb1i}I_tR!Mc9I=T1XAHE&>?`1YFF!Skn@+74;{ui^ideM+Ei$`>iggPj-LKV*gm7H#TmcNJS~{3OG6>gsLQt4)@k z6wm5dGjabRrH&^Nhmx|&XX{@unQ(Mp&D0wg^|bS6`YLV~S{eFp|02PKDy%njmTkeDaQXCAtFq&1TutfYQ+#*4gZZAD%wN$}R3|BJ`tSPG zo@R?6J@4<2+{7)eX5DYR(k8G?;mPsJv)7J$e4NU(^v!hNzF$GdZ<}&8%g*bYFZoGU z#_H7m4d%1gzrXqWcG-%fm!7RCvYvJR<&KLkeP4U~@?smB&GzcA{NaBu%4UaA*=`o~ z)B|g*mR~6S>aF$vx6dx`SDH+<57c}fvPP=UIlbb-=k+_2rOrAnzkFlM?_}e$)7Biy z+mA}G|F*k#O5@fun|tLLC*1p~df*YiP1Tll-Fm-o&%S&?>|e6Z>4iFfKFKX(wCzyr zI{I_olUoaff)22zI84u4@pL8If>XA0qGqdYoOGnxKBAbrze8h9`I+EJpL z9&~*-KCHr}aQlO-(VAH&HrQ*eIdp86PTfm|E7gu`9`Xce&v5e`56}^`PF<$+jLLvyT39ez98TUH7``*mt8}C3D3_PsxJIOLsah;M19xQ>1A7#ma5VvXDD_8ay>E zCe0Av>{uejb5^N$d+{+bZRdc%j@u_z&pkBb+iO9~C5PLaI(E!-(MWb%A8od;!A<0k z{-!4W=#s@bECy1+{0o~`X#ab2to)C#&aTZ-$ypl*w|)_{T{&twP(0U<@RN%y}Sw2U@ z4|OcRaB>-k%fm@_N0>Z6D6hM1y*=QS($)-(DKVZ&ryZtDUb|sRzmTNi>OY4Q7!~KN zk(Q2>Ietj4;h^`9J+tKwYRNq8~?w-AO%GCWmYx-l( zb}r&`m9l=Tq_SCI(@P8A$B)%JRGhC~=~_R<{rZyQZaOn&>&Wc6=&fV3^3JY9jStS; zS;OD1BfPYzqvEkq(4A$=O?3V$=x@JrE-p~}*&LtEk9xHCX#L*dGxNm>ze{#+TNXYbT^-E+*$il+7~IXAiMpMuCw@f|J~&kBm)7e4=+ zL1vn92TzPwdeCvfk~6bArmN09KJk>L#VfDr0!xn@?d?8u@%)?B;zdfX%KO$Sc7`uL zT)lFa@K@pIw}edAo@k2{{J(gH_T8QtlQ&xAc4$aTG&T4w;PH07+NH3z8yQP6u8{M>8)m_|cvV20{ z<}bXiN!pC74jla*qGsq*TB}ff>Bx;03aPhmroUZqE#~IQGn{!ns=0R+-c)Y)zkTh$ zw@c$yHEzrGO;wlH&W@_o>gjeAQ2TYX%INq~)?)&1FZyrn5D4w{S$u2dDZjJ3qe9o1 zxy^>%2GoygFNVpXUA= zz3%PFSVn^g+gZ9bF`ry{R#eYdpK{JObx*-Zb6#()@aTIAf26Bd>n^QY@z`}Et58Bp zWn|{pIPtlHI(y`p&Eg$-`VG#wi@lk8^6^A=X8=CNZipQSh76zScE|7OdQEyOWP?N{DHP@HgR= zJRX9{i5Vp!-&Oxi{26P@KI3m z^_<5?DiZ`>dC!|O-SXYth&Q2=3fQtO56$O|@A|OO?%2KEuD#D&)e0W>{+q+)ee=e% z)@|IJhHl?HpLsr+6_a9qjAwDf6UUax_gA`$?vq;OZF1nOpbg&{xs!7jh~6@cXkTzgYn9}zAhB*opM-niPj4)ORV;aU)K+cQ za`h={)7GVwHop8av0v9T>({2&eo{$$!yf!wv*gO9+mkoX&Rmdug~y1i+VH+z;L{Tq zR^BU^c5K@syGynw^j_%4@c}eq>8t2cW{z6|?ooCCPmTKYE8}Q67cfsWs z1y07Cb-en&UTbBtq<5^~`@69J;}Kr#&dp~O*(6tlcUzr3c1=qzaUDa}nQ2FS&OWF# zOEi4J*0Li1*Sogg$MXN?JZwY9*X4^W&;Nbw!o_V17}9UHCtHhu znU{QV>Oo1Kz_YCE&-1)2*K?fb-yk6LVVg$3v3VJ5^8b1N-cNZFBA}btVfRsVdXd=s zXiu{&iRGtXy?*}WmNRSpyv30nlRDdV{W}5*U;f&x!T!f$zjkKby!)#B(>GnXs3yME zIFv)wpW|PfPEE-pn`1?haT52Q9+DKDp;0wYeD;H@8XVTol;VsocdpnPGFe1fI`Olk z+$E+S!M2FxK+8vV95%~8{kf+TeP&zQzv9&;QuFUUXN}^YxA9n;E!XA)tCzm8&S&)B zQdDx`r;b&WmhRADwsC>8ZqDKU0SKbTGjPQ?t&gFMmuAlv* zKm7NOIkDG+W)~QLO*pFU|44}=p8eb3ZTZFWrI!P~irarPN!5I|=+W7KogwS$`hRa@ z$V~tEyN=OTzxQcxZpVtQSmpEAZavtiuztzEDf#l>&#A@j={r+CJ(Xq4)6)wtMZVkT z6#ll&um?|VO#_vlg!gy)&&Mgguk( z-DjOw-1lZ# zV$zzvSL$-rby41o>m9$#}|G>b@L^zgeR6C&B)^=Jbh2^;Q4W)jZ{>5|RFU z{lVg_jFuyNr@2YIG|R8D4}2OSv|vR#$CgN|cJ!f9ZWM=CAb4PTm;K zxzL>R!}4z*dnT*Dy!;>Y# ztyFFVESS-^L`+NP#Dug%;p-*-2~R$scz&MP|G%f*yFzRi>O|}1p8Bx-@`|A4L8&V? zYk4n?*k1JZ->yqbk51Omou-rhP3P8@tm~U{Uw_lRv!m$wrP$dj4;>7(M4x@l_?#cw znY=elq}u4D+XNNMDxQ;pNyqkTdz#*j*sON0b>`YI?}!lXkVR4QTzk7kbrR=Q1aHop zJ7J1(`cF4uy;=NOThHx^pSbJRtQ8Wsf&zEPY3@wA`{dMgi zcDbu|OndIr$+L~EezslpTXI)TDuiK|nMBcr%pHpDvQs23~dIIjztV$`I^8Dcy0*yNYF_ScMO8@oil zd=eITYq?0^dXDg!*%Q)AF3#F`%1V1}&eZE^o-Ic;)tz+9RKncZYD*?W9slVyFEnuB ziU8#n0k4_9)^XP^OpX0%9sC{u1HXoNEiU+fMVNor>~q&%w#=3NnwK9s zGb=h^rO}iX!M9&szcQcq>M#8RjHkJl{Y%x!G8f!FF}EZqrrIzkXw#`jTP{1fOWs=S zd{u4FM(2A)cS5I$_eS~JoiZsZQeG@sk!SN^!S9ug+-kr0HBEw+8G45qJvn3ju|#Ul zqUe{unX$w$JV!U&A|h>$Vu-YcE%YeN9`s;6%!z zGt0C$zxi}}v#aOq=D@3~qw`*_l9msTE^c3VsrL8Z@6kTiOC~<>73W)!sOmfS+S*?0 ze-=+BdfQn(of=(d`D|wPKFjBGtN&TPSlDg1>uSU7x?lI()fI0fTyor3_^3-i@7iHC z#jq;R)!RKomHkv7e#{j6YiINEsC2!}r<2a+1)(ZAN@v~Kj}h~_0}WnTeUwjxbSf-_$$L-zf$sSY=v_dpBhX2Z|_6eJw`{C zDzvFC%M?>Qr#4&u=yNl%gqi0%B&IIr;A#+*Trfo^sCSlQcB<^31tAUFr_=~MFORZR zlzH5t_UY=n2(y^3)C(P=qW8G;x6FPR`DppEl?#?Dibh?W*mZoXW6(d=4y#Uaw@DHX zDsoGuqV`;C-ZG_S-W(m_4TWqcA0^M$?mC^j@*%Gg$3^Ld0V-b7EhlbH5!AnZK*u@K zQh-U)b5>QV!1RBDT{6v@XL#3c&}~2bc=nD%1}Ek&?K@j@@YS@UMVWU~B8!@)FxW41 zzy4yACR5Ms>2oY>Hs91SS7G*B{wlHY<_iJs=Tadns+YAI!XhBqnNIpLNN4>cLYF0s^~TzhwICvOLrsAn>n8 z_V#nf$*V$Me0VwIu%UOthU3fhkGZG{XR0ZrwqKf>U}7!dEwR|{mGgRy)+PQ-(u}QA zQ>X4bBe{2Hki*&)o0)1)sc({*;H3UUWPX>Z(h9~$_Kj_Gl=Cjdd)w+Hy?zw3&@e=+ zedenxJfFGN+&Z=B>Xd5>_nlh1;almNxR=W&S*^I%6*0*~XQ`4;<)W$e5oKEE8rW^# zeD2cRbZtdca_1_W&jPVxqDqIAGm@IMW3~o}dhWc`=j9RTV|^;4eEo+flMSqdJ5shO zI`GbOcYl3zO5L=HJ&%IwU5+l+*Ub;ITp4!ujj?>-)@fQ*pWo_jR?&*^JZWzzqgVI& z^>Po{a;<-p3e_HO+GY33>$TCVlSihmsy;ilq-=NS%bv2$-cAp$PT5c{nY_nosk3z1 zXL&K7wR!&^N_Q4L-z)q2^=-b+iQ8sAEB{qnSRfw#C|B>PKf^N)O}S+8H?}OUm*<>w ze75AlnkT&n1!RVMb9Mb`ui<2yLpnY;a@Z+hYOzZj8{oom+Br5LZ`)_VL_ zE?r$$=tPR+q{lN=+#*>1?OY$mTfA00#vnQA!xE!C8d;*^l^0r`otmiHBi`%CoxaqU z>10Z#ap=>>M%u;Zm37+<4*uO} zH;q3+uCRZei_)}@PxxltFutE4aM@bpW=>GVwapif%zTsl-ap#=TdCgKM+yrqqz)c3 zmXy>iJ~yk)r>er@?o~}{@>fn7wx>=W%DA4U$Jb7(}@o&Cm3Xen4jfV5;5Pd#4H(Z z_wl{gGljMFnP-HIt}nAFojP05LtU@-pJvsymzs&rn(=ETU+mO3x?M5j?4%tf<>#3B z?Y<~VMzpV-o2=t7w~=SP^il287v;OxFgevWd@NAfbos`EmBtk>c@>YI+N{eaVDGw@ zcY4KbcmMAjeuyr0iVWX%Jk>ZT;gYBN@hz+=hcy@dTBr4MqE$fkqE#C>_x(`yxpU?C z@nnggp8iZKyXIf{UU6G-`IfgkFYS^#>JsrY@AH4dLMh|0wNX`{C+-b3g>R+l$)O0?LnieyY3LJuJ5@e=zGK?}L{D_hxVGbBy3E72u!ur*H0z z<8OG*U!CCUx$3;=P5l3xvJB@W808dS@yISzeDG)|2eVPz zT$504Ki-cyEtlSg=FOOt9umi(cAnkus8?*~RF%N36Ce8jIl=ZW>{{&oNrJ50Nl~q~ zN8Y8(HTuK%er)m)fbm<0>2ilbVeF+{zB{*vT6<-Q%wKFTW-R<*f_2R-A30_>|>ktz6KDxz8Ps z{NM5D#b2r3IcIjNFzO}+GS_-F+r$*kO>SBfw$^-I6MWei@AIkx$KNE)sla_^G2p$?;mH!>rSMSp~P}UI{Gu z7vvY@Eu(csTj8*;5R>8=8T(_??&f%%y*R}|?0V(j*o~$4f>*iiNby?iJ5lgyYiOL> zC5eP3Z@OI9op<588=cj{vhV5pCo>}M_|1MYUt&SGpf0C-W+Z!60=w7cju;-($Xj<6 zC#tiaoovH-R>n2Q;;z@va6b*_>!wZYGR(V9Jo@wZ1XE~q&IOg%Zzlx5icMc|is7e= zZAI+j!sO)-y1f&YRmk~P?r2F{=C=RC#R!ipi9GxnTK?w`USitl(_CuuU|bT`kbz$?x|8@vN|Ol=Z+v5c3q zw=!z`UT>R!E3YwJP1qXBb*6+(s&{$Ur1Tmg--|9vZ2|sIwx;uRE}b{^sVrxp@}AQb zJCB_axUc1Qr!2OoDkUV&=*W#7;;}|^S%2MPT{U%r#?!C0r_}DMpA|{=`*8X@=i*Y% z)}Nt5Q#NofzoYb`GQw%n2r8=mkhbuueNd9u{ed|*v?YBhm zv##X7nYM-JW|(b@#w+&vzBxa5H5PGRYnwOS@6NH)qKiGxv2~@roODuJPIji$&nH?x z)0~!Eo^kIoo9hYt6`HH6IvQTy__3saR_2rArlKjCk7^a?syXpIn=8Nl#IxRIlJ#lb z3&d8wSi0EHLG?(bfMrz6zY?QW)2#0FUOM*Qr-NsgkXy#8Z-3*b?+Wenh!l(z7cKR2 z?o$Z#$yl~z)lQ+yy|HJ{9qhd7Fhxzs?`O>85Ywp%KiZv}Tm`Q3`XPI|Eh=wllM4u)t+j4^T^L`(vrEW zPh_2N`5QP-vGWSU9hFZJe>D7rG{jvM<}PZuvGq+R+u1WyEmB-}oRydq{XO;UCKkzr zlqpTBtM8=-EOS*(J;E(+B&fFbn4addfGKfyE*;)k(*8LUwzdgxb6VE%Rw2EkaoLi& z=X>t)$sH~3TXW0F{p2Qrlr3(vx2)cE=--52b68SuWL0}Uw?A=-p>pC+S%KCap1n<>{-CE!wWDGyORQY$UYT1xwY)wr6t;H7Nv7-ZU1_e%GFYAWg|6Qj6x4FhS5n&BXr@a@}{c<3&69KjpHzgo?gP z39Ym{b}?2le2q7!+9{@&uY)@d{_~BRVfUtk>*y+pcV!>fPY|1R!=gneHrh9eK{EAa zb_mz$O16(~>#vtwR%o1@wQcV0wcfTh`fTB0C%5Tx|9gA!j?>~86TbPKYsD*PY|wPJ zPF1+#%OT~*aj*4poX|V*U;0fPQVh*Ytox=jdQR!cD;@Te2=q_mDVlf2TITSLRo!R9UPoLs_>$HkEMKIysHK`| zhG_PawE`zs#Tkmdmb{mDRL-h(h2Mm@i`~+9(x3cq{J1(N?$bah6WCs2%6D4#^ozB*}o!lbN zZdGvY%gfT2bC1p8@DY)B{l?!KRD!ROSozN3d{>NLOiC9gKUpL*x~n*&>wC%-W~eBL>A$uB3Jr0A^{-&VPrKMip?@hh}@ zW|G`8ujj?7P9mvHKEEd~x_Qq}rt{B=Q~zgNnd&ev{C4uTB?~e=I{(>~ZM?-2a)u@0 z_15ovU-EP}MV#4|ZuUuJ`BBS1-yHsIE7*U)a)C=>_cPU?O~F1-7g%uLek!o(U~1U* zQ`L@Td+FbwId8*Hp6yN&dmiK;T zmt)-a=6^@pDqO#%+)TEvl`)xW9x=r{>+rIRRh!icx!zQIy?Dr;`oJZye41qL!PJ3pRul7wJe@X?e4}5i2{q^3sp*R#UIpdPv{ao%?c! zfplpkhj{_NMWsx6%G~Ck>$07d4=d`Nx2k=SExS9CJ?%a=u)7$0q1t-j|BV zY5Q()sGR=zU+K}uJBpodY2Eveq^$^xJ6yRcYTCO)BJR(>y=63;r+&Qux|KUmWeU$v z%Qb##dfQvrqL=+R@^698gh`n@Ed&>yKUa{I61+8pLo(KEOY*HFxj}4UM`Gvs_WfBn z!?Y`P#YDBLM}eI)wthI`x5H$&Pn4(q@{RhhXRrQsOY2IMedoI<*<}y6MSNikI3IaU zbK3eFyXG_h^n7Eb`g+$Di&l1qXA(!*L}&8cTA`q%RBf(bXRX+e{HNaT?LNXA z*!oUR$MW>eKT8-7ixlqQieliOYnRi^IiX#3<4s9r&Sk;3K7N#&e^G_F!9jpviUyrO2UBt3vf>YXe7N5m)S8Co2v#?|>D!q1M z$<5I2jjK2L<;d-uaP~&;40$`roIn4Xy#?3vUE)ZHYS_Hul4h8jf?|wK@>i9Ei)R_D zhFf=}ou9ue=V5<@-gYA$ZM&3n%U|7y4lUnp)pX;`f`}MtTy#D{4{i5dc zb$G7Cv7|+J8pU)S`tOtH_x=6P2}z4X_`0HfwiGhXy+6nMcg0>EX}J{(COB=5U^pdk z=Nk=!~R@u9NKkk;`h_)&I3<6lk8B>c7x!s@2r2(AD7^ z)2{BCnjOAA{$QJ^^|zyk#d*Wk8YN2}oSDXF+<0cn%Y}y~C&?8ZxgD_3b$|Wus0|;S zHY98nXBGDIiFkYJP^a#WQcH1tuax5t4z5;T7Pl|!>c5XUno%YD>X(^|Z~0;Tw0lZj zYYwW?R%pB)9Pw}fB*RW`u=*Ie^Dzt*H>%kJ$`cf zg+-6rqwEE3GqeoMw(>Lw7`$bxK4ZwKSgm6-h40HcTmC*t&lQ)QgqcmII~nV4xtw)- z!q%H{#UItXZ)kD2T}|jNRDQduIMcdy&d*N$hhm4k-J0%gUSMf>Nx4O!y&*ee-K15O zTGlhRoYpuoukM$RdQaA!PWNj;Hu}OT6ILEy*3-r9X|*e^kXPv^+oSX6tUsO!K5zRo zyT`Q6Drf!9-CiPEJElrB8LMhk+wgdENEVkE9@bbJa9EB#%kR|Oe~xPNsvZSC6TMhv ztTDeTEaak@Pv|be%eC8zw(q89di1EGjbfNm0fvm%E2|$W>nS zi+;QH@zU!{>y92UkJo&7t@2fqT`=2H1xD8I2RYNjA`TsUQ5tdW(79D6oVDMdhR1hE zTo0El{Kp>T_PE8vYo$w$?jf_(+}BIO!t?@fo!cs(IX7^V%8NOl%|H6(om9#7U-vgf z$JgiE{XH_1Iu;w79+fb;So4cLY=wABky(H(_w|bAki2f!sYy~^oAvjr=+E2n%qjfk z&PN-2-+nx)fB(;h0)NiDYROk>OMG5^(_1>hQP|JK^!!`fMb`^ugb!^~nKJFT$V-DY z*)v#wePyct`dXB`rZvXqLPv7VDapBtzD^mRWanMrDOtZV@LZPL*49#i24le!&s&_; z`NUY7H7D||TQ|91_u)EQ!HZLt?Rz@U=gMM@)0&;D?l4JkKWWzIojF%x&ct~Eb2Cbm z7tej>6}okCjbHZM!hdbMmN0AW-;>||{PQF6gBAO}EKI58es!$q6YCuPr%#F>T1j#L zU%2rd2dAabs;Py90QOQg9(nwQx;Xyo1VI9My`Dwk)5a`;*U zn{8cBwpA>3inA0{y?fiY%&1*^USEe1`qhie<;ovt)-zRvu-k zv{)s&c3E#=%na4L9$G8kF3DbEB_RK}xC3VqEMI%48HIg9<`KIdR70Z}! zpLu1+)5j9?R^9Jo+q5)JNG0@Pj*H&~3ynQzTGM(hWX}EhVb3KiP{gIF%sk(|(T(o0--sMfprhHH;S#3MQdv1AQe~8q-Ux|+VR&goL*E=?@oWT6` z+zG{t^WXH%JowsTn=!Xjl0)Q4mDCWHUVantUR|e^#}Xu+lKzIfa5{;LXlrjSSeo_i z-ki8*JB~?PE^8-v&k$=|v#MnGyqS{|C8Sdl{C3q_@hyIHCp_kR%b9A~6Vq$vZr17N zQ|G~~db1??(cXfx zz9NC9h(d3Ty}s{fuz2lU6rh z+-LamDUW5-lH8x06xLpe-Z+2n$G=Bs6iOaUH!aa#JVo)&P09bMZ?3%gm*8i)$tdaY zkEWLG-xq-^Xxgr+rc6p1!{N_HlKR(-6UCwyw6{+Cm ztX_vUCu{3$?lJrDarZV3`CE>4Y9@Cr(#$_yhnnUm?S+xSIcI->sfI z{~oE$P`l(MDd-scd*|(^ryeiMdJ&}0!1}>1jNd}wiG@^>ah#(hM`I)h+h&z_)&}XD zm$`4TQdVZ;&^KWz=wprLQMOugxV%K!LyP-Bn2L#z_OwTQ@22&?XY+cuvIT3kLU3QtnqtaA280vE%=L-uQu_pp87xoDu{)caxH(^Vdd_Z^-dS)$A;@AEmy zLs4c;>z9Bz1}?k2dY#<7j_9v^z2e2r*Vc}Nvd{od;RKBq>ZYaRN{_x zA2bw{?r*!G5%KnaQtC~=*IKLGcrH)4;POV|W%IKJK7FN(`CVr$VmuZ^EIpT^uH=&X zfaQ4Nj3hOd#g;u>dQI=uP11hMb8WvT5X!}>9@l;_=E9=~es1?R{```a7Ps`e-|;LX z#o7tIyUuaVe8%?2F7@&r&Sbw+D$|rBr+nb`cQq(^XUySZd4lz%-_h+&uQk?boV4jm zKfXqxQw`|XyB|SH(c&`60a71s;lT~|~cle~JYw51D(n$3=bW35eVZn0elAH%z zNdYnI6rnIeTX9r{_p^bgk9pmuWo2%eN-t;ZcS?;G3pJY*7sqV|d=f!Ds1RJ*6J zI_*nK4)V-iwJ@jW5V!mr7G;BGJLSNP97T_mECr{migdG2zO(p>tvxp}RJH}o$ap$U zSi-UWKnqi+*IUDi^~{DWNj+Zw{9+E><~!->kgCvpu5!aEaY64h|MIP86)(J@sWm^t zZH16o^kfAd5wpn$96~2MZr>DN;>|OKaehj%eg`wxO64q#B>l%7A`{L?o?}jyN=o)H ztYLBfJK@chG-Lf$jIUdqK3F{c)WChHz(+YCAohtdi|NtIEBf*~KD+lD8vcKxT6)M> zz~I}tOqo*U1rmJb`9iZ*o`@>3q;KFnwdZZ4!y>Wi&u+|leoo?2fJHMCBZtZ7KA)NA zO)`}o1-B@~Ty~7U>^m`QLfwQ3oAMnWv>ZK@nm&!G`o-&zDM!WbGtZHGe@llUWKzz> zf+_kjB^p-*_1&9S2|Ws_(pBDc&g^s*deX-+bHlr-ERP>v(5sxlvDC=YnCCK2Gj0b(Ahik({EpDm(s}$$XCfy>d<=U9MXubSm_&Fy7+DK7Grolox4R z{WZDPpKjP>>D4*K&oel9S*GdrJ3qF*mOF96=7HzDRptiEm?rj{?Q8K`P?{mtR@(KJ zpHuzfpN!N=%0D7FuN7R=Jbfj%-BjyoS>8<1$kyw5H>WuM{kUshsP!(!^0t+3U$P_4 z6kWDy&R(~o^oeomE-0P}R{0em3zrMbF?&t!u?KSp2@r|k_j*I@8Z&_t!e9ZXY zXZ0mZqvW3%*cdrk2+U*Kn7N?GIxk%9ty7lCwQFrx-&t*I&|X)OyE?Dv^t^pRQ~dhw zG#uK*r4nd%R_)yR)6X;vUTyhz#XobxOBEMq&wjRyYURMA=7sL7t3i#)ibwDy~dmtE|I@wTZ)!a9pjb; z*=Y`f7LD>^zZbCovN?C8#@Wa3m*H z+WPiKYx0k~41FR(CyzKi+U<4D-Qu!7TZY*5LxH!1wM5!psCcGEzpwLBF+EZ$EBcRr ziNoje&~%gP-fYhsU0YKPzfUw?b20o>(*9421f4FP3+WV?#2`7ZFyf5Am7lo4WM>UN zrCfKFP|-uF$rtpv+B{s`eBM>e_<7{wl?}~D9VSj|5$QT)Te5xn?1*#)C+8p!%Ui+D zFIIZRF3o(=RQ=*>TiuI?8Ph@%oF7@;++z?q;p4lmW!x7E~+Xy$E@aUkMNrQXUoKYEK*AaUnOOgby_YGJX&+kLiG-_*N!%o zloL{mwQ`adMU^ix$U9SAsAJ2`G4I~zpC3FMm6mINZS8*6qFMgTxMJJMinUowW|&R! zzs6Sae zkZCrv~2Wp|J2f2ua{~NG2_{z>{_W8 zk-LLfcqfH_pY+_IBP@^EU*B`G=ZfXEv*z*!YOWD;o%3+Tbu)FAH?~Z-_J3OwVdSpQ zTBVlubHYB>D{ty_O6{4duQeDeki@+Oi!Ry(1eMV z3Cv-yr-Ug@dpV~v&4+oH$EMYGlNsb*Ss%ODCTYyK-;#svr?h8RYq(CzT3uhi@OA5_ zW@)WlRcw*H<$^OTLR zwZF1HTadDIukT{sMJhTIe^tFsi%~fw`{Shko}Ws8H;ONH-}3vEA(JGFh1!J9&pApP zgV;LEjhDYYRCaZ>;|xEQR5$DBziTef(vjf3wwy`TyKGIqc=qEgzo@V0UbxS=*eBii z_;s2}y|xv*ZuB>oG}jje>htEN8dTnzedprBm`lC+r{=zyW3BRgRmFXM--e0)QHS;f z9@lotIOeJrcW3|Q>(OcpuhrbkoTvTauZsRNi+>hv&NI@C?#`&Xv-0Mv={G-3KOXn5 zP&_5FNp+L#))tPaQlaX`$0jQ!*1!529kKau@9EWkzrUMKb1N1L{2*Y>EWS6o=k2Bo zXWqPB_pI}0=Iz#F-3X-iq`LW{2Br}yRU z3QRjB|5kH?!M}6UZEjz-RGszq@tXB>|0F$kO*7t-mR+97o4VJPZ*LlVd9jbS-OU+q zZ~uE1>fisD>-w>a1wZ7xe$SrpvAqA4`H_VJe|Hp|ztxw%_sQZrPA0qT!X>0{EUKQX zI-fOG-l9U&obhml(!Le1^ADz->vK4ndg8WAPN$le)K|$=tI+@d`mR2WePCymk(^nQ zS`fN;f3j(Z3jh0fC8Ltl;Q?hQJjHt}F6smyjm=#4gVCwyKx5J*6~zt%MaO0~Zn=;b z0*j7xim=+P=`ehJq+7~5YQl*N54a82PYMzK{62nfjXLYI)8QZD-J6U8^-MAzCLawK zu~eJ#Q%KE!o=K(IotZ}W_t)spva01WU3Fz;$m*!QU8bwAt&Q1T^_R$>6e)1TK;zb5^wWwhNs6` znIG$)X3fb^4O<;1toHgE>({1zKO)wB*{1owcWEz|)>K+V=VaY< zKUJ~*ip#@tCqB=ViEdIqXOv~>{`9K+&+Br^FKL2Ps`3lLmY(7k2RDKK$EhX?}mZe?$ncl4&V zr?Rk-oX3X9JMzvKy}E_Z=5a}Bxc=Ss<&y7D#Ve894|hi2I{A9uZ}qJWa<&sLob$Y? zx2RC-%Z(|!54ry9I;qp=?p45Pxwt1tX5P;y4%>armsVYAWtr}MGss2t`h<-hsq39X zw-?^4dVh9%$acQSZ7vIrxnJsS-E978o%vl+p`5cuF6;c5_xY^5)bxC|k>f;7^Ou`v zdWtNZZ=WxsciCLMHt*@Rb+5l%ez{NStAllQ)tdlMi^Csk3U8LZegT-uegaV`KR#2g1K5nXCdctC&_hxa<%VV_Bbu3I``?G)`DeO z+;vW$=O61#jO2=uaq)68X*u&WOS$+$;R7S9%Y`R2|1irsp4e(~WhQs5*A#!z?@Ed? zp@&t3SMlt337zuz&6ZQG; zt2XGy)Pr*#%ZPAJoBHUPvZUY1>W`-aAAf4G`&YqNt6Ot<^9r>cFW0CxZ`qmTeye?< zI-i%}|I;^LO}XL1uXw7hQz$HEnY!=?#^t%z-N(Blgm-uCQBl!KdhAt}IpK6pmLgZ+ zN$)4ikEdu|Zh3d<)Y@}`QyrZ?vtC}37W~V|2dX>d8T z<}~MP9_uQu%nIOq>Aq3Z z*Jst`_Tb=a-v6~E&a>)G`DxjeBhjR9xPfI=iIUcipqs+n|17U%$Ynimv0D@(Q@bo- zeMzUwy|xJrnR<$rg1TODn}n7#wJYZI1g?LyQN%=Tq1=ZXS$|t9l!6v)l*@g)HvjOY zx%!b2tL<%eNvu>zc#Bt+&PxJ(>XhwCg}41E?OdQJWFuO)T(|5SXrHd9DxqubHR zyzhNtq+B{mwq@`pC96GnuGsW1ZQrEK#PF%cf>RCn*Bs(}U3>o_U$eqLI*=xVFNS^BIl7k|7{ z{J)e}-3W9!pN+wOwI!PI0>P@yD{4Oioxf6!A?H@!RR!wrQ#U zTG>oKp+kapdcr4fFV-kkwz~Tw@aY|s1uRnE^e090?Bv^IrIpp9;h@&Z>OT31@Z`Qy zow7Hx@3$PCbi{bhftBvl4zbKxAQGLE?wjkD^!v&Zj+QwmSDPiP7lkz+QJ&p==7{Bu z9dB!Fq7){Y<$A7ce_68HS}Wue-*>ib)A*@U>D?z;c5-}t#pJXyxN_3_`D{~+!=+#R zn<6B&?YE7!t^2d6>z|gS+ugo%H-7TWEu5z(#HJ|81PL%nPO7sgnj$39@bb$op-9dH zIZw{r=-RgB)Vi&2q?n^kx^_!F|6rz>b&n%O!&$9GWr-JC(52~Xs$X#AZTPlhabmsl zks`J#*Oj|3b_Cv@V&@Y1b<2crE>{|TI*v)IIZX-P$fJAbYzx=N!<|}%y(`Wa2UKZz z76`w}mOo#y^6FXd5blX-v;Vw17VPQEwpseyIjL=Zbtf#Vri3pFsnDD??O9L<-_Lte zdk-cRUOO(d$f}sLOK-`9UFDT2ch~5%HF)xn7qUp`y(-Op<2`@LN5 zb-(?W)kLrzKH_C}?4HN$MK8DhIoqYd*04mp_JFabbS@9U?9%P5M~1yfI24+~+{KBttTzrjh#3??r||!Y7_)+*>AhPmL|? zTSl&-M?i?go%AO44$%kSGvygwm$j8HDXBZXQEex?>vCZS#wT?w?a_UT^}NOg(>}%> zcv$*Jyz2M&G?{}gQ%cLS%;MOWJ5Ky6w!BD4L?WtGyyc==Io~ABs}uRfE*P&(H=8F| z>se4?ZthXU*u=|R^6z;VC#xjU8E&WQySK6I>F zB(#o0HKyEZ^NYSa+jR;hB03-SNU1B&G^oD+SSoLM2jf@apEnA&KhI8a?_Bgk^UspD zXJ5<0RFoYRWqv-8))I15W$$nitbdpB0r9^1M2z`faPo z^&dGbj(*M_$=o{?J343kxtTptb-D9ziCoJA?a0S7`h!};%!L07C@+{K!Z2Cs-co_I z?LD(p<9>gye=|dG&rIuWoXRIY=;eMC;F_fLQ(RmnS$2BL+=VZsjw=+i3QXHHQLa&J!BHs(r)f)m&Xd0t7Pm}l&kP+Vi<$CAVs9|T?|v9!qp5RWwNP)D zp877SJ(X22r~6NT=soL`vTT%(xaaKHZTeb*{H>ljs~8t~&I-IdHEmhaf)!V!HoPo7 zq+({{x;Qs2Y3@r!Bd*0Rsf%|^jXY8&`1??NOXTE#iBh4FE`nZ$)<+nN(ss2tX-Ii_ zO>(~;HuZ|0Vabx$H-cA$DB26Dum0Xp?=~^~l;WkGF|5Dlvq$+D6`5`|7H2n9FPJ!M zfsr*&Qe$(JPyQA0wA7{HGxYyt%q?|VuKcq0q>6(3%2kd}7HhWJIUVh9Epy1-ph2F=@C!W3LrqL*TGI;jpwyBOv zYk$5H{qCqTb;_zws&)r|aVI=4e@dPc*oRU6WO8ds#R=(G5z+!3{4i{d(wk4uv* zMSgq~tKF$9Wnw*ZvdA0{$1c?m{WAn_pPAq;cKhn7Ym+0mBM7@&n+ESwrGp5_qw~K8+#jZ!rmuD?AdmS2zixqxTIpW|EGoo(>#`R~E(;FR7}kI?|!*+Ga2MyTdmmVy@$E+kZb5cd(gc z&-6ZQ(ZeO2RO2r2Vr9f}$33;v9V1LrZi(@Kikvv}^qdfjmHQY|=6CO{y6jZhC|Ej? z<*=t@N6Nb7uoN*C$D>R1orAaVT(OT>UdqTm;dk+@&)HV9G9wmq&&o0{T-cH%!8qd{ zhsv{xh4zMhznKhvE>+rGsgW*Nadu_wv#qG@Ofnv=`N#D0D{26S=IBW6h{a0V}L~HCwY}h^ z$=`*o*UjF*Jf+29@iOU6>25j#49RZAlSEj)@PBX!Y%n~){pk4KJ+f+^o}QJu;gRBx z+xvSDY}P66Z=d4I!lqSKB|r1%LXFMaH!Y6h*tz0PmWGdd$FrZFMKYaJrmAnbeyI0H zkNuUowNj@VxR)H)T=UN4bp5rpx2El3E)1(t^htPeI^SY*&Elr%;RReDw|(De`nf4+ z!p~z_TqnDN|H*F+T-cW=xm#xLtvmJ`x0x7JSiSypcAkk)>r(rT>Bo{M??@Fn`cOr0 znWD+g?XAl%hMwl`v*nq!Vrjs^ao?(xB0RTq^5|4emi znSXy~$oCa~>tJe^e$jxq7eOT#*%Gp*jX*~~SxOIM&JjvUe@1*3} zY&mARX-8FysC4ow4-jszkhaz>x-m);?)q>WGv(t9> z_(r*(KH=gkqVndHaAaYoOF)&|>1~4Z_Ni=IJNG>EMWfqBx6`G!-DRvxIV7Ibc++5u zh3OlOh0m0Crq5d8vufp(NN4VI)+-}zUT!bUTGHutN&nb_^O1KJH-}DqZOXxX=nik_ z51yJN;oF;R@9DqRtLfD?%kJHIR$G(VrK2Rs$ou|Uo*T+Js~0wV_8DHR=WdN&HEmb* z;my(yBy+v$IRY2(Ed5&><#*LMYoX@VX?yg8FH5N3IdA#UbzV~gt7*N6>Zk1~D|V|+ zIk<20Heuz%L4ub=y7ykx`uFH?;BLKt*8gJd8jRMNU(}Ym{$$_HSzabjJf&y%{q0ED_q4J5vGzH=cj{NIPIMSV z#pc?Y3g+gX`d&KqcjvL|ewvQYH)T9@PnzVxH|1O9i51rRH7z%z=AB&i@BVhHxszr* z%KRz0cA9a~MYZK+%E|3d|LHF0n#gaMl$L(gZT^j_2Dc@p*IggoeR=wyh?-=qyyLVI zNAcS!0!xC#`GoJUv0L+X)!RLKhlHwM#Oq#?-8LiX-#(+fh*+y@Cl${ht@im}y>su8 zMa*;7cu#!!^TDo7N8bFl3;KV{IUz;-;u%w3MY~cF^RFBKy*66Lb5uic=C37p-@O&! zey~CA_3IRe7Dv}-&ri;qyvQSZ+VqPJ+I#cA^%Na_=6!Bi)DsqwLYJrWkG^-^9=33v z@bc5@^09XhJ9WeqUkSg~%>C%feAnY|);yi?BYVo0I|lz`BpN^Zb-qsOd3{Ez`UaDl zyUxW29INbnW@y^aE*8o<`s_1{)=eGKUTvf;;<9sij8 z-#=BFd$#UtY(lTpi;lxAf8xHE-gRDeI3i?W;pBty_9aggza`&aKdJbpt=B|IE-KDR46xL3;|Mm18u7hXUE(!Fl`@U}8FWbh;U3TZWiWiywi`{cs zTEu?8_{u=%Q#VaI*5`HAKka#MxL?|Q;(Ix(c&(YDwhuRNl3e(c>(EcThd&oD^;>w} z#Km6FCtP5~`(LqV-Y&WPYkmGlGrwc9iod`4{JuC}kn2q8&6lNn@}JZOPE|58dR3?< zZ1n#cU)4ex@rt=O-X#WZV-vV~glEmStOo~FJo)7Qe9wRA+|Do0Fj`{Hj(+UkQoIRk&Ps%JabNKgpi!dPPp z;+EZAD|>Zr`7fH~qIGIT)ttbdDL?sMyUuKsEwVNJoqkp?T6Oa2-vPTluHA~M6*Vy}G|l(! zbo$D%tfy+yWsQZOZucHt;<59>skXjJ-8ZaiCtteYEWPzxRL@Fg*#^B|_@pMaM_=vY>0lng=*3qzhs@m79~ynXBs3yY_=>@UYZ0eK zx5RQX|5(;;UgWl-hJBT~+-bM9YF8WoO|#|l-`V%q_g1CxrEe)yg2O6T?_c%XPj79T ziP2J;dK8;@2dqhlYR&!uITUE;WT%Tu-PkP zX_?86rz)0aZewuWwd?efk8gL}OiQ=3i}?J8z@W3^Mr%kXGP_Z9BD zlW)~HrW>BBo~RWh)mdm*!Rz`aHY<78*(2KQv%~hQu=G|H2Q#1R{A{Qn8lZA{%f7E) z=5pBU-`FVo@6OiK?jc?lr!DUBD09ycy11g-%x-PogJyBN)V=y|ZwImJvd(ezJt`5i z=h1D}9s&Lp-@9+!`&{@;@fq9d6>skK=xvcoU&3R``R|@%dsUWx^4&dOF7Q6trMliZ zOXBO|V5=QrD`x-A)wr$ct66pMQr#>jiDMirHUA~zm}NV3^H%EQYgx|wbMA;u{?y}4 z^Do`jj{Cq>6u7?X%tH>%B=xqeE6qwiE7_;ZH0@(p<)1Le`@}w`G?mFST1q}e%l?%R z)7g;wqV(_K$*CG*&V3I$Bb+{7_U}raDRI*^mw!o;=-Li*uG%Gfe1}eo*-zzks#q;} zRXM!K;*p%ttLO4dR&?CFH%+34=V+!;%=};fT1}Z>8uUlMPL#JT-hb|ISLvH#&w^Q| ztIX4mbo>+$IT2Dm^=`znV5yx+s=Ar7-%s>9J$ZRG@m%_JKFxN4e-9RQvrlQZJ$CbH ztg+I3<4FdKS2?(t&rnp6RZel2{&(D``c5L>>*IEhN_^F}s@(1rSUK~|4?X37(lhVn z6wU1anO6O&>zwM^%ClQnxKD0~>3H;nRq^N#9o1-;gp?tUW0_uF$OkMG-wKc%19PA{7J@5Z^JD^r&KJCnTVZ6nKU;iWGk zY%I3>rX4YG`m89vQ&Xaufz9sgtW(Krl(sGZ=vlMZXzDGVS<7l(P3~iO+?m~^d8TjQ z7h}D@k6rH-drjqAvdCHW6?^0LJ?eXp%oS!*;h65cr8UIa^27@9sVO3ky*G@PTm5sL zEZf<+`hq}-3G>nTLpxrrauM@06BG1WIZgA*qGMlY&0G@g=02IF?c2&i=14vX-;*<* zZdsuaW_%^_%+1*g8#b*;mkscF?-{)D-qhLKe`+QF*N{4GptrW@=^9i0u8VC?L$fC3 z-YH_)yyQ!VqvXDi3LCtph4*dz=(u@lZfXNzb$W z`|_UD4&_=urXIfR{d({HmFf&n+~8@Fd>vPHR*tjOw~1%(?WnM8D~*kp@$O2kv}Cz* z_}i9wvwgORasSv5*>d5ZO1QX;(Bz-f_D%cne%|vlYS(7m+WpM*{|epnw-`N21wG%^ ze)P^MS^mTNQ&;$22Cszj?nJvMTs%ytcP7l%_^GcN%g4D|NhtN+x&Hh2I=9YOciAjd zv)B2WqBa z`Pk1l)>wbHdn&P_YPT(W;dUpr{&~ksLK<0L-g_((ysv44`p0!qe*|LX%1)d&$zQb0 z?Ah-x7cIZ0Mb1{Yd+uL<>$u4ynbY>i9cJvWF*LdV^UIODA9t$>e4Hhf@m^8?p!oac z>x5#i?q!?%q~d?j{r?6vr=^OQcmJ=`PW%0aV@j^mImXGqZ~s?#KBMQ0Id5(IM!O0| z@yz57p8z&LX3god_NpiEY1zGc`eOmMl*ylit!HJ-&<>obaLFLW%f20oz zp0S^Ef`x%))1gAnHQ6oUOoB}u>!(Spm`l$%zr!r?=Rd913cKgY`{ceHZ1r$#S$R-O z$3ADX!>ttu`XnX!zBs6h$oVo#$mN*6WafG5Z1mr|b4|p-^B?3=8YcZq&}h>-^#8;H z!OI=SJnNc1+rP{{{C78p;+Hv@+UrG~otB+i6~04YUUJ8bYA1V^wdWt$|C_vO%85hI zviC_O+Rb@jG3((Wy%^DoAMDqh`Jc?tW8A&<(}CWYT^!0Y=eH_MniDWLYBBdIt-~*> z4+s0WhVmTJnR1vbbIZBSho?`_`*VW-!^6Y&XIyt(TwcUIoM(}7w`5*}-7i8o@VKGT%E^9GR&5Zd3QZ4y7sG`=0&o@C)2Lal?Wvhqk;0Q_3DGA61Y) zm@)a?PmB2{CN!=(VW{ZJb!pZ-1r^ztqlGEX7BTQDzrBy-~$`o8f73Est$FzU~`$`z%=HHKk|q zh0`r3*vq*zr@mHeDd1UEpmE^$5pFB-1A#pE_Gqo@nY6uIpRw^wiQwEPf?_`eRXBL; zeVTPP#<)kGnZ2OP!OP{Mx+w(dlOww*7ptW2w{57J;1y4;(4G$$fd^sVffR zt9Ee*<@DAEEDUe9aGT6MNukeAN4O>Dgx+ei)}LlCdi*Xt>FC$;u)ADdjdA|Z4$NM@n- z)`JWSCUDGFtCo;mqH9$WXf-8JWy-ljGdb5B2sAuUe zCqX?Wpl$N1wv{s+XEo1YxEfS*$^{J>bk;Ey8P%Gwa@Fpg0;cgU zY)cw8&SEinV00qzx3TA)EfKw(YQ}m6zNUq~>%6#o8+$jGEWP4!cfyx5XWgsAJKuJ3 zTJ2v_axlo-WM9RG#W%M{%5IIgWwFC&;d@Vw_@iObu7Q3}eVV&waF%qns`mC?+2oYE zu5<6rfMQPTx$_j?TBtvbxYN=$EZ}(;_ zysf~x`m@ze+wjnz?VUe)xi|KdH?-Tljj;`lnI;-`Ey7>*uD@i{jn=)Er}joN|CTg9 zv|5ZiC{NWXI#8iYkonM3ju{!VroO#$B=oK#OX!6Qv!15-Us!mX!zbdxir7xytM^Xo zZGJUnVd-Y?+cVxqs^6dKud{m1sU>lrj3d70-sLkDeQ{UjP4AtyrM9n2!*9O5>Xf zh}dx_{K|~FR?**TqaGbIb@JA${=K)4vIgiqRF_-j(fZ;1Wex#pw?ks@I%Mj??s=1ZB9UME_CoB$fNz_(VYmW|$Hk|zTjlX7@=u6$J+I=^4+^*=B^_N#J z{rcgN<(yf2xNb5|6SVcb$R3kmQ0T`N_C&FG$M324w=V6qNxS;kS$X%ByE{V6e*KuU zpEt?#jG;#2-2YRfL{pzU&DF7bV)k?M+8KKvuUY%VdG8a?ofllTc^?s0(rP#3LD8-8W_w znPp{^d8No@u$HAuty;?#^Zee@gyn7NE03LPT;<*S=)S((+VaI}*S&SEloeBpI7GG5 z|3%K+tJIQZ>XUtRo@u9Y#`VH^Vz%pdo;&yO5=X?-j5E(-93MSfo4vGGI#r~~RM;^2 zWNdcrJHsh{=^I5hdfdxcfBAaov^OX9d^c(JgnYWV=iB|bkDKSeyI#`dDCE6llbpmU zr#DB+Qd-i|+9Crdhgm!hyLvP2NK zf1A{Izta&{?$+J*?pJ*fQd^1mDl#j?etYovnZ&>7H{UWlPt4N$tezElA~$w#>KC>g_+M_Vx zpVQV~%kzuW6o|OL_>0j;xAH?vPdl4M6?1&&IU*ns|I6iXB7afzv}MggYmKrarfdzU z&td=8V{uOThU^6A_Z#Xbd2#;RrhH*#sr&yG@3lD8EEQ|!@~YcS7r&PO{@B#|Z9C6K zmU%Aa&7MEsvaB<+d`4}gpIz7YC2j?|-<<3%Sx?J zzwU6t`*^v@pA9o4zN_cm%(-8|zvR`+L#vr%J|33NpT<$qBrCszIe*$B)%N-K?fM}RqW<Jl0F?)gIk9OL8N&(F-OKART%?b#~*%%{)da^!a>o!t26^!;Dw zdZ!l|f3;m*n zDn9r5#6QLNK8L#9-}rOVhvU`rXNfeN_~PXwWVnQ_t8%Hp!WnIR@^(H!0!pr(e5z3@ z9f66?ZR=NcuKd=ol4L~PP| zer}$9GoNmiN#^==Z@n#Z1e6X>6*FG#cXL?2zVfeZMd7 z&QBL{>H3(0^?#1cFb!u5uP;gsU(m>|G-E+&#=OZFUv zSLV5Sq^e!7SasK;xpYCF^`{$)`~ByBRmv7# z_AN|ESnf+e<(H#vx3c&|S1xIP)p$Lrv+-+MYEd|!+7q9sUuwsCG`D3e?o@I5vUpbZ zrkl&smj`aS5WPh(B-%ZIiB&jyP2dl~q}oEuh-r%NFlnYIVKc zhwDecvi(fbI!&{yJa0Q}7Cd&Xb*I*rbmzB~CLwX@hq$gUxO%Ha*R}K6lBGS{Uw!+h z)w4Qo*^CX(7tPko*mO!tEnDnfNBY9elWql=7W@4U@2z^vqPOmP)@zx)d$&r@-y!N~ zdjH_#It%TTS57h;k3^lkB{A{CHA$`Y^*1Fa)ibkmZ7OC=mp0h`>(P(3)jR!e83c;? z%4|MkByOEDpIi3i#Ld$0mlgInZ#=Q|gbMH6n2T*!*JPabUpq(jG}C#Rtyi9_@v2Td zt2Kp1GrBxRWN)$Ak9n&?o7fFbs>%Mno)l`_vn<$mzU+-F#g}6>Z`TOT+}tW{;>qUAeX&ms67|}iFYEIWasF?ySfIPNzH-Ow?zZl#S8Lg3 z{#ml|_3}OIw+6@W%_ujoSaP+dxbMQ=Bh$4lKDBti*W1QgZ?pat*Yxu{zP>*!FShYS zciz4)$Ao$BK3Hqdw>RZR`D@EFoRPnCME5f*8E88)DeR15llUO{ru2W%oX?N88T^{? zwuwJ6f#Ln710tdZjY={LL(W~=Q@L_Qi56$5kb;Dx0F!$2;zRdOubHwcuy8_?xMIR@ zj~9mpjoj=!SI+;R;d1ya=h>*g-0>fOF*}G)w1 zRp<4NK)fuF-2r^fAP$#x37Hn_|0QH$MCe;EthY;=?1}lTWXfcCI3AA zzNEaZ zS8TURxS!HkKVNF;tn!$nn;O|d5;MJDINkpD(x=_)bNAdiLfWdYgp+OttG7BSKb(+! zaZZlO^8JsVN``jMYUSLrxTxjvuY{wn&-S(%%9yD5Zoe^O_9K_&L0d)qvpYP~pBZva zyOLtb`*i;y$Iu*?qv}D@o36}MGF*`o9PhUL)iu-5uc>^oE5ZV~CeGyyU1ut*9XVC} z+RT72v#e!wlmfqBJN!#yo$Q;oSPAWkZDrhD8P*w$|8HHcICiDPXlDAUTU|HKt9fj6 zIhgFJSgmpH+t+DYva3_~t3>_IEz3EeBbbq0z2N#Ym8Bc+yvjUy>iV@~%WI!(c{a1b z+n;mWsconB`f0p9e0Arot8*2Zv+HN1UAaGPy?SYlj+@Nxf3X#pj9bq*uUj=O``$~F z?dB6J_2jlkoqYaiOSHPs`X%d5-+J@z+ssS0DgX0?&wOTzFnM{^_s}Hm`z4Fc?f9*H zb6HvS$-h#Y_p5LlwAxR;dFYGxCIQ<;P5tgy_!cio`ngoKWu<2HnT8v+qH!Dlbe+1M zxoK66X4s?09h2jI7ERfw=l3Wm?aU+j)BM}@>*Ri?7eI=kdccA_~sT z+{oCNle(6t;)J2zr@t9jLL0k^RK4|1c;0<^W_g&l^2WIzrrz$EdL&RX+Pt^8W#^jk zE4<-b1LM~2p5UWl&b&*@=~~geH**s7xwjoNm0A*beUIg1Hmggw9AB7SF5HS~rUMeGgiq6;!~sePvi6yZHGGv)Dve)m5|qOue#nUeA2v zTK|P#t0vohEu9zecec&)Yvwzq<^D^Un;pykOQ-FKX;x@bj@qs1=be{rcAoH7Hz7N~ zWrLc*7DnGW(XLtBzVH0@IgKYK@3KYSJ-PeVwkKaNGh1~h&&}K1F;{rSChM+SRptvP z-(9u4_V--wu=8u<7w6T!-@R0B+1r}`H(WPnN#0v?`STIp^5=JEtiP<^STS3BTDVfY z=7ZH)3r|(fcp+Xq+px9odmOLXVcF$fPr~I)_GIqM?fL&F=XPc2A#J%kFD{-!2`Cxxp`|FRHe=`h|7!%zqod$a~)|@V|e}`Dbga?4g)@aY3I~ALm=# z$XRgbyV^;Q8^KT4v;R2cXZus*@SRWECv8uri(9c>R4Mdy@^4^RbiIDw*Z!IZwMR29 zS~vaHd($L5?Ot>HiX=zNuhWZdYHBBPRDWXpc_LV1+47@L9~!O6jk+IxPu_mdvb9{F zIQFT@@7itsYC3mtaQU8#M!wZ=JNOTqsa$%R3 z-u&&@wNpa!e+*~M)dg&a5_)ydGwf@Ty&9@oD!%T`>3}PO)g{i=CC+vw&EiedPHv5| zJIK|2Azo4Hkz&|vuMOecMj|z#3#uE=cdwYaKIWnGf`_u1Q(Y~>VkKoiUvyFZ>nwLK zP~%skz`KZNf!9hl-U+C^A$aZqv*U$-w+^-mM%}ZV!`%0GzM5^A)5Mv|i)6W;UUP0) zboXtd{Io~nlddbzQhczJaW9JndQPRHo$kO7d>Agh~a}K-YEOx%J=wYnX zsoF(qyvlN6&4zN1LlmX=6(&f9#{K?shcnFks>(t0q{rMjk9EtEoog1`|4W`VCrPVM z-tU5t+_{TZyQWT`BAIEZ_~@j-g@ZF}b}1d$+hWV6V*cx~+oQ#1ZOS`aLd7~~maGld zP+Q{kD&*;eCzB^eZi~G(ZR;Y|-1~VUi;Gj9n8qEkuX^JC?7qn_8P^35l@2YmTC~J% z=_RjILEd}|_uXO9eJAfKx6pMHi(AkUyP}lPv?udy|EX%3UA!og>gPMJ_u>8Oxgj@i z&STuT=t`){beANr2anydQtY#yIDb$Pa|=}!3RkREwf7C#t*}&1b7`cOyieFP?R}4! zV^h2zsF>?5i%DB*p|mWPOYXuO)%Yj|+0N8V+u(#<@rPNb`j_2|T6QNyZGPA_u0_1h zc7>){WQr#ndPD{#sws)O9p(6UX0ee_RK(WQ%&2?*V)HY_RNB@)Gsx}YR9#?w=#EP6 zJ?4K`^(r4ZIH_g$J@bCjy_4s;X5ba?gK}4WRi^I{3jY_mpOx445Epaj3GYh}Be|ZJ z>IEg}UMPF^>|w)=a=B;uKCFumEU#MYqO8eY^-C>p-m|Jr%SHeFotOJ0{n*cG-&&W4 zPI!KBEAI)1sg|8*8#W2<-jZChDxvAvlSLk4RWV0>E~#XS@mH{|DB(JBBlJ06;0n>X zZV40TXXqtMt~%Ql(po=F-N;en!O;n#pYC<3UC7)b-|DPREpdo! zUbZw*Vs?L+U~|pXM5&A(v6U$AsxGn2V-o!E_GO1%=Ip5W z#@@vXPH8UuCpEiIW|7(iPQ6$4`&P`qwY*PHvp_~;Zl3C*1TCh_tb=DeYV_1!K0A76 zK~a{kqLR`)tyMy5OI_Wz=4MG}b+_%+T2nQ{L`gkvRodF9^M!q@dbFOmxv5HDn=`je zeaow*8@CCrHhaBxUCRcq>|>U%H&4pklBA&%lr7z;ovr(NuGP{`HrZZ*e$IENwkN$V z@q4}FQs3@(T6=D3JlP{xeCk}?HR%f1SKETr_Fn5-r1q#JG;86K?1e?yE3ACG4ChT0 zdh@2EaZ#T10il-*C7-RIG-*!hss_KDCn=FD(=rca>GT}av}t=dy({~Ko8Kxa`8i_G zj+MREU%O_)qbaBN>73cNYS*bHT%X*}uF9FKw&uVy7Y7Cg#h)w;TNoG^bQnN@`2%A+ zh-NTiU|<9@5CkJ5qXZgI7L(8xkx&+skdTm+l!Qo15(%WFm9^~j?6T01zI`@cl%;Qv zK>n^a_)&nH4IUKcW}e}$TkEON?;x?jOL(=L;8uIVgH|G^EXA){$Ue7H`=O`)Pgdri zu<$=w*?+pa|12#2+1mVbbol4u^v~V-pO4GmAlJW9?thc~|E7lh%=iCP?fIs|`^Ch7 zr_+L;%nW@zJN(h?h)1&{A1!l!Fgx=0%*acVqt8u@KHeL-t~R7I%_le19fsUs45&y> zs2c=jhPlQ?*tsVdTBT~6q^cXHs2U_I8>gvR=jpjsT6or2d)8Qc)!KN~+W6Gl1-H9K z^ms=0dPVknMfG_jqv(F0xJmvQb0V_lMZr<_{Af7JSrAjcCar3HCK{?l$i-@Ett!Gnho^bi3aKYsl8@85r@;P1bG@BjT<_4A+M zr+*CZk4 zx_)NR|H|=?fs;eVW5a@j%^bp7F()=GJlrmz>@~+@Zd!VJx*=6FZH<2RhjO+C$2ASR}$?@EnCc0?rs?S~d(pKuy9$n7U;Hm$FIU#)&ABU#_22ty|NQ#Cc=PG_IJtjiSGJfM`_~pL zto_gMul!U4%e+k|8#(fXPB!tlT{_V$V7BQ*%QRipFq!Lu49DAK{2o4R4~|fL)Daet z_^30)W#OZ)uosUWb?^G)c!2H9m5k)x<4;yRHazm>g|gux7EKic7ctH=`W|XOx%KxP zxSCS9er_R~WJs7Nn?yt!r<-_8*-cjQgtn7M#ZsnaE}JPWz|d@wv+bmsO~J9s^K(k> zZ9YG@;@#o%^J>1G?2nzm+kQLyzev`Lg&k_EG!`+MyvkUd#T&kGL9gekkn)(lZPS)| zxw1yHO5-3Lsgy(EgfHQQyYEG~#JPwaTqslAEg zaktSWiwnKSd6qoxXXccA;@~E?GG&s#(1rJsPu8j|of?{EDImdIshKt--7F?;RyN-g zH5=xGkiyh*3_Xja@h zTeYgB;gptS~pU~W7i}fT6J`8_R)8HX0F}4Y`b=xM=6 z!`|bY9ZpZXtHXHCsat_@wt&If+bj{xnyGQW^w5KlaUv4(Aya%knnD&<}zv5 z?$^gGl#N*o`1Bb+o_np#ctjv!y#Wiu3W4 z4)Z$e@G)-o_hC3HcHNWVn3V8`^y4*6J`E?DkNJFB#4=+|%_lAOQ=1m+?SFHp?&$fN z@@2f`*8TICJ2f z-7@LQO;1iJOf~L!#uBvToUUfWUa8oZKGUtPY&@qB<8VTZYm;+hj$_%sEsJAkb1N&! zD`QeS_Gt}Q_!sWQlCM9T{EK|ir~G<;<+BrR2Ftwk({q%+KiRSG|Jp!%C+XRY;*|$j zth#gAg-$ezsR*#yYUBwtEpU(%5ai)J(p0NF-GNEnLXh`~3)9C1TAU1r!ZvM+?aPkg z^q0^-_0>bccGvW$ErCnUzRZrFFYRH#%4E=VVnYd=^9+Zu<5qgSDn5292l$q2p3V9f z;JZ~=X?9+_b>oGTvnzy7%xz-dqax*!!v3*cbfJao6v8y}pT(!R=>8<6Tn)sg~pWGYI zz3*nze870o?UMVZw@ao@3EOJaxt7!R!BT@-2bnal%d@UsX%hc4yWPjri>Y9Of=tX( z_Sl2gw3vB1jUc;j5z4wE! ziF4xNcxUaHh2K8tWPkAVbDS*f{;fu$|I%cQUo}fFIHX1#;VLU=KN_CIxnC@UIV6RV zHz{cHqq6P0OBKAMx$aEoE8lVS=z{wuH-eweyR`Fctsci>>qE^~Cav6YWL??9T~6^; z-*@{6tq)6&h`V~XV9oiI_x~JLX!>rEYe~8%!%_Qww_)+C?fd`!T_4A66LIiV@jNES z54QEsTxaF3s(M&0nw)(&V{*mXixc`z{F1oi{;AgYIGc=SBb(6LZH$p8_RN)@*jy=<;)8` z&#J0D)VA#MR4APE*}T%d;rBE1{ZEe=*MGeeUdHrmt|oK2$V<;W57+$YwtHtg6MZJQ zy8pT>HPd9z^S=p8&$=1$P1SYw=?dBrx~b7=-?aR0oy$S@MW#Fw>d-LHn4n=^e(KGn zcTVeiVjJI7O=rnmxV~OCC$TtmrkPDg#J}5n(^Bo2+NC zvefxCzIj=}a~AzQdp9#@&abDzXHx&&e`dL_^65QGJKvb3BN;yqW%n6eGC%+Goc!OH zXS=E#1wu;MPoCYrU-j#?^r|d@##omm^Oz~uOUt$xJHHj#t~x=F{ris3-H%J;*H3-d z_kBh1pX`xb5acf;s>_5a@w`2V~1DE#|0 z`~RPZ?Tx>#pa1u5_y4~i`uG2N?*ISS?f?Hi-#@_i@4)|mpX-_5Y_I=!z5W+FGm}LF z$BYK99SuA;8u)%R2uL&vSv3A-XBMewl$g;dwWCqyMk7ZCi`a`sC5uKyize|57Po{Z zz7_TC9!*+5n)EyBjVzi?BAU%Insr~)Gwi6dT+yt<(d;16;`E~7m3fO>MTh2T*PH)~H|YFm{?Fc!w4ya@N87*f zmYj^1h#zep675A1?Lr()1{0b*D;i2N+Icnj3OL%DEIL{=nnODp{;{5@v#MyTFX*WI z(V?}Xo%2TfgcXfbB07~in))^PK3u3Tyj0&{(K+Eoo70T887CSRYP7WO=vs55%VtG` z^@-+Gi?+Usu5BHSlR6rvS9I?=(WbDYa}GyqzEXw%jqYfVE`t;8$5ynhyU}y*MeA0N zhPTf<+B3RNSM<8<=)SR|*KtMX1P#6|0$l+odUIFwN@TRvC-mL+;12rH^X^CAkMK73 z86BH`G{5lZyK|%cb$h?wiGJ}FeS2qg&)}FKT+uH#qpQAQ0>_O`{uL8EIr;@8Cnjn% zeECs#@kRe>jS0*vIwWpRRD01Y=Q$x#V-jm+54&U|&&vir%XXoglk_6{KVARVaqY## zsVgS#>gX|1)KhI49& zW}_=-bFNUI$Ib?)pB=F)8!ygi5AAI9j-2ML*)RQLN^)h>v>pA~Kc|LNPF0qun8qCV#YBalO-D0&(tb|&SvieWb4K>hhBD2md682qcJ@}UY@4sq$~d#f_Qs3|&X#D; zriPg_&wI?+e4~Laa>kjC$yPgO-0+wgd7`u65RY_6Q~Svt2aUERI~xxEXnK63aaQH* zbrId4I;QbNG#GQvim_}8=A625=ERDZYEnPcj$de8)7kZC!K|o=Io%eWYkxL;ztEuh zvVF(DopV=f^o!k`$-HCs0nPTqGbblm&inJdyX?mtyOWIyCnlem+4?H8DeLDvHI9y( zGbc=!oOS2s{Iwm^c1teU-#JfOviYgxq)R8eKksaNdy?nv%X!l^dVWiGUX^Uz6WRED zXQTGa`JON5%l&Fix9AVmSkU6RKz_!8mz4`lIu|X^n6=GfA@hy}oF3DCPAvMryg_eO zqs*;^^0yWnYs^#QY8LI9+37ilMQd^S&8Y@bi}@mYytU>yXDytsIZJxRT>oDa>}DCpf%eW=#y(E_Ry_!<&wOr)H^6(uk6<(98tY+n$ zP+RV>Omfx)EzQ1etHljm&6%?nU5i*Dd}4yXSNr^1ef?6a*(6r3_goz)**bmKqDfM- ze19y-HEP^{s?Drw-nLa;ZJCYTnyWXRTIzgaO@`FmoYGL+jZ#q>UX0_`7iaLI;C8u7kIHxu9=&z1zR-GTc<}I*X&$D6`f5jBxu353a z*5%%qP&>10=dFJGo$EGo%?sJFY7PGCnJMuqmlK8duw#4iQQCl|coOs2n zrD8>|{qKz%Ga8IMSF4IsKWs2Nf>ebv`?iusDqjt#M?s!2Z7FOsmb0Br1*YDa@%cI+k7o_(}Tke&Q?o!yj z*Ri8b?#AAG(K8fo9xW0R~_2yqc*FQVC zS9`}Mz7q$RoZ4f0=diNI4k^t;Y`=G%v{=3I_s)Ym56Rpi})6>VNU&#iaO@LE}*x!?MAy{6=bDcY+P-ZXfhIpSM$XrAVQd6rZD zy{tI2-s|Xull_-8x8`g*U|hq)SEXQgyH4$OXYr;6?mMeHUu~JSdNRAr;%63<&pog2 z&^cn-bE5Osba~AQReO%_UAe3BDw?LNQf^opCCa(A3$teo-V)Q<2yQ_J^o z)*m?kG-uO)o_@91e|_;QW;64yRNQr;SGv0O;`M1JPH`07jVBHF}W%x#)+!G*g%D)#ag zp54!1oL$3nE-m-6b!WfN--b6C2T$HuOx6bL5HF zu7%cTH9D@o>o~jdO*2!))fkBjzMN;Wex2&rF*~!iakIs_Z#vhqau;=<;G7uxFBO@5);z&J^ezKPa)G=+Bl*f11vA-q`u_$Tyj57k*r2 z$-R);eKuvrA(0oyl{>c0x_h&&XMgFxxeae-beY~cJA3lbxqMH#ev+G`uxO zZszW*l3N5S_8nd^|HscQN6u{Ar`z$e;^vIrZLt~mkNRGl8hgh@`vQYb<5u1W>tjxD z^XXfX$uigKR=iB#yo~)1BzBctTIsst?(7MBR%q;+uFWT=v9ItqyVTkR@t%{4yB>0B z?uyP{{_$?B;;%bBd+VDcdRb=dxVz^5r5jC*eP`9z+_~_!JzV#??Ax_#@76CD;GF&7 zXuydBQhCaGD`$!PYkB(n%yjK5yl9*Luz#UU_4Ws@#q8n)RA>%?ET( zPxO7JeCn#6?43-$qZ(@-Xa2lz&9^@%XR}1t;-r;N1G*aiu@`aO@13Bqjk{o=4Ud4IOL0Yd!1t+YlqU zbduf6pLZUvn(@+NNBiboGq={h63}?XqW5aWy0-9jt2Wj>n#`x3($lkN-N6VO^<971 zSJXavw1NM4&efuvQ>wD(-@cw6RN1He;y_vV{y)>#o%{EOv-(-9#RAT~O?m&CI(NLh zsds(Gx$D-P(|LCCoBr8aWzqA&?}iw2-!p9bDabCgE7xn)3!cw@#dRvFvWk zvOfd$Q8+^2VA>g`)U z?;JhdcS3OW^^mvct#fwI<2jWm|LjQIw~sgO|Nr;$``qSrt9N8YwcPi4HB0t;$^G+B zdFxmGn={)$VKwiD?++Gmaqx4#Q#kVA$}YoDQD`Zj@OFL^Pw#;W?JY0vEvj<;^|W%+lfA8MFQuOSHh-FQtXI~4 zo<_y;(=%M+H?^z@G;%s+>Rt738;^2&(xs)o^WEnDdZBjBmtD+H!(1=8G2y%GrC-PM z@2rl$e5vci8PkKyvqa@uzHfUNe1k*zm7nZiea{1p-RFGgM*c2)*XLNjPsZ+H+Qh|* zdHtT2dlP0_?f;#lAN%jm{HrgQ+m-PsUJl;!&`76m?TQ&$Tx)CVB2zRU#C5OQzPe9w zgZ+US3kCGHP3hKMyGv-&3p@U|+^^I><;+^G_blMGo~vm1mW(4a4_SL|`Fbx~W6zmO zi#gpsGu_^(ekSFMfrp|j)5T>W+nLPX_Bv4cPu*nbYe_T-!}JR;}J3?BQ$Vthz?gSlV@> z)q)$wp8i(-#yRKqemG)!{U}?jzeyx-ZKu-5B+(N6IZ3sGDpHZD+afRb+|EAPmhn+H z)N`Q>cknW+7QgMuTQ03%$hP$B>J6<|CUGko8Rw5?EE_C4193BlOBMw8ijHT5)$91%FTEdhr?rS_yAb%*s zMW#@Lk0Z8ALPF?j%m*cbO$O;c$9G=8dxGPbY3Ij|g0Ac5zfGT^5%$UNQ1XoLd`H*I zCw|Ou5SsT%LelNr!=n>jHcs_Z*{WzNaSQXogt&-*>q!xefFjlvtSojhs;frXG-1?X}q@K zc;tqei7ra_Hcj&=UfNpnkSidpuW7dL6QQ>6f1*^R3Jw45Vbxwe`_v?!N+WfllT)70 zIjOMG(qW=NzeeP(NoK06Wb?{4m*?M#^*R%5@@(qya7!l*D|aDZS5+a!NlN#Eqax?z zZ*3FX`SY-C>Ms$SPnYc@5-&vHQ;h(vqV~y7^XT1nggab!JWP((Ik5ZpOHaOPIan&$9N4`f_QCheLLn#hLT+-yB!9 z3rt+{JVp4v&#iwBaTmAhd$GQ8-*oC~;VSj+%Zpd_h(s(tA-FBp^~|m1ai0(G@OhQY zTq)op9TaB8`CKIb^d(Ix#-7?UUlkezuZA9rx*lqIP4Q*O(>O`)1x^1`FZOukjpv@B5#Rnks)$s*-!;=?VgDWfe@}{dzx3h+X5t*^Fw}JBt>X32(Y` z`;j?k+eD$)=B8(O;-9HJo$zmaW=HVC=(qA3OKyI8d5&YtiM$E>%leC!mMAhW3ucVp z?UQWgzR7Q6R2%>HMy6j|)L$~6>)3inNPK@+Vc^k;Usk#ASN<>BBQ0H&`rLERY*g8@qXpkHr>?qNxn4lDQ5p z*mYdsm~Q6N*uc)C9<$`S7K^JsHRg(zk>2nrAa1X&cl52IMQWYQx>9!xEJWv<#`_Of^KH1!i_nzpU_cj4%+(#6~ne3sfddR5z(`0>=FKbnP;0q3TTSqCij zv)}fbCc5voRQ+GNf9s$87QdYCyFFS-XSXmKx9-x=ewcq~cI4yj2Sc^5Z^^80IJcE4 zo+oCbjazA`^gB=PW{D$Hwoh)Geqn<3wpV_RI~x_}ZjMZ~)zFeR47R+oc=fCBTMD94 z4|D21*T39z`gior7ccJ2G8eABTO?<`tmfyGu{m(Dk?R3hW^fcFK`R(`H zqbokEwYrCzH@+%2I<-pdWRZWGA~Q=^eEg5=m-e6En3+Cn8O#4|dPPz$r!*^5TDRD` zckf{}kNnn9DD}5b*1oQ2NA{xD=b!k^yp3+m=05m%#DG+u7u!M~^*Tt~XdCa*b8$Mk!(mC5lX}ac zt((oxRokt8$)opqi@@*0OF!Dr-R&GEDDb=SaInSR1HW~qILYo!*9Q9OCz#R=!{&Gw>q)*j28SQsQ0@<&>%WVcJoREJHIcI?=9z?kn&qqWZ+ zJ>H$N(+oHKGMs3sBO1Un$!X2@AQtP!!hJrQIgOHK429P@Uy)?{Wgn@ta&k9U&PL5B z!|CrQZGY&g8@Nzhb)oq=LpOd&-pCL)z9Z{}=kVwBY->DtXj;acIlsHr*BmNInbyxO zTKLa`H7`W3QApta3LTjzN9Jl+*0AhwyE)N1=Xghp`_Bo-EAQ+N6LI0aHTlmY_qAT! zZ3hiy3_b1# zD1~0)+?8YLI#c}fr_Sv?UaJz11@1c0m$5r~n(E* zEcD@cSfRT&#Qk4Tick3;$;OBK_h>pE+i+%SrOXu`&Gnp1M54AGkoMZ5)482fKj6rj zeJA^_ww(SnW6n&TSx48Lvb~~ovHH;N8}7G7T#r=jT5EVV^Wgk-AI?tH@cg`Z=E+I3 znHT%L7Em~DeD1+z1)-WlydslMgml`kS$|#U{EUerZydVUW}2+G)_j#^6L3)H6HmbO zo2L|C%Kh^?zrw}w$g7@XjEisN@J!R5adXZ2vXhF>Dnybds4m;QQl@(TTbWZWJ}aVF zCL|jf&)?HI<@8>b)U*E4{Err&?`k~%gGDTR_BpPl{RN%|YcA*%Jf5{dq*L$X>b+5t zH7i|rDXsX=qbcHhaqa_Yv8f(ApZr^NE;;B}RnHo&X-A_@Yx(lDOLd%I)%`|=%UtL9 zL6e{+iRp}gJ&rj0%sU{Zk$Zd(i?Q1#ubvYcI=+rsM=qY3!fT-|A5yic_|N%^TRJCQ z^w$^F^Sa?c0+VqgB)gi8WChOSc&t)6o1X|mkPUpD@(D;ic!4tl&#`*xSRBG2YW z=MTO)_iDkqdv`9MEV#O>>8g9~yq1YF%9&U1Snbo#J}MvRxA3Cp+N!>kRF#z2;PwMM zx?l3IXYkt@xGq?9o2zobB^mwbwflcfxR#mfd;H&$Id3QQ# z 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%0DD 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 literal 0 HcmV?d00001 diff --git a/tests/components/generic/sample2_jpeg_odd_header.jpg b/tests/components/generic/sample2_jpeg_odd_header.jpg new file mode 100644 index 0000000000000000000000000000000000000000..80372d4edd65c5e4f8fa29be43512199159fa3fe GIT binary patch literal 79018 zcmex=;r|C11UZ;4ure?+Dlsq#GBOJ?{y)MX z&%nUQ$_NGwP{7E<%)-jX&cVsW{r?EVRsjYkMrLLv7G_pf78V8u##%-uW(F2PRv|@0 zM>gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzIxuvzOy`!^h(&Q;qr%j(RbJn88OO`HM zzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI_1g6tH*YC(E1epaH z>>2(BFY3}@z=4^y!)B~dw)rIT_4$8>wGra&->WxnU-YEs@to_fD>YM9D{UQ)x$j(8 zAH>aNWO*{E=;+4V_H!AE!*596{ZPdD{nxWSw~vI&<*^v?FQ#v3k8`6a$6WrzT!G9(w@QP_^0ammv!oqpQUvi&qdC7-rQj8TYjtKr^+3N z1OtaRh4c1LEu3|wqVtKHI{){6%g2{LKWebJ>lC+gXT|F~^^+s4T8^H)bNJ4*x949+ zO<(7{FM9611%K^Nt}6~Gh7C)pN9n&*VfE1YWa{PndhPt$+jS^2JZ zHFNL|>%DCi3wWY`dQ9Bd#9(>i-~6c4Q_q>3#}vQ3v~N#_s+dmFE6FtvA?ke-7>dy|mY40Zq>oQwD z)f&Hc;3)p%{$<(ZInS5!)c$Aq_2u&9{|wg`Fm|5v{Lk?EvgdR4zB_^Q>Q644-0Z;R zZ78{H-d@jNK^ql!*k&&6JaOWr2t&)0e_JkDJ$bC~*{fW@KPEV0{)Yu@bN(}QzMhpS zQ&jl%z5mHy|4e3nKVTzsfu-L3r^y;mOWU8nE>3WdUoW}j$&<-n?6xpmKEM63*O6^* z)d}T`=RCPMNB+43yXE;$%QmM=+OE9$_VT^i*YjUD|EpZUAmnMeBzfYs={IIMzn;I| z@=pdM=XslXbz5e>KfYGqa`C(^3^Ubx{)MFL_!*v+e7;uG@`e9@hNuRC=lj`uKL6_C zEB+CbGWq_!2Iecze=p&ww_nTfsdDkh#rul?YA~E%FKH80RC!ik;qz<9=licU2z-5V z!C(DS^W>8za_xH=BrTpzDZX<_=J{HdUytuKFuAW)S9vl|(~r+S%BWhd_64Jr%;cli z`-89b%z4(ge6j<3T-BV>id_Q6yLe@_2kL_3{ng} zzc0;?vp+U@Qt_V^=ZhV-kHwU!IQ&o_yt> z2*aO^Pv$wkcB?#BFB4R^<;}d}UtJ8AHgg`$S#In8*q7b*KSNZx#g{)Bj5jTx%*(ht z&-1Yx?@&Ta($XSDedW z^iWrs^Vw^`WG~CdAD2qL@4v3rAhY1(-IKj>vq%4$%?Q1zpSfOv6RfK zWv{xtb@uhG3?+gVi9gC#U#nzid9L0QX1evb@A36(b0SXeZTJ7UfcJJ#fMQWqsm=Fw z{eC+lPEAcdXuac)_N=yqa~nNw1XQfewcKAl;U>!@yF3SpKgP2d-mg{noILS!?t}gX zuV05ZFm7LwS9m;5o7;0j3a3o^#XjS=vAnxo6__h5BxRCK7}qX8;c=rbpsM`ww%XUO zN!Hz(udnCVd|M^vC>D67r*X^2vw63UsWP)X`Q*M%gYnxlf|lRxe#V0;;LP{ETeSmn#Pdi&4URz}^`dFb}% zt%C5|#>OzmX=XqBye1X@tJPqxJ9sLFxq&_uZm*OSk#3VyGifca`s2jIw4N87K| zUwoIW<)I)Sm#z2vaHXgCdkxm9?KXVbk#Ey8&9{X%mjqvz)q0})xpfNfLBIRo+UwUc zRI;A!*k1Y8e9}fSWo32S!lT6}_gzb2f7fpDSd#re!}_?XkLD@{eTrOr!!63_08hd; z|KK}P58vmSZ}xiOtmnMI#;ksQK?ZZywTJI@-OtWka9Ek~`*F#*zplZ%wM~vLelRu1 ztnv=O^Gl!0e@=&me)dk`k=xyIfc^DxyRS=<+P+?MIv8VUX;a)&XLEslZbiFyU-kU0 z4o=f;9!oyn{&|gN?gjIk&pDrZaecMdm;5Djy>Ru-EyX9fCvWmSDrxog;NzX9$K&!p zM{#EAYFFsgr>5MPx%J%+VpYMj=@|?{sGjH+h*mpaQ96BOkcy;HO z<7+F=EwvODsh|B^vEAl#e8Med*KYk9Qu zZcy#bFK>@Bq~7)M7T*0bviOYS$&=65M(miZK950m=h=WAd29URmISaEpFH;F^UGT+ zPpVs-tG+VX&du}J=W8o`HST!k|EX$_R%y3?*wxx;-|y_~x#K^>*Zt?RjQjj7mFEPo z6q_0O)%J#`9X$Cb^Rry>i6c{ zw5|TK;=H=u7Y4(VuP?P({`&o8!Q{TZ?$15^{QT{gWiY7QJosR@rTpo}V}4c_kITBM zOFn6y#bD%L}VDkIoF+c07`gs}labITI$XsBW+{9_S*>ldE z$&M;>WEgL&_3?W#NCJRkpIvEA~QUKJ(`_iMh0$On5*;+NfT<>#;Rcou{4 zInTw*|1-pYUZV2ca;fU>=j@J9>GQtKt^Ust758Tr1AkmqK#}!@YI!ds&+}Cb)+uv-S6zB+Z)hp=*~9YXkKeP5 z&)bxLUBLRqMEF&0y_C=A-P^ZneziQypm?spzba_%hF8YFE`2_4Yc)U0alYTYE$@~;d->9V(bA^s`%;#9 z^-EaZWPGx;TKeHnZ&@cn!reC&8xk^Kx)26c(^dp+emUpq(%?yFSW zqU-t2^B3RO$-)Vr>VJJ*obyv5{lj|)sSO)tx^7;6s@V9-xa(;91U~=IYo!xH-kr02 zmBGBYE#VyJEB2*1FDJ_zB&#nx?)B_J?h!KstF9Xx(pf!iet%!UckEupX|}t4El;j{ zCtCchpIUJ3bCbL5=ZVKE*G)S1m5J%e+Y`2xFEp69Z{FQ8vH67Zmvyrabariw$zzyT z*i#)oFZ!lHhmO{Lw~w!4cv*7Rv0H3(8lc;eu%{|x_^!UdQ=4;#D%x0*$ zYt?+O!K7X)$~E_}+O!5PjYR~pq*C6K(yz;T9?y#M`Ru^$DiXlxYjNIws?|&d_4(%- zB(+i-gZuu31g%KpKX6dp_E^=w(&YCIcba>ySn@pe53&?t?0l4K{&t$13;UTkM%miJ zFJ~(cN8b5!>Xq}c`U**p6K{WBUw@iof{hW|tp^S6Ym38oN%BQ!s-y^iZE#ajV zibWgG-QZcy+ruW?Zu?q3?s9sQyh&b)f(7%2W40R1XP3H#+}3)qI``-PwW%@7rz^Eg zsCnm@cc-V+ZtJzIuw|~+-aiL7qbEo+&N9cWW$NoL>YeyzdMHtYJC zd4U_4B--3w@vkl3bhENZ;6OnU=i}z%^>x~7_tzWcn>0_8j_%tZb>_<3KQD@Q-djE4 z=dr5CFTZ?Sz<=Qvu4qbGBPuzghr!?E*V!w267PB=7%$A*`Ss7Q0JgZ*C71NxeBeLb zS5TEweO-Cwsg#=Zxi`*RuD$CNW6tWBFE=Y&>-1!8XU|E02F(#~i)qVBzjQ zzp5wy8Rmn+B`)cbVV+&`$+r>0=lh0@F79+Daa}Ufr5~6PFzt-!y^2_tr z|7e*VE2>v2bl|m*Gk)!px14>s&-dj*#`6zwWeZiFXSu*EG3UX=uU}W3w9RtwTNo}C-)o*FZFl8zlV9}(CbN3`YqO5;JXbvDa=)#ARi@?R@|p|Gp3lqP zRd+F}|0-XqZ29YQg$TpU_b1QW8vA}XdH-+!V~?r}Oolekn_v2#wEZEnGw1ow(j_(z z()U#bFqM4yYSK34NzJ`2*K9p)e+Mub%l+H=Cv*AadG%J@>Hit7x>cTKP<;7kC0|kH zuUWz8)tdZcE-)4M%&UEA;IH!h&SyW_b5&i=C*S|Lz~HWaq3WZDoArN&sKV#!$}eYS z%Kc~Pc-A1$=T?15psLyL_4)nlt1@j*E?~*?tM@XVeEbTt3_Tjc!J`i%eiGAP-lTbUpsJB zeO;__N#^^4c{^?QR$X9vZX;!RKG3T&zvfc%UIymLlV=%up8wAfH(e&>QjuQ zx_uNw{qp+P?!{+WCg0y4x25{|b}xpC^Tii}p3KW!@Lc}6$xhoBj5j6DYS{EuU6MUA z`Tp%$Z2s}58dROn+ge>&@Og5ehVtYfJsKhYB}KRtPF@v^66UAIsEjJ)LY@yjxp zS8fV=AaG#bp1;|7_fPW}Fse`f`e=hr6_0Swuk^2LBP8BQocv?#AnmDs#xBUHs&DFc zZpjIG3?B>%@9q4Zf6qwH-Oux7tk`U+89(!k_&AR*f9xQ(@J{bDJtO|RHfh`6sGVMr ze#^l4SpCU0uPYQ2nB|N-+19L@tjKjFvC-nagRDqa@qP=7FHe%^O}+4P^Y$r=l^ZP1 z)vq@_b<^&_&YaJeElu|)Dy`OFEvOQxU&>Hc<@>s+TzKA%b+gVTt83huldQI5wvvMz z!{Z-QWd4QjO)dLqwDX+AEuJC^R)PkW$__hhdhgUB)J)hX~ zcve?{`EyCh_vf-aJ}ggre|WFa6NAJ33*S3PM1AX_j2+H_E6o2R=>fAYN@{NgVcNx!!g=iaW^&hUKwmjyi5ZGEq9e-C$t6j{se6+Kqh~tpP^m4R^YPd9x!K|j(@r-nnUnBEelA0D{GI9QlM3etX!AWQyK~?Cl>gGG zeLE~`3~eiA8Y9E`3Xkvnx`6BZq|GULHpTO^j_yi3dCo)f{kd$nmp7PqOR7Bi{b*C3 z#FNkO9Rv+kWQq#!tv*#3BfnmLt=HMfljnSYv~c<}_Q`X8)!Ak+EqU5k#Ag^Xw_NDX z*VkcBY!5#8J!{=#h0l|0t1qzdUY}H9(;ug$u2Ws|a$a$eo#(d|bA%3Td}XnKBie57 ze+I8)9{JgSvYd>s%T`^MX!3m4pltW}`kc%B%Ig0auKOo(eqSbYftk<$W9jQz$ugyu zUsg&_{`2#_&+8K}=SMMQS3Qw0_RZh>ZN>AH#>0N zwtT);@xkZoHub+=C1Q~Jt$2Qvfp_|f1uV%2^ZaZrkD7e`arv+R*JX2WzVc--XX)E%ve#c`lJy0T*XJeA z+h*K}crIz{%V4_ex%|3}bD$m~cUoWlI>!YpNyc*uU(WKGQ=K+_Jzr({-rd&!xqBRF zF#iJ6=PMUHm0x9Af3lA%RC#cLsr*_0I`^+j)#p6xubX?|>jD#Qyp&)c%|?XLi)IbY8|_cA&=sru`JCQp@Q zyDjq#-#hS~x9LALcT$mm@np}*SAJdkHUGH-_qn>n^L1aC&tLeq^!&obKQ{lo!1V5X zP_5>H$+y2O`?{R3@+^bKNxOSr-ue60ADbXwQ}z4u4%-)uH{Q=bH215-d*9~^-@d&z z$EbcSgN0nx^K0{rZ_P0NXR*9L-;1F_^0nW4kLBtMp3Pf6NB&TQXz`W#YyD=bEe>oB zT4DKoD}xVTrD>Xw5&t^tXRFq^2ieb!=y=#}BYa{3ug1nXJ!xwXy|GKU@qBAw#}no` zzmwLwdAyz<>>w5PNpyn2Ny)XhCCjJYu{@#BR~YkzFQRe2*Nx3HRs~ce?;Md2KAIb~$(Z5naaH!^k7K?~ z+}Y;&y0863<+>)mg2>%Z%KwEhE@qi;sJgqr_|o<*zb9{CIJ_-$a}viA!Yp@_fbloNHw>b0RmbonYISov0gOFgfvr{o$hw zFmGKIM7p2%TcEEP6;@=n~S3Si+>@>%kZvE#Asy-hW) zT=6$_ay&|(M$3Gh|1@0AGGR)?Q)f%<<38!1R(uuBe6#1`BwbAEAUmM1-F|&NKe*Srl^@IuMBH8Z+Pp~}s z)oW17Vi>A5zWB;7a}{xh_1EuM9DOU_X~yU;rZ)-wHP>OzaSXD-+t9;Tw_W;I9N zOJB~qEhNdhd4hrC<6jrpr~a7jpItpceyXM34O#p1Ym23pZhDu{AbKPESGwKQm-|#V zJ8B))d@P}RrB&8G>S5{WNTr!-w_CMB3|`zg$^P-5Q-j@I98cd)&xp6~c7Ys&M>7P1wl$|uU^Z1YrKzBFi_^GOz&&y!EsuVo0?Iayu3uj2fA zuk*9jEsy){UmCNcc+Q+TvzBT;`SL4(HBDW%s=9PVL|?6~)wh-Lvh{17)j0ocVdQ>n zdHnIL)t1i-zpgeev$c;}uAJVK!Ni_FDuwSBpy_~q;O4ty$K@BRqpS5`K8d|dtV^1bFNlYd@d5seM4&+@)@RGC!2 zltHcd{7dhzi`9Gnv9Fc=Y2VAB=czFH`17NZ&sFM7W}Xjr_|K62VF9b??mgo5OGOZ|@66&y&yBua))7Utn%uSMODD{<-gaKiNwAr3^kT^R{Z(9{kVn`U+S5$(4?F`{y$FEGeG%)k?1F z_tk_V%jK&dq+DQ|qkmcD`Rj1ejiv|H`@*y?S6kSCLn!-=<-mok*?s@dcy^lclkN1Lqu1F5qiADcSebMsBAJ*NZjH0?a%KeYN#ghc7259eCXQ`pe(! zg#8^(o%%6W7mwSQuhkCekv0v;QV#P?UBA(#FN}LoOigBZAvY^ zEWP&SYv}I~_snB4jQ2PG%@)(XI!)5%{WMpJJoo445uT6DbDsbC zeO-TL#JkMM!Z|{QxBISM%xY~pe6o&9QF&eAIrg1vwijJKCegUR@q~rN%YOe43wSh^ zS14syPvGLIx&Qpu>Q_Omlh3zqYIXd2xV|?l{`SwwC-V+JOHEqxfq(nsn6F1RxoaI& z|FvDhLYCw4=X1plg4v%;uP6m>uj=`7&g0wbw=rvb`LdST_K zMJFz%tS(}FGBM`qkr&$;rLE)3;}&1<`@;D3sz{&%D+UgZ-N5-}0oScLi>%X59z1A2 zwX$z-nMzrB-mv$Hm9hN` zSw7zB`uv|k_|@(yi{>!jW`Fj_aozkQ;&lOs?V?rYS$#cPxo&U8b9I$p#qTO)_y79B zc;K*(_f26ulO5g{`|0NMIe+b^7GD1(Uc2YPM$6;NZLWP=Z$495_$13$S?zhd3-}&c z|0-qgVpwkcr{#lPPXDo$zG{r;t7biN;8b5{uN<+N-$%jeux zp3grwD%dovh|9Zx=&sm*vj`OgY=3PmdS1-lDdCqb{f0c#B z;vEZERK6-rk*hy%a{1uvAHj`p{Qon=HSqnJzgExBHq*%Sz4!b84aVZ*>U~?%=lL_Q zH<@|-T7R$uTjj~cD<9it*gfi7z8s`OY`UyphDh;i2k!Ut|Lv8n{Li2^+48x`lK%`} zcP?O+TmJLwQp>aIfiKThUHW3H!B~9$$EEMDJ^XEE8Bbfl{#>Ev(Byer8ZJGaBVc%c zFN0<6iPu5Q?|)ooRG)P3%g0xh_H!AmryaMSYr*{b*X80_WfdX}5B!~K`D`=cr3TSHpg&YRdD$I{o7^ltW$bxOr*F_gH&q-CqHEM1I}=WS$?`!bl* z?un@6KB&63x7)~rEn4<$>P_VWkpj0Ot7~_sIP6HPDYE_OApOndEsOB`+lmbj z{hDXQd3^b$&2K`?**Cr`oL{wP+9&O~^JADN&tkZ>)!(x4)8}ai-~KDSv`ts>Kv#vXd`+~XmW84yk#2uyQ+3lZP zpZ=pgD3kNn-=DJXG0%4G7rom!d7kIH`|+=D-`jIMbWe~(-m5zk?pi+I%aEE??i#Yu z;`to;V>@;I-Y%Wkyi?Xu`Sagw@9_ORkdj8SarR8-X&F@ zS1DhY_sQq4<4N1q*}=0_8roExxXQlC|->ecTqtxZsmEpU)TfTuoQ?wEU}; zZu#W%ln|97UzN+qF|LZ`@lMnmXG8k_1x7%}} z@}1?9^K02}?%NZn7(gdD)?(ts1tu_fC?@eC$ujT@iytA*0 z@Oir_k58)nlYBO#_|9hs-lXbzU#`7A`Cazqk>{VET#iWJzT+r^Y^VCZiY2cnS$vV( zvUu|3AD6^ze=K0Ov_CiD*W|w$D$gf-_*)#m)F3c#z8C*1+YFV@6YSTj>l8b%Ri$6w zYcj8DUcFSg%GZ~^C(oNO@XYgn?)ZH2%f5{Bmajb`=Fe&n`TFF`lH>JN=INGS7nfV_ zN}k1#slM~q#q;Z*dQYnQ&!A;ge0(i~eDJSJa+YtuF1=Ire9o``4D-IsJgF{!-Gt%t z`KRY9mnQkE&ndAyr}6$=?TbH^3mBfSFlm?k`uy`^^*`Si_FI}T%slz5$?}~4bGPE3 z-xk~avzx-uK6%IE*Ik^?=SNBJx6PFJykqjK+82yP)${7-vafhH+4A|v#g@Mw+h#C4 znbY%0#Ag~7TlD4{xXPpV)OB9 z4Z1FW9$#kLqWk2Wk>%GlOA;hfzOVkU|MFUfY?q%0ANRFfyV^Wy&tuc`M-|ThG+m!) zapL)Y6UOT=FPjC{@|#|J>BG(bjzdw$8lWYpQ+b*X-}2L>+7R#ZR_NWpcRM&$g|(ZDH}^Zf*Vf zwP`myo^&uDJoqO&=xUSRwLhQd{AZY7#c*p&!r{E?x~-MA#gW%~x6D0%KvvsTy4-Gm zFM}Nkqm;-ank>o%3g6d6zgCp_{cYV%JNL&co{Me|xN}6UXXV$k43&IaS!R{~Ud`D1 zv^{TsRNWkxb*eqh`|sQI75)tS-d}Pl#6v)+@Fe?si+`b~UtB(!Z@^Q;(q52wLISjE z{C$b~{r0>_N0!Dp&3#vU*WHlc;$76iQ#AM7Uazk*^pzZSl98A-|Vv8HJRdu$fcu^|o|& zzV5Ov9~PBO#kX&NJX`Rc@7neQ6I+tp-ri*Y^K}9L$_y+Ma@w=5t&=cL>7T8>K0oRu zzf*Bc-gZsBCxwkyJ|8&0mZ3Q6=uIx~Z3b(bEWe6AmD9a4&Af0obEYEWd70c)%b3 z`OoXKTea`5Ix=VOaY@imTY=P97C1PLSF#fv0ruz2SvmBxFs_%bR ztG$bVX#1g3R_pEIhua1Js6TuEulkzlmp6{KjOTy8b`V*g$`jFizwiF@IoEVg3q?F2BE`&B*%?Z0>KLG5`_=53xfPe3u1ZD9HM>xvVvzb;i*mp?bL zUuG6V@}%l3I)25MmJ}cJe0gt?<&*yma{`!t|7Y0xV-~;q{I%+F#b;T{&oU^di%L)O z^IV?JuFeu1@wlmO?`H>Ib(=|rUzcrs^6|%|`JZRG{p=6&tL|cOzFh8=_|EhFoZrFy z{~6ZJzW?lR2BVSZ^A9SQzMpe{>{0bA==bw`4N^bV|7fgvFG#q-Zics|d4D}!F;Qp+XJKQ4Vc=iiW>cGlizy(_`YoN00} z>!b25Ps-M6x>bK&z^=bPs^YnQ%-3a_Uu|DLsQ#zcAjA3EkL7Zu+Pvix&tGeh@B7cN z)`I`}Q8mw3p5IOOJpUEI{PTRw<;G<^&sQ!leeWQ|_SNM5o{DAtpU-MoK5LL$zP#?M z#<@4omnT?V@t#!4wA68a+!jWoIbYvdU*0z7gXXHLg#Kf5CtsPjs6l4W=gTkeEeZQ- zD)GYh6tA5mhGBrRCdFls&B_xhAR2? z_ZvdQUw?Oh7&~>(sZZJ~U)b%N`mD<39oK{(&let#)82e(z2uI&pPtlK2e6kW7b+F> zPP}e={O9*|>wR{py47ypQu_G%*LCv#6Z_0IKCF=2_iR_L)x*W1VYc<{w)IjBZ`Y(G z?Tq<+X%{zl;{<8T2QqX1Gw4@6G5LLx!FlSjdxmFA!lujeRVc`O{m-Co!gxK>$Ggcx z=J};vu{N3#_A}SX`|i>`X8Hcd_2^~iLKr;G*=8^=+;YHLdY{v`wH1MEoH=rTuDLD@ z*D7zG#BuO!o}>7T1#`Zr1hD=tQ!L!wUsoO0benYl{-1xeFMn12 zY<%+6Cvn;A-u)d%H8l8c%Ig@;nK=LSpA4pJJ!i9x(i^vDSUjFQNB+pQFBc>iWEyxL zW~zE{t!#$!6pe(%1;-p;ST$wmUG$4nx1KWj%0Cgt0~O8Q<&ml93T7V`oPXF>`?RTb zrSi^-J92!g%j9KpTucx_;e>m&+$$

  • mNU;i1j7=B^% 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 z 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