From 236acd62067c81ba5d12f355f638bf6d37d0ed0b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 16 Apr 2022 18:04:09 +0200 Subject: [PATCH 01/36] Fix retry when Met config entry fails (#70012) --- homeassistant/components/met/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 536cf03bde2..2f663df458e 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.distance import convert as convert_distance @@ -33,6 +34,7 @@ from .const import ( DOMAIN, ) +# Dedicated Home Assistant endpoint - do not change! URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" PLATFORMS = [Platform.WEATHER] @@ -82,6 +84,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +class CannotConnect(HomeAssistantError): + """Unable to connect to the web site.""" + + class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]): """Class to manage fetching Met data.""" @@ -173,7 +179,9 @@ class MetWeatherData: async def fetch_data(self) -> MetWeatherData: """Fetch data from API - (current weather and forecast).""" - await self._weather_data.fetching_data() + resp = await self._weather_data.fetching_data() + if not resp: + raise CannotConnect() self.current_weather_data = self._weather_data.get_current_weather() time_zone = dt_util.DEFAULT_TIME_ZONE self.daily_forecast = self._weather_data.get_forecast(time_zone, False) From 0a6182264ab831aefd543b96592c955e5d62cf38 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 15 Apr 2022 19:14:07 +0300 Subject: [PATCH 02/36] Set source & sound mode at start in media player reproduce state (#70064) --- .../media_player/reproduce_state.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 47d365ff75a..131366ed95b 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -83,18 +83,7 @@ async def _async_reproduce_states( cur_state = hass.states.get(state.entity_id) features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0 - if ( - ATTR_MEDIA_VOLUME_LEVEL in state.attributes - and features & MediaPlayerEntityFeature.VOLUME_SET - ): - await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL]) - - if ( - ATTR_MEDIA_VOLUME_MUTED in state.attributes - and features & MediaPlayerEntityFeature.VOLUME_MUTE - ): - await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED]) - + # First set source & sound mode to match the saved supported features if ( ATTR_INPUT_SOURCE in state.attributes and features & MediaPlayerEntityFeature.SELECT_SOURCE @@ -107,6 +96,18 @@ async def _async_reproduce_states( ): await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE]) + if ( + ATTR_MEDIA_VOLUME_LEVEL in state.attributes + and features & MediaPlayerEntityFeature.VOLUME_SET + ): + await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL]) + + if ( + ATTR_MEDIA_VOLUME_MUTED in state.attributes + and features & MediaPlayerEntityFeature.VOLUME_MUTE + ): + await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED]) + already_playing = False if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and ( From 410e0f52a304c00f24006de498cae160a31fc911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 15 Apr 2022 18:31:02 +0200 Subject: [PATCH 03/36] Limit Supervisor refresh updates (#70075) --- homeassistant/components/hassio/__init__.py | 16 ++++++- tests/components/hassio/test_init.py | 48 ++++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index f3f6887570f..278f8a50ebc 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -917,7 +917,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def force_data_refresh(self) -> None: """Force update of the addon info.""" - await self.hassio.refresh_updates() ( self.hass.data[DATA_INFO], self.hass.data[DATA_CORE_INFO], @@ -976,3 +975,18 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): except HassioAPIError as err: _LOGGER.warning("Could not fetch info for %s: %s", slug, err) return (slug, None) + + async def _async_refresh( + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, + ) -> None: + """Refresh data.""" + if not scheduled: + # Force refreshing updates for non-scheduled updates + try: + await self.hassio.refresh_updates() + except HassioAPIError as err: + _LOGGER.warning("Error on Supervisor API: %s", err) + await super()._async_refresh(log_failures, raise_on_auth_failed, scheduled) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 2e7bea90f68..6f4b9a39a9f 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -646,7 +646,8 @@ async def test_device_registry_calls(hass): async def test_coordinator_updates(hass, caplog): - """Test coordinator.""" + """Test coordinator updates.""" + await async_setup_component(hass, "homeassistant", {}) with patch.dict(os.environ, MOCK_ENVIRON), patch( "homeassistant.components.hassio.HassIO.refresh_updates" ) as refresh_updates_mock: @@ -656,14 +657,47 @@ async def test_coordinator_updates(hass, caplog): await hass.async_block_till_done() assert refresh_updates_mock.call_count == 1 + with patch( + "homeassistant.components.hassio.HassIO.refresh_updates", + ) as refresh_updates_mock: + async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) + await hass.async_block_till_done() + assert refresh_updates_mock.call_count == 0 + + with patch( + "homeassistant.components.hassio.HassIO.refresh_updates", + ) as refresh_updates_mock: + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + assert refresh_updates_mock.call_count == 1 + + # There is a 10s cooldown on the debouncer + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await hass.async_block_till_done() + with patch( "homeassistant.components.hassio.HassIO.refresh_updates", side_effect=HassioAPIError("Unknown"), ) as refresh_updates_mock: - async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5)) - await hass.async_block_till_done() - assert refresh_updates_mock.call_count == 1 - assert ( - "Error fetching hassio data: Error on Supervisor API: Unknown" - in caplog.text + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, ) + assert refresh_updates_mock.call_count == 1 + assert "Error on Supervisor API: Unknown" in caplog.text From a9c670c56f3d10dbdb3edf952c15e4763636e779 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 15 Apr 2022 16:20:54 -0400 Subject: [PATCH 04/36] Correct tomorrowio weather units (#70107) --- .../components/tomorrowio/weather.py | 20 +-- tests/components/tomorrowio/test_weather.py | 164 ++---------------- 2 files changed, 25 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 3006be989ba..bf687f8bdca 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -20,11 +20,11 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, - LENGTH_INCHES, - LENGTH_MILES, - PRESSURE_INHG, - SPEED_MILES_PER_HOUR, - TEMP_FAHRENHEIT, + LENGTH_KILOMETERS, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -73,11 +73,11 @@ async def async_setup_entry( class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" - _attr_temperature_unit = TEMP_FAHRENHEIT - _attr_pressure_unit = PRESSURE_INHG - _attr_wind_speed_unit = SPEED_MILES_PER_HOUR - _attr_visibility_unit = LENGTH_MILES - _attr_precipitation_unit = LENGTH_INCHES + _attr_temperature_unit = TEMP_CELSIUS + _attr_pressure_unit = PRESSURE_HPA + _attr_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_visibility_unit = LENGTH_KILOMETERS + _attr_precipitation_unit = LENGTH_MILLIMETERS def __init__( self, diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 3e5eb81edec..f9c7e00b7cd 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -17,9 +17,6 @@ from homeassistant.components.tomorrowio.const import ( DOMAIN, ) from homeassistant.components.weather import ( - ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_RAINY, - ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST, ATTR_FORECAST_CONDITION, @@ -93,153 +90,22 @@ async def test_v4_weather(hass: HomeAssistant) -> None: weather_state = await _setup(hass, API_V4_ENTRY_DATA) assert weather_state.state == ATTR_CONDITION_SUNNY assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert weather_state.attributes[ATTR_FORECAST] == [ - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 7.7, - ATTR_FORECAST_TEMP_LOW: -3.3, - ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 4.24, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 9.7, - ATTR_FORECAST_TEMP_LOW: -3.2, - ATTR_FORECAST_WIND_BEARING: 262.82, - ATTR_FORECAST_WIND_SPEED: 3.24, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19.4, - ATTR_FORECAST_TEMP_LOW: -0.3, - ATTR_FORECAST_WIND_BEARING: 229.3, - ATTR_FORECAST_WIND_SPEED: 3.15, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18.5, - ATTR_FORECAST_TEMP_LOW: 3.0, - ATTR_FORECAST_WIND_BEARING: 149.91, - ATTR_FORECAST_WIND_SPEED: 4.76, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19.0, - ATTR_FORECAST_TEMP_LOW: 9.0, - ATTR_FORECAST_WIND_BEARING: 210.45, - ATTR_FORECAST_WIND_SPEED: 7.01, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.12, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 19.9, - ATTR_FORECAST_TEMP_LOW: 12.1, - ATTR_FORECAST_WIND_BEARING: 217.98, - ATTR_FORECAST_WIND_SPEED: 5.5, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 12.5, - ATTR_FORECAST_TEMP_LOW: 6.1, - ATTR_FORECAST_WIND_BEARING: 58.79, - ATTR_FORECAST_WIND_SPEED: 4.35, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, - ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 23.96, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 6.1, - ATTR_FORECAST_TEMP_LOW: 0.8, - ATTR_FORECAST_WIND_BEARING: 70.25, - ATTR_FORECAST_WIND_SPEED: 7.26, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, - ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.46, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 6.5, - ATTR_FORECAST_TEMP_LOW: -1.5, - ATTR_FORECAST_WIND_BEARING: 84.47, - ATTR_FORECAST_WIND_SPEED: 7.1, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 6.1, - ATTR_FORECAST_TEMP_LOW: -1.6, - ATTR_FORECAST_WIND_BEARING: 103.85, - ATTR_FORECAST_WIND_SPEED: 3.0, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11.3, - ATTR_FORECAST_TEMP_LOW: 1.3, - ATTR_FORECAST_WIND_BEARING: 145.41, - ATTR_FORECAST_WIND_SPEED: 3.25, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10, - ATTR_FORECAST_TEMP: 12.3, - ATTR_FORECAST_TEMP_LOW: 5.2, - ATTR_FORECAST_WIND_BEARING: 62.99, - ATTR_FORECAST_WIND_SPEED: 2.94, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 2.93, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 9.4, - ATTR_FORECAST_TEMP_LOW: 4.1, - ATTR_FORECAST_WIND_BEARING: 68.54, - ATTR_FORECAST_WIND_SPEED: 6.22, - }, - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, - ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.22, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3, - ATTR_FORECAST_TEMP: 4.5, - ATTR_FORECAST_TEMP_LOW: 1.7, - ATTR_FORECAST_WIND_BEARING: 56.98, - ATTR_FORECAST_WIND_SPEED: 7.76, - }, - ] + assert len(weather_state.attributes[ATTR_FORECAST]) == 14 + assert weather_state.attributes[ATTR_FORECAST][0] == { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, + ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 45.9, + ATTR_FORECAST_TEMP_LOW: 26.1, + ATTR_FORECAST_WIND_BEARING: 239.6, + ATTR_FORECAST_WIND_SPEED: 9.49, + } assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - Daily" 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] == 6.7 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.12 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 3035.0 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 44.1 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 8.15 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 4.17 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 9.33 From 73478dc76d591bedf4a4684dc5f6d5774aed7c29 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 15 Apr 2022 23:15:05 +0200 Subject: [PATCH 05/36] update xknx to 0.20.3 (#70123) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0fdabcec9fb..f11ef8c56cb 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.20.2"], + "requirements": ["xknx==0.20.3"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a5b62691256..22cda78651e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2435,7 +2435,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.20.2 +xknx==0.20.3 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c669f014c39..0b69a7a97f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1575,7 +1575,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.20.2 +xknx==0.20.3 # homeassistant.components.bluesound # homeassistant.components.fritz From cdc979e1e6100300e87875516819088d1bbb6bf9 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sat, 16 Apr 2022 12:08:21 -0400 Subject: [PATCH 06/36] Bump pymazda to 0.3.3 (#70136) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index a75c7f99e4c..a18b4406355 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.3.2"], + "requirements": ["pymazda==0.3.3"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 22cda78651e..af898c00b1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1619,7 +1619,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.2 +pymazda==0.3.3 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b69a7a97f1..1c55984b551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1077,7 +1077,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.2 +pymazda==0.3.3 # homeassistant.components.melcloud pymelcloud==2.5.6 From 2205b6377848038e44e3fc51b941aecef9d7f035 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Apr 2022 00:12:16 -1000 Subject: [PATCH 07/36] Ensure powerwall retries setup when api returns too many requests (#70143) --- homeassistant/components/powerwall/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 6537ea249c1..af75ace8bd4 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -162,6 +162,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Authentication failed", exc_info=err) http_session.close() raise ConfigEntryAuthFailed from err + except APIError as err: + http_session.close() + raise ConfigEntryNotReady from err gateway_din = base_info.gateway_din if gateway_din and entry.unique_id is not None and is_ip_address(entry.unique_id): From 70a4de5efeb7d9fb717f7a95ab03dede45763362 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 16 Apr 2022 17:08:54 +0200 Subject: [PATCH 08/36] Fix StationInfo not string in Trafikverket Train (#70153) --- homeassistant/components/trafikverket_train/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 82f248f9de0..7bcd796bb16 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -6,7 +6,7 @@ import logging from typing import Any from pytrafikverket import TrafikverketTrain -from pytrafikverket.trafikverket_train import TrainStop +from pytrafikverket.trafikverket_train import StationInfo, TrainStop import voluptuous as vol from homeassistant.components.sensor import ( @@ -166,8 +166,8 @@ class TrainSensor(SensorEntity): self, train_api: TrafikverketTrain, name: str, - from_station: str, - to_station: str, + from_station: StationInfo, + to_station: StationInfo, weekday: list, departuretime: time | None, entry_id: str, @@ -188,7 +188,7 @@ class TrainSensor(SensorEntity): configuration_url="https://api.trafikinfo.trafikverket.se/", ) self._attr_unique_id = create_unique_id( - from_station, to_station, departuretime, weekday + from_station.name, to_station.name, departuretime, weekday ) async def async_update(self) -> None: From f790a343c0dcc23c1a14349a407f78126a8eb63b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Apr 2022 18:17:07 +0200 Subject: [PATCH 09/36] Bumped version to 2022.4.5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8fa0b529c04..b307e8a5435 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index e0103f4657e..b1e8c6d8d40 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.4.4 +version = 2022.4.5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From bd02895781f570ed33c767cbe6df203e31f08293 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Apr 2022 22:33:18 -0700 Subject: [PATCH 10/36] Fix bug in google calendar offset calculation (#70024) (#70166) --- homeassistant/components/google/calendar.py | 15 +++-- tests/components/google/test_calendar.py | 74 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 1174868bb78..f9c05d3c846 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -120,13 +120,20 @@ class GoogleCalendarEventDevice(CalendarEventDevice): self._event: dict[str, Any] | None = None self._name: str = data[CONF_NAME] self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) - self._offset_reached = False + self._offset_value: timedelta | None = None self.entity_id = entity_id @property def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" - return {"offset_reached": self._offset_reached} + return {"offset_reached": self.offset_reached} + + @property + def offset_reached(self) -> bool: + """Return whether or not the event offset was reached.""" + if self._event and self._offset_value: + return is_offset_reached(get_date(self._event["start"]), self._offset_value) + return False @property def event(self) -> dict[str, Any] | None: @@ -187,6 +194,4 @@ class GoogleCalendarEventDevice(CalendarEventDevice): self._event.get("summary", ""), self._offset ) self._event["summary"] = summary - self._offset_reached = is_offset_reached( - get_date(self._event["start"]), offset - ) + self._offset_value = offset diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index a5aca8a27d4..dda4cddc962 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -505,3 +505,77 @@ async def test_scan_calendar_error( assert await component_setup() assert not hass.states.get(TEST_ENTITY) + + +async def test_future_event_update_behavior( + hass, mock_events_list_items, component_setup +): + """Test an future event that becomes active.""" + now = dt_util.now() + now_utc = dt_util.utcnow() + one_hour_from_now = now + datetime.timedelta(minutes=60) + end_event = one_hour_from_now + datetime.timedelta(minutes=90) + event = { + **TEST_EVENT, + "start": {"dateTime": one_hour_from_now.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + } + mock_events_list_items([event]) + assert await component_setup() + + # Event has not started yet + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == STATE_OFF + + # Advance time until event has started + now += datetime.timedelta(minutes=60) + now_utc += datetime.timedelta(minutes=30) + with patch("homeassistant.util.dt.utcnow", return_value=now_utc), patch( + "homeassistant.util.dt.now", return_value=now + ): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Event has started + state = hass.states.get(TEST_ENTITY) + assert state.state == STATE_ON + + +async def test_future_event_offset_update_behavior( + hass, mock_events_list_items, component_setup +): + """Test an future event that becomes active.""" + now = dt_util.now() + now_utc = dt_util.utcnow() + one_hour_from_now = now + datetime.timedelta(minutes=60) + end_event = one_hour_from_now + datetime.timedelta(minutes=90) + event_summary = "Test Event in Progress" + event = { + **TEST_EVENT, + "start": {"dateTime": one_hour_from_now.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + "summary": f"{event_summary} !!-15", + } + mock_events_list_items([event]) + assert await component_setup() + + # Event has not started yet + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == STATE_OFF + assert not state.attributes["offset_reached"] + + # Advance time until event has started + now += datetime.timedelta(minutes=45) + now_utc += datetime.timedelta(minutes=45) + with patch("homeassistant.util.dt.utcnow", return_value=now_utc), patch( + "homeassistant.util.dt.now", return_value=now + ): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Event has not started, but the offset was reached + state = hass.states.get(TEST_ENTITY) + assert state.state == STATE_OFF + assert state.attributes["offset_reached"] From 0b5b7d5907fc2c2ce110cdbf5e85b369f55ebcf1 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 18 Apr 2022 15:27:14 +0100 Subject: [PATCH 11/36] Improve roon integraton (#66000) * Update to new library, revise discovery to work with new library, specify port to work with new library. * Move user gui to fallback. * Revise tests. * Handle old config. * Improve debugging, refresh faster on load. * Remove duplicate. * Bump library version. * Fix docstring per review. * Review suggestion Co-authored-by: Martin Hjelmare * Review suggestion Co-authored-by: Martin Hjelmare * Add check for duplicate host. * Add error message to strings. * Tidy. * Review changes. * Remove default. Co-authored-by: Martin Hjelmare --- homeassistant/components/roon/__init__.py | 9 ++- homeassistant/components/roon/config_flow.py | 53 +++++++++++++----- homeassistant/components/roon/const.py | 1 + homeassistant/components/roon/manifest.json | 2 +- homeassistant/components/roon/server.py | 46 ++++++++++----- homeassistant/components/roon/strings.json | 8 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roon/test_config_flow.py | 59 ++++++++++++++++++-- 9 files changed, 142 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 133598070c2..9e5c38f0211 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -4,14 +4,17 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import CONF_ROON_NAME, DOMAIN from .server import RoonServer async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a roonserver from a config entry.""" hass.data.setdefault(DOMAIN, {}) - host = entry.data[CONF_HOST] + + # fallback to using host for compatibility with older configs + name = entry.data.get(CONF_ROON_NAME, entry.data[CONF_HOST]) + roonserver = RoonServer(hass, entry) if not await roonserver.async_setup(): @@ -23,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, identifiers={(DOMAIN, entry.entry_id)}, manufacturer="Roonlabs", - name=host, + name=f"Roon Core ({name})", ) return True diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index 31391a0ff36..6ccf97155c4 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -6,11 +6,13 @@ from roonapi import RoonApi, RoonDiscovery import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv from .const import ( AUTHENTICATE_TIMEOUT, CONF_ROON_ID, + CONF_ROON_NAME, DEFAULT_NAME, DOMAIN, ROON_APPINFO, @@ -18,7 +20,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): cv.string, + vol.Required("port", default=9330): cv.port, + } +) TIMEOUT = 120 @@ -45,7 +52,7 @@ class RoonHub: _LOGGER.debug("Servers = %s", servers) return servers - async def authenticate(self, host, servers): + async def authenticate(self, host, port, servers): """Authenticate with one or more roon servers.""" def stop_apis(apis): @@ -54,6 +61,7 @@ class RoonHub: token = None core_id = None + core_name = None secs = 0 if host is None: apis = [ @@ -61,7 +69,7 @@ class RoonHub: for server in servers ] else: - apis = [RoonApi(ROON_APPINFO, None, host, blocking_init=False)] + apis = [RoonApi(ROON_APPINFO, None, host, port, blocking_init=False)] while secs <= TIMEOUT: # Roon can discover multiple devices - not all of which are proper servers, so try and authenticate with them all. @@ -71,6 +79,7 @@ class RoonHub: secs += AUTHENTICATE_TIMEOUT if auth_api: core_id = auth_api[0].core_id + core_name = auth_api[0].core_name token = auth_api[0].token break @@ -78,7 +87,7 @@ class RoonHub: await self._hass.async_add_executor_job(stop_apis, apis) - return (token, core_id) + return (token, core_id, core_name) async def discover(hass): @@ -90,15 +99,21 @@ async def discover(hass): return servers -async def authenticate(hass: core.HomeAssistant, host, servers): +async def authenticate(hass: core.HomeAssistant, host, port, servers): """Connect and authenticate home assistant.""" hub = RoonHub(hass) - (token, core_id) = await hub.authenticate(host, servers) + (token, core_id, core_name) = await hub.authenticate(host, port, servers) if token is None: raise InvalidAuth - return {CONF_HOST: host, CONF_ROON_ID: core_id, CONF_API_KEY: token} + return { + CONF_HOST: host, + CONF_PORT: port, + CONF_ROON_ID: core_id, + CONF_ROON_NAME: core_name, + CONF_API_KEY: token, + } class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -109,33 +124,45 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Roon flow.""" self._host = None + self._port = None self._servers = [] async def async_step_user(self, user_input=None): - """Handle getting host details from the user.""" + """Get roon core details via discovery.""" - errors = {} self._servers = await discover(self.hass) # We discovered one or more roon - so skip to authentication if self._servers: return await self.async_step_link() + return await self.async_step_fallback() + + async def async_step_fallback(self, user_input=None): + """Get host and port details from the user.""" + errors = {} + if user_input is not None: self._host = user_input["host"] + self._port = user_input["port"] return await self.async_step_link() return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="fallback", data_schema=DATA_SCHEMA, errors=errors ) async def async_step_link(self, user_input=None): """Handle linking and authenticting with the roon server.""" - errors = {} if user_input is not None: + # Do not authenticate if the host is already configured + self._async_abort_entries_match({CONF_HOST: self._host}) + try: - info = await authenticate(self.hass, self._host, self._servers) + info = await authenticate( + self.hass, self._host, self._port, self._servers + ) + except InvalidAuth: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/roon/const.py b/homeassistant/components/roon/const.py index 7c9cd6c4999..74cf6a38160 100644 --- a/homeassistant/components/roon/const.py +++ b/homeassistant/components/roon/const.py @@ -5,6 +5,7 @@ AUTHENTICATE_TIMEOUT = 5 DOMAIN = "roon" CONF_ROON_ID = "roon_server_id" +CONF_ROON_NAME = "roon_server_name" DATA_CONFIGS = "roon_configs" diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index a3b22a3c2cc..297bf5e9d7a 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.38"], + "requirements": ["roonapi==0.1.1"], "codeowners": ["@pavoni"], "iot_class": "local_push", "loggers": ["roonapi"] diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index 8301bf73fdf..d5e4cded08d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -2,15 +2,16 @@ import asyncio import logging -from roonapi import RoonApi +from roonapi import RoonApi, RoonDiscovery -from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) +INITIAL_SYNC_INTERVAL = 5 FULL_SYNC_INTERVAL = 30 PLATFORMS = [Platform.MEDIA_PLAYER] @@ -33,23 +34,38 @@ class RoonServer: async def async_setup(self, tries=0): """Set up a roon server based on config parameters.""" - hass = self.hass - # Host will be None for configs using discovery - host = self.config_entry.data[CONF_HOST] - token = self.config_entry.data[CONF_API_KEY] - # Default to None for compatibility with older configs - core_id = self.config_entry.data.get(CONF_ROON_ID) - _LOGGER.debug("async_setup: host=%s core_id=%s token=%s", host, core_id, token) - self.roonapi = RoonApi( - ROON_APPINFO, token, host, blocking_init=False, core_id=core_id - ) + def get_roon_host(): + host = self.config_entry.data.get(CONF_HOST) + port = self.config_entry.data.get(CONF_PORT) + if host: + _LOGGER.debug("static roon core host=%s port=%s", host, port) + return (host, port) + + discover = RoonDiscovery(core_id) + server = discover.first() + discover.stop() + _LOGGER.debug("dynamic roon core core_id=%s server=%s", core_id, server) + return (server[0], server[1]) + + def get_roon_api(): + token = self.config_entry.data[CONF_API_KEY] + (host, port) = get_roon_host() + return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True) + + hass = self.hass + core_id = self.config_entry.data.get(CONF_ROON_ID) + + self.roonapi = await self.hass.async_add_executor_job(get_roon_api) + self.roonapi.register_state_callback( self.roonapi_state_callback, event_filter=["zones_changed"] ) # Default to 'host' for compatibility with older configs without core_id - self.roon_id = core_id if core_id is not None else host + self.roon_id = ( + core_id if core_id is not None else self.config_entry.data[CONF_HOST] + ) # initialize media_player platform hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) @@ -98,13 +114,14 @@ class RoonServer: async def async_do_loop(self): """Background work loop.""" self._exit = False + await asyncio.sleep(INITIAL_SYNC_INTERVAL) while not self._exit: await self.async_update_players() - # await self.async_update_playlists() await asyncio.sleep(FULL_SYNC_INTERVAL) async def async_update_changed_players(self, changed_zones_ids): """Update the players which were reported as changed by the Roon API.""" + _LOGGER.debug("async_update_changed_players %s", changed_zones_ids) for zone_id in changed_zones_ids: if zone_id not in self.roonapi.zones: # device was removed ? @@ -127,6 +144,7 @@ class RoonServer: async def async_update_players(self): """Periodic full scan of all devices.""" zone_ids = self.roonapi.zones.keys() + _LOGGER.debug("async_update_players %s", zone_ids) await self.async_update_changed_players(zone_ids) # check for any removed devices all_devs = {} diff --git a/homeassistant/components/roon/strings.json b/homeassistant/components/roon/strings.json index 565a66a1320..ce5827e2c6c 100644 --- a/homeassistant/components/roon/strings.json +++ b/homeassistant/components/roon/strings.json @@ -1,10 +1,12 @@ { "config": { "step": { - "user": { - "description": "Could not discover Roon server, please enter your the Hostname or IP.", + "user": {}, + "fallback": { + "description": "Could not discover Roon server, please enter your Hostname and Port.", "data": { - "host": "[%key:common::config_flow::data::host%]" + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" } }, "link": { diff --git a/requirements_all.txt b/requirements_all.txt index af898c00b1a..edc35a47e23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2073,7 +2073,7 @@ rokuecp==0.16.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.0.38 +roonapi==0.1.1 # homeassistant.components.rova rova==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c55984b551..899276c559a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1345,7 +1345,7 @@ rokuecp==0.16.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.0.38 +roonapi==0.1.1 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 686109f968e..7cc37bc73cd 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -1,9 +1,11 @@ """Test the roon config flow.""" from unittest.mock import patch -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.roon.const import DOMAIN +from tests.common import MockConfigEntry + class RoonApiMock: """Class to mock the Roon API for testing.""" @@ -18,8 +20,13 @@ class RoonApiMock: """Return the roon host.""" return "core_id" + @property + def core_name(self): + """Return the roon core name.""" + return "Roon Core" + def stop(self): - """Stop socket and discovery.""" + """Stop socket.""" return @@ -93,8 +100,10 @@ async def test_successful_discovery_and_auth(hass): assert result2["title"] == "Roon Labs Music Player" assert result2["data"] == { "host": None, + "port": None, "api_key": "good_token", "roon_server_id": "core_id", + "roon_server_name": "Roon Core", } @@ -119,11 +128,11 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass): # Should show the form if server was not discovered assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "fallback" assert result["errors"] == {} await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"} + result["flow_id"], {"host": "1.1.1.1", "port": 9331} ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -134,10 +143,52 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass): assert result2["data"] == { "host": "1.1.1.1", "api_key": "good_token", + "port": 9331, + "api_key": "good_token", "roon_server_id": "core_id", + "roon_server_name": "Roon Core", } +async def test_duplicate_config(hass): + """Test user adding the host via the form for host that is already configured.""" + + CONFIG = {"host": "1.1.1.1"} + + MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( + hass + ) + + with patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock(), + ), patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryFailedMock(), + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + # Should show the form if server was not discovered + assert result["type"] == "form" + assert result["step_id"] == "fallback" + assert result["errors"] == {} + + await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1", "port": 9331} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + async def test_successful_discovery_no_auth(hass): """Test successful discover, but failed auth.""" From df768c34e4c9c5d467bdb8f3811454b50cd35bd7 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Wed, 20 Apr 2022 14:53:20 +0200 Subject: [PATCH 12/36] Fix opening/closing for awning in Overkiz integration (#68890) --- .../overkiz/cover_entities/awning.py | 39 ++++++++++++- .../overkiz/cover_entities/generic_cover.py | 55 +++---------------- .../overkiz/cover_entities/vertical_cover.py | 41 +++++++++++++- 3 files changed, 84 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/overkiz/cover_entities/awning.py b/homeassistant/components/overkiz/cover_entities/awning.py index ebbff8710f3..19422d9c193 100644 --- a/homeassistant/components/overkiz/cover_entities/awning.py +++ b/homeassistant/components/overkiz/cover_entities/awning.py @@ -14,7 +14,12 @@ from homeassistant.components.cover import ( CoverDeviceClass, ) -from .generic_cover import COMMANDS_STOP, OverkizGenericCover +from .generic_cover import ( + COMMANDS_CLOSE, + COMMANDS_OPEN, + COMMANDS_STOP, + OverkizGenericCover, +) class Awning(OverkizGenericCover): @@ -67,3 +72,35 @@ class Awning(OverkizGenericCover): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self.executor.async_execute_command(OverkizCommand.UNDEPLOY) + + @property + def is_opening(self) -> bool | None: + """Return if the cover is opening or not.""" + if self.is_running(COMMANDS_OPEN): + return True + + # Check if cover is moving based on current state + is_moving = self.device.states.get(OverkizState.CORE_MOVING) + current_closure = self.device.states.get(OverkizState.CORE_DEPLOYMENT) + target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) + + if not is_moving or not current_closure or not target_closure: + return None + + return cast(int, current_closure.value) < cast(int, target_closure.value) + + @property + def is_closing(self) -> bool | None: + """Return if the cover is closing or not.""" + if self.is_running(COMMANDS_CLOSE): + return True + + # Check if cover is moving based on current state + is_moving = self.device.states.get(OverkizState.CORE_MOVING) + current_closure = self.device.states.get(OverkizState.CORE_DEPLOYMENT) + target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) + + if not is_moving or not current_closure or not target_closure: + return None + + return cast(int, current_closure.value) > cast(int, target_closure.value) diff --git a/homeassistant/components/overkiz/cover_entities/generic_cover.py b/homeassistant/components/overkiz/cover_entities/generic_cover.py index c25cd1ab806..6e927d52ecf 100644 --- a/homeassistant/components/overkiz/cover_entities/generic_cover.py +++ b/homeassistant/components/overkiz/cover_entities/generic_cover.py @@ -14,7 +14,8 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.components.overkiz.entity import OverkizEntity + +from ..entity import OverkizEntity ATTR_OBSTRUCTION_DETECTED = "obstruction-detected" @@ -111,55 +112,13 @@ class OverkizGenericCover(OverkizEntity, CoverEntity): if command := self.executor.select_command(*COMMANDS_STOP_TILT): await self.executor.async_execute_command(command) - @property - def is_opening(self) -> bool | None: - """Return if the cover is opening or not.""" - - if self.assumed_state: - return None - - # Check if cover movement execution is currently running - if any( + def is_running(self, commands: list[OverkizCommand]) -> bool: + """Return if the given commands are currently running.""" + return any( execution.get("device_url") == self.device.device_url - and execution.get("command_name") in COMMANDS_OPEN + COMMANDS_OPEN_TILT + and execution.get("command_name") in commands for execution in self.coordinator.executions.values() - ): - return True - - # Check if cover is moving based on current state - is_moving = self.device.states.get(OverkizState.CORE_MOVING) - current_closure = self.device.states.get(OverkizState.CORE_CLOSURE) - target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) - - if not is_moving or not current_closure or not target_closure: - return None - - return cast(int, current_closure.value) > cast(int, target_closure.value) - - @property - def is_closing(self) -> bool | None: - """Return if the cover is closing or not.""" - - if self.assumed_state: - return None - - # Check if cover movement execution is currently running - if any( - execution.get("device_url") == self.device.device_url - and execution.get("command_name") in COMMANDS_CLOSE + COMMANDS_CLOSE_TILT - for execution in self.coordinator.executions.values() - ): - return True - - # Check if cover is moving based on current state - is_moving = self.device.states.get(OverkizState.CORE_MOVING) - current_closure = self.device.states.get(OverkizState.CORE_CLOSURE) - target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) - - if not is_moving or not current_closure or not target_closure: - return None - - return cast(int, current_closure.value) < cast(int, target_closure.value) + ) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index ec502a403ad..70bc8fb1654 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -19,9 +19,14 @@ from homeassistant.components.cover import ( SUPPORT_STOP, CoverDeviceClass, ) -from homeassistant.components.overkiz.coordinator import OverkizDataUpdateCoordinator -from .generic_cover import COMMANDS_STOP, OverkizGenericCover +from ..coordinator import OverkizDataUpdateCoordinator +from .generic_cover import ( + COMMANDS_CLOSE_TILT, + COMMANDS_OPEN_TILT, + COMMANDS_STOP, + OverkizGenericCover, +) COMMANDS_OPEN = [OverkizCommand.OPEN, OverkizCommand.UP, OverkizCommand.CYCLE] COMMANDS_CLOSE = [OverkizCommand.CLOSE, OverkizCommand.DOWN, OverkizCommand.CYCLE] @@ -107,6 +112,38 @@ class VerticalCover(OverkizGenericCover): if command := self.executor.select_command(*COMMANDS_CLOSE): await self.executor.async_execute_command(command) + @property + def is_opening(self) -> bool | None: + """Return if the cover is opening or not.""" + if self.is_running(COMMANDS_OPEN + COMMANDS_OPEN_TILT): + return True + + # Check if cover is moving based on current state + is_moving = self.device.states.get(OverkizState.CORE_MOVING) + current_closure = self.device.states.get(OverkizState.CORE_CLOSURE) + target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) + + if not is_moving or not current_closure or not target_closure: + return None + + return cast(int, current_closure.value) > cast(int, target_closure.value) + + @property + def is_closing(self) -> bool | None: + """Return if the cover is closing or not.""" + if self.is_running(COMMANDS_CLOSE + COMMANDS_CLOSE_TILT): + return True + + # Check if cover is moving based on current state + is_moving = self.device.states.get(OverkizState.CORE_MOVING) + current_closure = self.device.states.get(OverkizState.CORE_CLOSURE) + target_closure = self.device.states.get(OverkizState.CORE_TARGET_CLOSURE) + + if not is_moving or not current_closure or not target_closure: + return None + + return cast(int, current_closure.value) < cast(int, target_closure.value) + class LowSpeedCover(VerticalCover): """Representation of an Overkiz Low Speed cover.""" From 0e89bde18996eb109a2d30e3275461b1aee020b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Apr 2022 19:32:31 -1000 Subject: [PATCH 13/36] Move handling of non-secure login to the elkm1 library (#69483) --- homeassistant/components/elkm1/__init__.py | 10 ---------- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 3fed62e961e..2536e5a8de0 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -351,21 +351,11 @@ async def async_wait_for_elk_to_sync( login_event.set() sync_event.set() - def first_response(*args, **kwargs): - _LOGGER.debug("ElkM1 received first response (VN)") - login_event.set() - def sync_complete(): sync_event.set() success = True elk.add_handler("login", login_status) - # VN is the first command sent for panel, when we get - # it back we now we are logged in either with or without a password - elk.add_handler("VN", first_response) - # Some panels do not respond to the vn request so we - # check for lw as well - elk.add_handler("LW", first_response) elk.add_handler("sync_complete", sync_complete) for name, event, timeout in ( ("login", login_event, login_timeout), diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 695b6bcd999..ceea8e92ca5 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==1.2.0"], + "requirements": ["elkm1-lib==1.2.2"], "dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }], "codeowners": ["@gwww", "@bdraco"], "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index edc35a47e23..e7929f55258 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -574,7 +574,7 @@ elgato==3.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==1.2.0 +elkm1-lib==1.2.2 # homeassistant.components.elmax elmax_api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 899276c559a..1f0627a93b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ dynalite_devices==0.1.46 elgato==3.0.0 # homeassistant.components.elkm1 -elkm1-lib==1.2.0 +elkm1-lib==1.2.2 # homeassistant.components.elmax elmax_api==0.0.2 From f4f1a36a166bf6c13f9eb1e2238aa89e5ad2fd13 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 19 Apr 2022 12:49:07 +0200 Subject: [PATCH 14/36] Fix file size last_updated (#70114) Co-authored-by: J. Nick Koston --- homeassistant/components/filesize/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 97fe5f5511d..22b8cd60d79 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -137,9 +137,10 @@ class FileSizeCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Can not retrieve file statistics {error}") from error size = statinfo.st_size - last_updated = datetime.fromtimestamp(statinfo.st_mtime).replace( + last_updated = datetime.utcfromtimestamp(statinfo.st_mtime).replace( tzinfo=dt_util.UTC ) + _LOGGER.debug("size %s, last updated %s", size, last_updated) data: dict[str, int | float | datetime] = { "file": round(size / 1e6, 2), From 40ba0e0aed962f4fe14b44a58df08a7f593724f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 17 Apr 2022 19:52:17 +0200 Subject: [PATCH 15/36] Revert zigpy-zigate to 0.7.4 (#70184) * Update requirements_all.txt revert to zigpy-zigate 0.7.4 * Update requirements_test_all.txt revert to zigpy-zigate 0.7.4 * Update manifest.json revert to zigpy-zigate 0.7.4 --- 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 c780bdcb16f..3704e9715fb 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.14.0", "zigpy==0.44.2", "zigpy-xbee==0.14.0", - "zigpy-zigate==0.8.0", + "zigpy-zigate==0.7.4", "zigpy-znp==0.7.0" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index e7929f55258..a065e8f366e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2488,7 +2488,7 @@ zigpy-deconz==0.14.0 zigpy-xbee==0.14.0 # homeassistant.components.zha -zigpy-zigate==0.8.0 +zigpy-zigate==0.7.4 # homeassistant.components.zha zigpy-znp==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f0627a93b8..32419b0f6f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1610,7 +1610,7 @@ zigpy-deconz==0.14.0 zigpy-xbee==0.14.0 # homeassistant.components.zha -zigpy-zigate==0.8.0 +zigpy-zigate==0.7.4 # homeassistant.components.zha zigpy-znp==0.7.0 From bc2c4a41b878c6a4d953ab7ede9e57914c9b95c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Apr 2022 19:31:48 -1000 Subject: [PATCH 16/36] Fix handling of powerview stale state (#70195) --- .../hunterdouglas_powerview/cover.py | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index d1f33387a15..32d78c5c8dd 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -46,10 +46,12 @@ _LOGGER = logging.getLogger(__name__) # Estimated time it takes to complete a transition # from one state to another -TRANSITION_COMPLETE_DURATION = 30 +TRANSITION_COMPLETE_DURATION = 40 PARALLEL_UPDATES = 1 +RESYNC_DELAY = 60 + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -103,6 +105,9 @@ def hass_position_to_hd(hass_position): class PowerViewShade(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" + # The hub frequently reports stale states + _attr_assumed_state = True + def __init__(self, coordinator, device_info, room_name, shade, name): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) @@ -112,6 +117,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): self._last_action_timestamp = 0 self._scheduled_transition_update = None self._current_cover_position = MIN_POSITION + self._forced_resync = None @property def extra_state_attributes(self): @@ -224,10 +230,12 @@ class PowerViewShade(ShadeEntity, CoverEntity): @callback def _async_cancel_scheduled_transition_update(self): """Cancel any previous updates.""" - if not self._scheduled_transition_update: - return - self._scheduled_transition_update() - self._scheduled_transition_update = None + if self._scheduled_transition_update: + self._scheduled_transition_update() + self._scheduled_transition_update = None + if self._forced_resync: + self._forced_resync() + self._forced_resync = None @callback def _async_schedule_update_for_transition(self, steps): @@ -260,6 +268,14 @@ class PowerViewShade(ShadeEntity, CoverEntity): _LOGGER.debug("Processing scheduled update for %s", self.name) self._scheduled_transition_update = None await self._async_force_refresh_state() + self._forced_resync = async_call_later( + self.hass, RESYNC_DELAY, self._async_force_resync + ) + + async def _async_force_resync(self, *_): + """Force a resync after an update since the hub may have stale state.""" + self._forced_resync = None + await self._async_force_refresh_state() async def _async_force_refresh_state(self): """Refresh the cover state and force the device cache to be bypassed.""" @@ -274,10 +290,14 @@ class PowerViewShade(ShadeEntity, CoverEntity): self.coordinator.async_add_listener(self._async_update_shade_from_group) ) + async def async_will_remove_from_hass(self): + """Cancel any pending refreshes.""" + self._async_cancel_scheduled_transition_update() + @callback def _async_update_shade_from_group(self): """Update with new data from the coordinator.""" - if self._scheduled_transition_update: + if self._scheduled_transition_update or self._forced_resync: # If a transition in in progress # the data will be wrong return From 82cf6e44d6cbcfc1cb8c6a42f4a21a46a9e13b01 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 18 Apr 2022 06:28:25 +0100 Subject: [PATCH 17/36] Auto set content type for stream-only in generic camera (#70200) --- .../components/generic/config_flow.py | 82 +++++-------------- tests/components/generic/test_config_flow.py | 19 +---- 2 files changed, 22 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index c6310d22dce..270a71911f0 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -109,20 +109,6 @@ def build_schema( return vol.Schema(spec) -def build_schema_content_type(user_input: dict[str, Any] | MappingProxyType[str, Any]): - """Create schema for conditional 2nd page specifying stream content_type.""" - return vol.Schema( - { - vol.Required( - CONF_CONTENT_TYPE, - description={ - "suggested_value": user_input.get(CONF_CONTENT_TYPE, "image/jpeg") - }, - ): str, - } - ) - - def get_image_type(image): """Get the format of downloaded bytes that could be an image.""" fmt = None @@ -283,17 +269,16 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): if not errors: user_input[CONF_CONTENT_TYPE] = still_format user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False - if user_input.get(CONF_STILL_IMAGE_URL): - await self.async_set_unique_id(self.flow_id) - return self.async_create_entry( - title=name, data={}, options=user_input - ) - # If user didn't specify a still image URL, - # we can't (yet) autodetect it from the stream. - # Show a conditional 2nd page to ask them the content type. - self.cached_user_input = user_input - self.cached_title = name - return await self.async_step_content_type() + if still_url is None: + # If user didn't specify a still image URL, + # The automatically generated still image that stream generates + # is always jpeg + user_input[CONF_CONTENT_TYPE] = "image/jpeg" + + await self.async_set_unique_id(self.flow_id) + return self.async_create_entry( + title=name, data={}, options=user_input + ) else: user_input = DEFAULT_DATA.copy() @@ -303,22 +288,6 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_content_type( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the user's choice for stream content_type.""" - if user_input is not None: - user_input = self.cached_user_input | user_input - await self.async_set_unique_id(self.flow_id) - return self.async_create_entry( - title=self.cached_title, data={}, options=user_input - ) - return self.async_show_form( - step_id="content_type", - data_schema=build_schema_content_type({}), - errors={}, - ) - async def async_step_import(self, import_config) -> FlowResult: """Handle config import from yaml.""" # abort if we've already got this one. @@ -362,6 +331,11 @@ class GenericOptionsFlowHandler(OptionsFlow): stream_url = user_input.get(CONF_STREAM_SOURCE) if not errors: title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + if still_url is None: + # If user didn't specify a still image URL, + # The automatically generated still image that stream generates + # is always jpeg + still_format = "image/jpeg" data = { CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION), CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE), @@ -376,30 +350,12 @@ class GenericOptionsFlowHandler(OptionsFlow): CONF_FRAMERATE: user_input[CONF_FRAMERATE], CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], } - if still_url: - return self.async_create_entry( - title=title, - data=data, - ) - self.cached_title = title - self.cached_user_input = data - return await self.async_step_content_type() - + return self.async_create_entry( + title=title, + data=data, + ) return self.async_show_form( step_id="init", data_schema=build_schema(user_input or self.config_entry.options, True), errors=errors, ) - - async def async_step_content_type( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the user's choice for stream content_type.""" - if user_input is not None: - user_input = self.cached_user_input | user_input - return self.async_create_entry(title=self.cached_title, data=user_input) - return self.async_show_form( - step_id="content_type", - data_schema=build_schema_content_type(self.cached_user_input), - errors={}, - ) diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index aab04dae203..007e7fc8ebd 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -203,15 +203,10 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): data = TESTDATA.copy() data.pop(CONF_STILL_IMAGE_URL) with mock_av_open as mock_setup: - result2 = await hass.config_entries.flow.async_configure( + result3 = await hass.config_entries.flow.async_configure( result["flow_id"], data, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_CONTENT_TYPE: "image/jpeg"}, - ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "127_0_0_1_testurl_2" @@ -516,20 +511,12 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open): assert result["step_id"] == "init" # try updating the config options - result2 = await hass.config_entries.options.async_configure( + result3 = await hass.config_entries.options.async_configure( result["flow_id"], user_input=data, ) - # Should be shown a 2nd form - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "content_type" - - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={CONF_CONTENT_TYPE: "image/png"}, - ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["data"][CONF_CONTENT_TYPE] == "image/png" + assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" # These below can be deleted after deprecation period is finished. From acd6048cbd0bea73939f013e23d538240cb1f8ab Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 18 Apr 2022 15:57:52 +0100 Subject: [PATCH 18/36] Hide credentials from generated titles in generic camera (#70204) --- homeassistant/components/generic/config_flow.py | 5 ++--- tests/components/generic/test_config_flow.py | 13 +++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 270a71911f0..55541a6ea68 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -8,13 +8,13 @@ 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 import voluptuous as vol +import yarl from homeassistant.components.stream.const import SOURCE_TIMEOUT from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow @@ -172,8 +172,7 @@ def slug_url(url) -> str | None: """Convert a camera url into a string suitable for a camera name.""" if not url: return None - url_no_scheme = urlparse(url)._replace(scheme="") - return slugify(urlunparse(url_no_scheme).strip("/")) + return slugify(yarl.URL(url).host) async def async_test_stream(hass, info) -> dict[str, str]: diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 007e7fc8ebd..3caa1aa7adf 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -62,7 +62,7 @@ async def test_form(hass, fakeimg_png, mock_av_open, user_flow): TESTDATA, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", @@ -96,7 +96,7 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): data, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -176,7 +176,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): ) assert "errors" not in result2, f"errors={result2['errors']}" assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -202,6 +202,7 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): ) data = TESTDATA.copy() data.pop(CONF_STILL_IMAGE_URL) + data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2" with mock_av_open as mock_setup: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -209,10 +210,10 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == "127_0_0_1_testurl_2" + assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, - CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_STREAM_SOURCE: "rtsp://user:pass@127.0.0.1/testurl/2", CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, @@ -227,7 +228,7 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): "homeassistant.components.generic.camera.GenericCamera.async_camera_image", return_value=fakeimgbytes_jpg, ): - image_obj = await async_get_image(hass, "camera.127_0_0_1_testurl_2") + image_obj = await async_get_image(hass, "camera.127_0_0_1") assert image_obj.content == fakeimgbytes_jpg assert len(mock_setup.mock_calls) == 1 From 2b39f4b7cdde8b9c6eb74219066903fffea12cdc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Apr 2022 19:29:48 -1000 Subject: [PATCH 19/36] Bump aiodiscover to 1.4.9 (#70213) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index fb9ebc70408..e540d077781 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.8"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.9"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1ebfb713b70..f3df4c88c80 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==2.3.0 PyNaCl==1.5.0 -aiodiscover==1.4.8 +aiodiscover==1.4.9 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index a065e8f366e..9bcf8ba109a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.8 +aiodiscover==1.4.9 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32419b0f6f6..7f24fcced3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.8 +aiodiscover==1.4.9 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 03a896cbc89ae46570c6a9cc4eefe3601c493a67 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 18 Apr 2022 23:51:05 -0700 Subject: [PATCH 20/36] Screenlogic config: Filter unexpected host names (#70256) --- homeassistant/components/screenlogic/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 260317dca11..1aeedfb421d 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -35,8 +35,9 @@ async def async_discover_gateways_by_unique_id(hass): return discovered_gateways for host in hosts: - mac = _extract_mac_from_name(host[SL_GATEWAY_NAME]) - discovered_gateways[mac] = host + if (name := host[SL_GATEWAY_NAME]).startswith("Pentair:"): + mac = _extract_mac_from_name(name) + discovered_gateways[mac] = host _LOGGER.debug("Discovered gateways: %s", discovered_gateways) return discovered_gateways From 767e519e861bc2efd026a2138358e7b0c49f3c09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Apr 2022 07:07:00 -1000 Subject: [PATCH 21/36] Fix handling unassigned areas in lutron_caseta (#70258) --- .../components/lutron_caseta/__init__.py | 43 ++++++++++++------- .../components/lutron_caseta/const.py | 2 + 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index ebd9e041332..d0561bc47ab 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -12,7 +12,7 @@ from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, Platform +from homeassistant.const import ATTR_SUGGESTED_AREA, CONF_HOST, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -41,6 +41,7 @@ from .const import ( DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, + UNASSIGNED_AREA, ) from .device_trigger import ( DEVICE_TYPE_SUBTYPE_MAP_TO_LIP, @@ -195,22 +196,31 @@ def _async_register_button_devices( if "serial" not in device or device["serial"] in seen: continue seen.add(device["serial"]) + device_args = { + "name": device["name"], + "manufacturer": MANUFACTURER, + "config_entry_id": config_entry_id, + "identifiers": {(DOMAIN, device["serial"])}, + "model": f"{device['model']} ({device['type']})", + "via_device": (DOMAIN, bridge_device["serial"]), + } + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + device_args["suggested_area"] = area - dr_device = device_registry.async_get_or_create( - name=device["name"], - suggested_area=device["name"].split("_")[0], - manufacturer=MANUFACTURER, - config_entry_id=config_entry_id, - identifiers={(DOMAIN, device["serial"])}, - model=f"{device['model']} ({device['type']})", - via_device=(DOMAIN, bridge_device["serial"]), - ) - + dr_device = device_registry.async_get_or_create(**device_args) button_devices_by_dr_id[dr_device.id] = device return button_devices_by_dr_id +def _area_and_name_from_name(device_name: str) -> tuple[str, str]: + """Return the area and name from the devices internal name.""" + if "_" in device_name: + return device_name.split("_", 1) + return UNASSIGNED_AREA, device_name + + @callback def _async_subscribe_pico_remote_events( hass: HomeAssistant, @@ -230,7 +240,7 @@ def _async_subscribe_pico_remote_events( action = ACTION_RELEASE type_ = device["type"] - area, name = device["name"].split("_", 1) + area, name = _area_and_name_from_name(device["name"]) button_number = device["button_number"] # The original implementation used LIP instead of LEAP # so we need to convert the button number to maintain compat @@ -322,15 +332,18 @@ class LutronCasetaDevice(Entity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return DeviceInfo( + device = self._device + info = DeviceInfo( identifiers={(DOMAIN, self.serial)}, manufacturer=MANUFACTURER, - model=f"{self._device['model']} ({self._device['type']})", + model=f"{device['model']} ({device['type']})", name=self.name, - suggested_area=self._device["name"].split("_")[0], via_device=(DOMAIN, self._bridge_device["serial"]), configuration_url="https://device-login.lutron.com", ) + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + info[ATTR_SUGGESTED_AREA] = area @property def extra_state_attributes(self): diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 90303dc0023..56a3821dd64 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -33,3 +33,5 @@ ACTION_RELEASE = "release" CONF_SUBTYPE = "subtype" BRIDGE_TIMEOUT = 35 + +UNASSIGNED_AREA = "Unassigned" From 2c6e670fd84c02813289d6a7797692b553858299 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 19 Apr 2022 02:45:39 -0400 Subject: [PATCH 22/36] Fix updating CameraZone coords for UniFi Protect (#70260) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index e120e4ada4e..d5dbb51ffc1 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.3.0", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.4.0", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 9bcf8ba109a..3cbcae74441 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.3.0 +pyunifiprotect==3.4.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f24fcced3f..426c490798f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.3.0 +pyunifiprotect==3.4.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 596d46dbba561bbe57f14090efe5fabc767b9e48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Apr 2022 10:07:35 -0700 Subject: [PATCH 23/36] Add Insteon USB discovery (#70306) * Add Insteon USB discovery * Update tests/components/insteon/test_config_flow.py Co-authored-by: Martin Hjelmare * Black Co-authored-by: Martin Hjelmare --- .../components/insteon/config_flow.py | 40 ++++++++++++++ .../components/insteon/manifest.json | 8 ++- homeassistant/components/insteon/strings.json | 4 ++ .../components/insteon/translations/en.json | 4 ++ homeassistant/generated/usb.py | 4 ++ tests/components/insteon/test_config_flow.py | 54 +++++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index d9081c5b45e..5bf8769f321 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -1,19 +1,24 @@ """Test config flow for Insteon.""" +from __future__ import annotations + import logging from pyinsteon import async_connect import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_HOST, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -107,6 +112,9 @@ def _remove_x10(device, options): class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Insteon config flow handler.""" + _device_path: str | None = None + _device_name: str | None = None + @staticmethod @callback def async_get_options_flow(config_entry): @@ -177,6 +185,38 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return self.async_create_entry(title="", data=import_info) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + """Handle USB discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + dev_path = await self.hass.async_add_executor_job( + usb.get_serial_by_id, discovery_info.device + ) + self._device_path = dev_path + self._device_name = usb.human_readable_device_name( + dev_path, + discovery_info.serial_number, + discovery_info.manufacturer, + discovery_info.description, + discovery_info.vid, + discovery_info.pid, + ) + self._set_confirm_only() + self.context["title_placeholders"] = {CONF_NAME: self._device_name} + await self.async_set_unique_id(config_entries.DEFAULT_DISCOVERY_UNIQUE_ID) + return await self.async_step_confirm_usb() + + async def async_step_confirm_usb(self, user_input=None): + """Confirm a discovery.""" + if user_input is not None: + return await self.async_step_plm({CONF_DEVICE: self._device_path}) + + return self.async_show_form( + step_id="confirm_usb", + description_placeholders={CONF_NAME: self._device_name}, + ) + class InsteonOptionsFlowHandler(config_entries.OptionsFlow): """Handle an Insteon options flow.""" diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index e9f5e60f9f8..63eb24ee453 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -6,5 +6,11 @@ "codeowners": ["@teharris1"], "config_flow": true, "iot_class": "local_push", - "loggers": ["pyinsteon", "pypubsub"] + "loggers": ["pyinsteon", "pypubsub"], + "after_dependencies": ["usb"], + "usb": [ + { + "vid": "10BF" + } + ] } diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index ca88b43956f..793a38a2694 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "description": "Select the Insteon modem type.", @@ -31,6 +32,9 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "confirm_usb": { + "description": "Do you want to setup {name}?" } }, "error": { diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index 18217bb2842..4c4a439b938 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -8,7 +8,11 @@ "cannot_connect": "Failed to connect", "select_single": "Select one option." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Do you want to setup {name}?" + }, "hubv1": { "data": { "host": "IP Address", diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 1ba9b235f85..2e5104ce66d 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -6,6 +6,10 @@ To update, run python3 -m script.hassfest # fmt: off USB = [ + { + "domain": "insteon", + "vid": "10BF" + }, { "domain": "modem_callerid", "vid": "0572", diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 9ca54ea8d8f..878b540b721 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow +from homeassistant.components import usb from homeassistant.components.insteon.config_flow import ( HUB1, HUB2, @@ -594,3 +595,56 @@ async def test_options_override_bad_data(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "input_error"} + + +async def test_discovery_via_usb(hass): + """Test usb flow.""" + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyINSTEON", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="insteon radio", + manufacturer="test", + ) + result = await hass.config_entries.flow.async_init( + "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm_usb" + + with patch("homeassistant.components.insteon.config_flow.async_connect"), patch( + "homeassistant.components.insteon.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == {"device": "/dev/ttyINSTEON"} + + +async def test_discovery_via_usb_already_setup(hass): + """Test usb flow -- already setup.""" + + MockConfigEntry( + domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE: "/dev/ttyUSB1"}} + ).add_to_hass(hass) + + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyINSTEON", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="insteon radio", + manufacturer="test", + ) + result = await hass.config_entries.flow.async_init( + "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" From 6f13ce9445be2279ec7b2222742506451d5a346b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Apr 2022 00:02:17 -1000 Subject: [PATCH 24/36] Bump aiohomekit to 0.7.17 (#70313) Changelog: https://github.com/Jc2k/aiohomekit/compare/0.7.16...0.7.17 Fixes: #67665 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 9ca447ad2fe..ae9b1261bc8 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.16"], + "requirements": ["aiohomekit==0.7.17"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 3cbcae74441..a75516a4668 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -162,7 +162,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.16 +aiohomekit==0.7.17 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 426c490798f..77df1910b77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.16 +aiohomekit==0.7.17 # homeassistant.components.emulated_hue # homeassistant.components.http From 50bfc4d86c8ebf27e9cf4205958b31ffa67a5ffa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Apr 2022 10:11:19 -0700 Subject: [PATCH 25/36] Bumped version to 2022.4.6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b307e8a5435..02e1b896df1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index b1e8c6d8d40..903d09f4267 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.4.5 +version = 2022.4.6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From c4db48d8f18fe902e7f0e43b2ea72e20b85a13ca Mon Sep 17 00:00:00 2001 From: Johann Vanackere Date: Wed, 20 Apr 2022 19:43:44 +0200 Subject: [PATCH 26/36] Fix #69952: Daikin AC Temperature jumps after being set (#70326) --- homeassistant/components/daikin/climate.py | 11 +++++++--- .../daikin/test_temperature_format.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/components/daikin/test_temperature_format.py diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 4b97c8dc21a..3d064f47e97 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -117,6 +117,11 @@ async def async_setup_entry( async_add_entities([DaikinClimate(daikin_api)], update_before_add=True) +def format_target_temperature(target_temperature): + """Format target temperature to be sent to the Daikin unit, taking care of keeping at most 1 decimal digit.""" + return str(round(float(target_temperature), 1)).rstrip("0").rstrip(".") + + class DaikinClimate(ClimateEntity): """Representation of a Daikin HVAC.""" @@ -163,9 +168,9 @@ class DaikinClimate(ClimateEntity): # temperature elif attr == ATTR_TEMPERATURE: try: - values[HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE]] = str( - round(float(value), 1) - ) + values[ + HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE] + ] = format_target_temperature(value) except ValueError: _LOGGER.error("Invalid temperature %s", value) diff --git a/tests/components/daikin/test_temperature_format.py b/tests/components/daikin/test_temperature_format.py new file mode 100644 index 00000000000..d05efa98b8d --- /dev/null +++ b/tests/components/daikin/test_temperature_format.py @@ -0,0 +1,20 @@ +"""The tests for the Daikin target temperature conversion.""" +from homeassistant.components.daikin.climate import format_target_temperature + + +def test_int_conversion(): + """Check no decimal are kept when target temp is an integer.""" + formatted = format_target_temperature("16") + assert formatted == "16" + + +def test_decimal_conversion(): + """Check 1 decimal is kept when target temp is a decimal.""" + formatted = format_target_temperature("16.1") + assert formatted == "16.1" + + +def test_decimal_conversion_more_digits(): + """Check at most 1 decimal is kept when target temp is a decimal with more than 1 decimal.""" + formatted = format_target_temperature("16.09") + assert formatted == "16.1" From cbdef596519d3c94d3fb8fa680cf475b8d1372cf Mon Sep 17 00:00:00 2001 From: Dmitry Katsubo Date: Sat, 23 Apr 2022 09:29:25 +0200 Subject: [PATCH 27/36] Fixed syntax error in ALTER TABLE statement (#70304) (#70336) --- homeassistant/components/recorder/migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 26234be0502..87cfb88032f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -544,7 +544,7 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 # https://github.com/home-assistant/core/issues/56104 text( f"ALTER TABLE {table} CONVERT TO " - "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci LOCK=EXCLUSIVE" + "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, LOCK=EXCLUSIVE" ) ) elif new_version == 22: From 1dfd76085ed9b20fe63576d68952cf13bd754b00 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 21 Apr 2022 01:16:08 +0200 Subject: [PATCH 28/36] Update xknx to version 0.20.4 (#70342) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f11ef8c56cb..1058bf25ad8 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.20.3"], + "requirements": ["xknx==0.20.4"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a75516a4668..46d54af904d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2435,7 +2435,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.20.3 +xknx==0.20.4 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77df1910b77..fd3c8de1fea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1575,7 +1575,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.20.3 +xknx==0.20.4 # homeassistant.components.bluesound # homeassistant.components.fritz From 0d42bda3164d2f567769d2a4ead4de90c2fcea3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Apr 2022 13:43:27 -1000 Subject: [PATCH 29/36] Bump aiodiscover to 1.4.10 (#70348) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index e540d077781..87de91b091f 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.9"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.10"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f3df4c88c80..1d98d9b518d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==2.3.0 PyNaCl==1.5.0 -aiodiscover==1.4.9 +aiodiscover==1.4.10 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 46d54af904d..d95d8fff083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.9 +aiodiscover==1.4.10 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd3c8de1fea..d0fa7f74a2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.9 +aiodiscover==1.4.10 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From d54558530c2c881831a24dfed95ef9adadc2ff61 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 21 Apr 2022 08:14:39 -0400 Subject: [PATCH 30/36] Bump zwave-js-server-python to 0.35.3 (#70357) --- homeassistant/components/zwave_js/__init__.py | 20 ++++++++-------- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_events.py | 23 +++++++++++++++++++ 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 12ff9acb530..fa87ad954ad 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections import defaultdict from collections.abc import Callable +from typing import Any from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient @@ -289,12 +290,7 @@ async def async_setup_entry( # noqa: C901 ) ) # add listener for stateless node notification events - entry.async_on_unload( - node.on( - "notification", - lambda event: async_on_notification(event["notification"]), - ) - ) + entry.async_on_unload(node.on("notification", async_on_notification)) async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" @@ -402,12 +398,14 @@ async def async_setup_entry( # noqa: C901 ) @callback - def async_on_notification( - notification: EntryControlNotification - | NotificationNotification - | PowerLevelNotification, - ) -> None: + def async_on_notification(event: dict[str, Any]) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" + if "notification" not in event: + LOGGER.info("Unknown notification: %s", event) + return + notification: EntryControlNotification | NotificationNotification | PowerLevelNotification = event[ + "notification" + ] device = dev_reg.async_get_device({get_device_id(client, notification.node)}) # We assert because we know the device exists assert device diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index c3968181563..abdb5d6fbb8 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.35.2"], + "requirements": ["zwave-js-server-python==0.35.3"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index d95d8fff083..5032e75edba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ zigpy==0.44.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.35.2 +zwave-js-server-python==0.35.3 # homeassistant.components.zwave_me zwave_me_ws==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0fa7f74a2e..8d8087546ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1619,7 +1619,7 @@ zigpy-znp==0.7.0 zigpy==0.44.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.35.2 +zwave-js-server-python==0.35.3 # homeassistant.components.zwave_me zwave_me_ws==0.2.3 diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 32859ae3c37..5085175de83 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -307,3 +307,26 @@ async def test_unknown_notification(hass, hank_binary_switch, integration, clien notification_obj.node = node with pytest.raises(TypeError): node.emit("notification", {"notification": notification_obj}) + + notification_events = async_capture_events(hass, "zwave_js_notification") + + # Test a valid notification with an unsupported command class + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": node.node_id, + "ccId": 0, + "args": { + "commandClassName": "No Operation", + "commandClass": 0, + "testNodeId": 1, + "status": 0, + "acknowledgedFrames": 2, + }, + }, + ) + node.receive_event(event) + + assert not notification_events From cad90a086dff9cb5ed62327ccb5e8501d14d00ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Sat, 23 Apr 2022 07:28:26 +0200 Subject: [PATCH 31/36] Bump pyplaato to 0.0.18 (#70391) --- homeassistant/components/plaato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index 5335a79fe15..4a88db58934 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@JohNan"], - "requirements": ["pyplaato==0.0.16"], + "requirements": ["pyplaato==0.0.18"], "iot_class": "cloud_push", "loggers": ["pyplaato"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5032e75edba..76df7c829f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pypck==0.7.14 pypjlink2==1.2.1 # homeassistant.components.plaato -pyplaato==0.0.16 +pyplaato==0.0.18 # homeassistant.components.point pypoint==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d8087546ff..cd5da626f71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1154,7 +1154,7 @@ pyownet==0.10.0.post1 pypck==0.7.14 # homeassistant.components.plaato -pyplaato==0.0.16 +pyplaato==0.0.18 # homeassistant.components.point pypoint==2.3.0 From e389beb1d1a640083a42065b0c17254a366cea0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Apr 2022 06:26:06 -1000 Subject: [PATCH 32/36] Bump aiodiscover to 1.4.11 (#70413) Fixes #70402 Changelog: https://github.com/bdraco/aiodiscover/compare/v1.4.10...v1.4.11 --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 87de91b091f..fea7772f72d 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.10"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.11"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1d98d9b518d..948fea5fa44 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==2.3.0 PyNaCl==1.5.0 -aiodiscover==1.4.10 +aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 76df7c829f4..c1ca25c6f4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.10 +aiodiscover==1.4.11 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd5da626f71..66bc32309f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.10 +aiodiscover==1.4.11 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From c7ff4baa6b64435d5de1e492b74299e019eba0d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Apr 2022 03:58:19 -1000 Subject: [PATCH 33/36] Ensure rainmachine creates config entry titles as strings (#70417) --- homeassistant/components/rainmachine/config_flow.py | 2 +- tests/components/rainmachine/conftest.py | 4 +++- tests/components/rainmachine/test_config_flow.py | 4 ++-- tests/components/rainmachine/test_diagnostics.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 989616f6367..d24dae46c2b 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -132,7 +132,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # access token without using the IP address and password, so we have to # store it: return self.async_create_entry( - title=controller.name, + title=str(controller.name), data={ CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], CONF_PASSWORD: user_input[CONF_PASSWORD], diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 3a32b3b7c9a..457df4a3ef2 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -49,7 +49,9 @@ def controller_fixture( controller = AsyncMock() controller.api_version = "4.5.0" controller.hardware_version = 3 - controller.name = "My RainMachine" + # The api returns a controller with all numbers as numeric + # instead of a string + controller.name = 12345 controller.mac = controller_mac controller.software_version = "4.0.925" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 5a0e1fc08cc..8b313eb2fb5 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -124,7 +124,7 @@ async def test_step_user(hass, config, setup_rainmachine): data=config, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "My RainMachine" + assert result["title"] == "12345" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "password", @@ -232,7 +232,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "My RainMachine" + assert result2["title"] == "12345" assert result2["data"] == { CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "password", diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index 575e596abc2..d770d01fd36 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -585,7 +585,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach "controller": { "api_version": "4.5.0", "hardware_version": 3, - "name": "My RainMachine", + "name": 12345, "software_version": "4.0.925", }, }, From 6f6b16b4101b675409c26a9b5ad87a87841dab4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Apr 2022 11:34:00 -1000 Subject: [PATCH 34/36] Fix history not including start time state (#70447) --- homeassistant/components/recorder/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 82a74c36a83..95f69bf769f 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -395,7 +395,7 @@ def _get_states_with_session( if ( run is None - and (run := (recorder.run_information_from_instance(hass, utc_point_in_time))) + and (run := (recorder.run_information_with_session(session, utc_point_in_time))) is None ): # History did not run before utc_point_in_time From 9925a19a75a936384c083eb84b7d7e6c073914a1 Mon Sep 17 00:00:00 2001 From: Johann Vanackere Date: Sun, 24 Apr 2022 23:47:09 +0200 Subject: [PATCH 35/36] Daikin AC : Round to nearest half degree (#70446) (#70452) --- homeassistant/components/daikin/climate.py | 4 ++-- .../components/daikin/test_temperature_format.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 3d064f47e97..fb708916039 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -118,8 +118,8 @@ async def async_setup_entry( def format_target_temperature(target_temperature): - """Format target temperature to be sent to the Daikin unit, taking care of keeping at most 1 decimal digit.""" - return str(round(float(target_temperature), 1)).rstrip("0").rstrip(".") + """Format target temperature to be sent to the Daikin unit, rounding to nearest half degree.""" + return str(round(float(target_temperature) * 2, 0) / 2).rstrip("0").rstrip(".") class DaikinClimate(ClimateEntity): diff --git a/tests/components/daikin/test_temperature_format.py b/tests/components/daikin/test_temperature_format.py index d05efa98b8d..bc085fa1fd8 100644 --- a/tests/components/daikin/test_temperature_format.py +++ b/tests/components/daikin/test_temperature_format.py @@ -8,13 +8,13 @@ def test_int_conversion(): assert formatted == "16" -def test_decimal_conversion(): +def test_rounding(): """Check 1 decimal is kept when target temp is a decimal.""" formatted = format_target_temperature("16.1") - assert formatted == "16.1" - - -def test_decimal_conversion_more_digits(): - """Check at most 1 decimal is kept when target temp is a decimal with more than 1 decimal.""" - formatted = format_target_temperature("16.09") - assert formatted == "16.1" + assert formatted == "16" + formatted = format_target_temperature("16.3") + assert formatted == "16.5" + formatted = format_target_temperature("16.65") + assert formatted == "16.5" + formatted = format_target_temperature("16.9") + assert formatted == "17" From f3d12635cce3679454303c4ad2e98c91a85c6f6a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 24 Apr 2022 14:47:52 -0700 Subject: [PATCH 36/36] Bumped version to 2022.4.7 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 02e1b896df1..e8eee10d950 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 903d09f4267..e19c4dc9572 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.4.6 +version = 2022.4.7 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0