From ab2e891e91cec4db721a336fb5ac42bdd616c2d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 May 2023 17:55:47 +0200 Subject: [PATCH 01/66] Bumped version to 2023.6.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 212000aa26d..6c6b21ad0d4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 9bb55d3d29d..2f1bec4171b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0.dev0" +version = "2023.6.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 408f0bdd6b4e0c697e66d0c9dcc6f60ba1fb4c0d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 1 Jun 2023 02:03:55 +0100 Subject: [PATCH 02/66] Always update Filter sensors attr on new_state (#89096) * always update attr * reset filter on unit change --- homeassistant/components/filter/sensor.py | 27 +++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 9b1e2250a28..a733040da01 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -18,10 +18,8 @@ from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, - STATE_CLASSES as SENSOR_STATE_CLASSES, SensorDeviceClass, SensorEntity, ) @@ -273,22 +271,15 @@ class SensorFilter(SensorEntity): self._state = temp_state.state - if self._attr_icon is None: - self._attr_icon = new_state.attributes.get(ATTR_ICON, ICON) + self._attr_icon = new_state.attributes.get(ATTR_ICON, ICON) + self._attr_device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) + self._attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS) - if ( - self._attr_device_class is None - and new_state.attributes.get(ATTR_DEVICE_CLASS) in SENSOR_DEVICE_CLASSES + if self._attr_native_unit_of_measurement != new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT ): - self._attr_device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) - - if ( - self._attr_state_class is None - and new_state.attributes.get(ATTR_STATE_CLASS) in SENSOR_STATE_CLASSES - ): - self._attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS) - - if self._attr_native_unit_of_measurement is None: + for filt in self._filters: + filt.reset() self._attr_native_unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT ) @@ -460,6 +451,10 @@ class Filter: """Return whether the current filter_state should be skipped.""" return self._skip_processing + def reset(self) -> None: + """Reset filter.""" + self.states.clear() + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement filter.""" raise NotImplementedError() From 22ed622152f8a6556eec69c8950b82e04d2f3454 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 1 Jun 2023 02:10:15 +0100 Subject: [PATCH 03/66] Delay filter integration until after HA has started (#91034) * delay filter start * Update homeassistant/components/filter/sensor.py * Update homeassistant/components/filter/sensor.py --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/filter/sensor.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index a733040da01..a1470baa4d2 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -39,6 +39,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -351,11 +352,16 @@ class SensorFilter(SensorEntity): if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]: self._update_filter_sensor_state(state, False) - self.async_on_remove( - async_track_state_change_event( - self.hass, [self._entity], self._update_filter_sensor_state_event + @callback + def _async_hass_started(hass: HomeAssistant) -> None: + """Delay source entity tracking.""" + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._entity], self._update_filter_sensor_state_event + ) ) - ) + + self.async_on_remove(async_at_started(self.hass, _async_hass_started)) @property def native_value(self) -> datetime | StateType: From d10dd54d883820697dd10c5f2a77e0e103dad89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 1 Jun 2023 02:09:23 +0200 Subject: [PATCH 04/66] Update aioairzone-cloud to v0.1.7 (#93871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone-cloud to v0.1.7 Signed-off-by: Álvaro Fernández Rojas * airzone_cloud: fix copy&paste description Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/entity.py | 6 ++++++ homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone_cloud/util.py | 5 +++++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index 59f6aa14bf8..c7e59ee1a3f 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -6,6 +6,7 @@ from typing import Any from aioairzone_cloud.const import ( AZD_AIDOOS, + AZD_AVAILABLE, AZD_FIRMWARE, AZD_NAME, AZD_SYSTEM_ID, @@ -26,6 +27,11 @@ from .coordinator import AirzoneUpdateCoordinator class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator], ABC): """Define an Airzone Cloud entity.""" + @property + def available(self) -> bool: + """Return Airzone Cloud entity availability.""" + return super().available and self.get_airzone_value(AZD_AVAILABLE) + @abstractmethod def get_airzone_value(self, key: str) -> Any: """Return Airzone Cloud entity value by key.""" diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index d03fe5913c2..b2899a7c80c 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.1.6"] + "requirements": ["aioairzone-cloud==0.1.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd6e25400a3..f95653df215 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -116,7 +116,7 @@ aio_georss_gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.1.6 +aioairzone-cloud==0.1.7 # homeassistant.components.airzone aioairzone==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fe6051f676..b16d42bedf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -106,7 +106,7 @@ aio_georss_gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.1.6 +aioairzone-cloud==0.1.7 # homeassistant.components.airzone aioairzone==0.6.1 diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 044cf880a16..4eab870297b 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -32,6 +32,7 @@ from aioairzone_cloud.const import ( API_SYSTEM_NUMBER, API_TYPE, API_WARNINGS, + API_WS_CONNECTED, API_WS_FW, API_WS_ID, API_WS_IDS, @@ -160,6 +161,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: return { API_ERRORS: [], API_IS_CONNECTED: True, + API_WS_CONNECTED: True, API_LOCAL_TEMP: { API_CELSIUS: 21, API_FAH: 70, @@ -170,12 +172,14 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: return { API_ERRORS: [], API_IS_CONNECTED: True, + API_WS_CONNECTED: True, API_WARNINGS: [], } if device.get_id() == "zone2": return { API_HUMIDITY: 24, API_IS_CONNECTED: True, + API_WS_CONNECTED: True, API_LOCAL_TEMP: { API_FAH: 77, API_CELSIUS: 25, @@ -185,6 +189,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: return { API_HUMIDITY: 30, API_IS_CONNECTED: True, + API_WS_CONNECTED: True, API_LOCAL_TEMP: { API_FAH: 68, API_CELSIUS: 20, From d6f2e1cdffa0b36afd55ff9e124891ed030dc1f5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 31 May 2023 20:08:18 -0400 Subject: [PATCH 05/66] Increase Zigbee command retries (#93877) * Enable retries for requests * Update unit tests * Account for fewer group retries in unit tests --- .../zha/core/cluster_handlers/__init__.py | 4 + .../zha/test_alarm_control_panel.py | 5 + tests/components/zha/test_device_action.py | 4 +- tests/components/zha/test_discover.py | 2 +- tests/components/zha/test_light.py | 98 +++++++++++-------- tests/components/zha/test_switch.py | 4 +- 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index 7863b043455..ec29e4e53eb 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -45,6 +45,8 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +DEFAULT_REQUEST_RETRIES = 3 + class AttrReportConfig(TypedDict, total=True): """Configuration to report for the attributes.""" @@ -78,6 +80,8 @@ def decorate_command(cluster_handler, command): @wraps(command) async def wrapper(*args, **kwds): + kwds.setdefault("tries", DEFAULT_REQUEST_RETRIES) + try: result = await command(*args, **kwds) cluster_handler.debug( diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 319301cf7dc..34ce746e128 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -96,6 +96,7 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, + tries=3, ) # disarm from HA @@ -134,6 +135,7 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.Emergency, + tries=3, ) # reset the panel @@ -157,6 +159,7 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, + tries=3, ) # arm_night from HA @@ -177,6 +180,7 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, + tries=3, ) # reset the panel @@ -274,5 +278,6 @@ async def reset_alarm_panel(hass, cluster, entity_id): 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, + tries=3, ) cluster.client_command.reset_mock() diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index f1ab44f69eb..9d9a4bc2a54 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -328,7 +328,7 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None: 5, expect_reply=False, manufacturer=4151, - tries=1, + tries=3, tsn=None, ) in cluster.request.call_args_list @@ -345,7 +345,7 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None: 5, expect_reply=False, manufacturer=4151, - tries=1, + tries=3, tsn=None, ) in cluster.request.call_args_list diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 236a3c4ad86..a87d624ec00 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -131,7 +131,7 @@ async def test_devices( ), expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) ] diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index c4751f7e7f6..5ea71573a27 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -553,7 +553,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -589,7 +589,7 @@ async def test_transitions( transition_time=35, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -600,7 +600,7 @@ async def test_transitions( transition_time=35, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -637,7 +637,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -674,7 +674,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -685,7 +685,7 @@ async def test_transitions( transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_level.request.call_args_list[1] == call( @@ -696,7 +696,7 @@ async def test_transitions( transition_time=10, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -758,7 +758,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -769,7 +769,7 @@ async def test_transitions( transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_level.request.call_args_list[1] == call( @@ -780,7 +780,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -838,7 +838,7 @@ async def test_transitions( dev1_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -850,7 +850,7 @@ async def test_transitions( transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -910,7 +910,7 @@ async def test_transitions( transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -968,7 +968,7 @@ async def test_transitions( transition_time=1, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev2_cluster_color.request.call_args == call( @@ -979,7 +979,7 @@ async def test_transitions( transition_time=1, # sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev2_cluster_level.request.call_args_list[1] == call( @@ -990,7 +990,7 @@ async def test_transitions( transition_time=10, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -1121,7 +1121,7 @@ async def test_transitions( transition_time=20, # transition time expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -1151,7 +1151,7 @@ async def test_transitions( transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -1184,7 +1184,7 @@ async def test_transitions( eWeLink_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -1195,7 +1195,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -1261,7 +1261,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: dev1_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -1272,7 +1272,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -1319,7 +1319,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -1330,7 +1330,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) assert dev1_cluster_level.request.call_args_list[1] == call( @@ -1341,7 +1341,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -1373,7 +1373,9 @@ async def async_test_on_from_light(hass, cluster, entity_id): assert hass.states.get(entity_id).state == STATE_ON -async def async_test_on_off_from_hass(hass, cluster, entity_id): +async def async_test_on_off_from_hass( + hass, cluster, entity_id, expected_tries: int = 3 +): """Test on off functionality from hass.""" # turn on via UI cluster.request.reset_mock() @@ -1388,14 +1390,16 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) - await async_test_off_from_hass(hass, cluster, entity_id) + await async_test_off_from_hass( + hass, cluster, entity_id, expected_tries=expected_tries + ) -async def async_test_off_from_hass(hass, cluster, entity_id): +async def async_test_off_from_hass(hass, cluster, entity_id, expected_tries: int = 3): """Test turning off the light from Home Assistant.""" # turn off via UI @@ -1411,13 +1415,18 @@ async def async_test_off_from_hass(hass, cluster, entity_id): cluster.commands_by_name["off"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) async def async_test_level_on_off_from_hass( - hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 + hass, + on_off_cluster, + level_cluster, + entity_id, + expected_default_transition: int = 0, + expected_tries: int = 3, ): """Test on off functionality from hass.""" @@ -1439,7 +1448,7 @@ async def async_test_level_on_off_from_hass( on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) on_off_cluster.request.reset_mock() @@ -1463,7 +1472,7 @@ async def async_test_level_on_off_from_hass( on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) assert level_cluster.request.call_args == call( @@ -1474,7 +1483,7 @@ async def async_test_level_on_off_from_hass( transition_time=100, expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) on_off_cluster.request.reset_mock() @@ -1499,13 +1508,15 @@ async def async_test_level_on_off_from_hass( transition_time=int(expected_default_transition), expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() - await async_test_off_from_hass(hass, on_off_cluster, entity_id) + await async_test_off_from_hass( + hass, on_off_cluster, entity_id, expected_tries=expected_tries + ) async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): @@ -1522,7 +1533,9 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected assert hass.states.get(entity_id).attributes.get("brightness") == level -async def async_test_flash_from_hass(hass, cluster, entity_id, flash): +async def async_test_flash_from_hass( + hass, cluster, entity_id, flash, expected_tries: int = 3 +): """Test flash functionality from hass.""" # turn on via UI cluster.request.reset_mock() @@ -1542,7 +1555,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): effect_variant=general.Identify.EffectVariant.Default, expect_reply=True, manufacturer=None, - tries=1, + tries=expected_tries, tsn=None, ) @@ -1642,13 +1655,15 @@ async def test_zha_group_light_entity( assert "color_mode" not in group_state.attributes # test turning the lights on and off from the HA - await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) + await async_test_on_off_from_hass( + hass, group_cluster_on_off, group_entity_id, expected_tries=1 + ) await async_shift_time(hass) # test short flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, group_entity_id, FLASH_SHORT + hass, group_cluster_identify, group_entity_id, FLASH_SHORT, expected_tries=1 ) await async_shift_time(hass) @@ -1663,6 +1678,7 @@ async def test_zha_group_light_entity( group_cluster_level, group_entity_id, expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition + expected_tries=1, ) await async_shift_time(hass) @@ -1683,7 +1699,7 @@ async def test_zha_group_light_entity( # test long flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, group_entity_id, FLASH_LONG + hass, group_cluster_identify, group_entity_id, FLASH_LONG, expected_tries=1 ) await async_shift_time(hass) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 9f98acb9359..8fb7825a953 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -176,7 +176,7 @@ async def test_switch( cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) @@ -196,7 +196,7 @@ async def test_switch( cluster.commands_by_name["off"].schema, expect_reply=True, manufacturer=None, - tries=1, + tries=3, tsn=None, ) From fb50f0d875a6993adefe4fecfffd27dee49d9279 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 May 2023 21:14:59 -0400 Subject: [PATCH 06/66] Bump frontend to 20230601.0 (#93884) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 12f98ef39f0..bde1977b1c1 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230531.0"] + "requirements": ["home-assistant-frontend==20230601.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dd2f8b44fb4..c8c085b761c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.1.0 hass-nabucasa==0.67.1 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230531.0 +home-assistant-frontend==20230601.0 home-assistant-intents==2023.5.30 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f95653df215..89bf881e716 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230531.0 +home-assistant-frontend==20230601.0 # homeassistant.components.conversation home-assistant-intents==2023.5.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b16d42bedf3..0ca7cb47c34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -716,7 +716,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230531.0 +home-assistant-frontend==20230601.0 # homeassistant.components.conversation home-assistant-intents==2023.5.30 From 65a9bd661d44103df78c63975de44369ad191678 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 May 2023 21:17:59 -0400 Subject: [PATCH 07/66] Bumped version to 2023.6.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6c6b21ad0d4..37eb0d02c60 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 2f1bec4171b..9d075644101 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b0" +version = "2023.6.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 32571af131e8609cf3e92ed396448de3fe6a1d9e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Jun 2023 12:32:14 +0200 Subject: [PATCH 08/66] Add `silabs_multiprotocol` platform (#92904) * Add silabs_multiprotocol platform * Add new files * Add ZHA tests * Prevent ZHA from creating database during tests * Add delay parameter to async_change_channel * Add the updated dataset to the dataset store * Allow MultipanProtocol.async_change_channel to return a task * Notify user about the duration of migration * Update tests --- .../silabs_multiprotocol_addon.py | 261 ++++++++++++++++-- .../homeassistant_hardware/strings.json | 19 ++ .../homeassistant_sky_connect/strings.json | 19 ++ .../homeassistant_yellow/strings.json | 19 ++ .../components/otbr/silabs_multiprotocol.py | 87 ++++++ homeassistant/components/otbr/util.py | 50 ++-- homeassistant/components/zha/config_flow.py | 2 +- .../components/zha/silabs_multiprotocol.py | 81 ++++++ .../homeassistant_hardware/conftest.py | 13 +- .../test_silabs_multiprotocol_addon.py | 209 +++++++++++++- .../homeassistant_sky_connect/conftest.py | 13 +- .../homeassistant_yellow/conftest.py | 13 +- tests/components/otbr/conftest.py | 12 +- tests/components/otbr/test_config_flow.py | 17 +- tests/components/otbr/test_init.py | 30 +- .../otbr/test_silabs_multiprotocol.py | 175 ++++++++++++ tests/components/otbr/test_util.py | 55 +--- tests/components/otbr/test_websocket_api.py | 27 +- .../zha/test_silabs_multiprotocol.py | 118 ++++++++ 19 files changed, 1072 insertions(+), 148 deletions(-) create mode 100644 homeassistant/components/otbr/silabs_multiprotocol.py create mode 100644 homeassistant/components/zha/silabs_multiprotocol.py create mode 100644 tests/components/otbr/test_silabs_multiprotocol.py create mode 100644 tests/components/zha/test_silabs_multiprotocol.py diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 8c502f080f6..34ab9a3cedb 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod import asyncio import dataclasses import logging -from typing import Any +from typing import Any, Protocol import voluptuous as vol import yarl @@ -19,12 +19,19 @@ from homeassistant.components.hassio import ( hostname_from_addon_slug, is_hassio, ) -from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN -from homeassistant.components.zha.radio_manager import ZhaMultiPANMigrationHelper from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) from homeassistant.helpers.singleton import singleton +from homeassistant.helpers.storage import Store from .const import LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG @@ -39,17 +46,144 @@ CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware" CONF_ADDON_DEVICE = "device" CONF_ENABLE_MULTI_PAN = "enable_multi_pan" +DEFAULT_CHANNEL = 15 +DEFAULT_CHANNEL_CHANGE_DELAY = 5 * 60 # Thread recommendation + +STORAGE_KEY = "homeassistant_hardware.silabs" +STORAGE_VERSION_MAJOR = 1 +STORAGE_VERSION_MINOR = 1 +SAVE_DELAY = 10 + @singleton(DATA_ADDON_MANAGER) -@callback -def get_addon_manager(hass: HomeAssistant) -> AddonManager: +async def get_addon_manager(hass: HomeAssistant) -> MultiprotocolAddonManager: """Get the add-on manager.""" - return AddonManager( - hass, - LOGGER, - "Silicon Labs Multiprotocol", - SILABS_MULTIPROTOCOL_ADDON_SLUG, - ) + manager = MultiprotocolAddonManager(hass) + await manager.async_setup() + return manager + + +class MultiprotocolAddonManager(AddonManager): + """Silicon Labs Multiprotocol add-on manager.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the manager.""" + super().__init__( + hass, + LOGGER, + "Silicon Labs Multiprotocol", + SILABS_MULTIPROTOCOL_ADDON_SLUG, + ) + self._channel: int | None = None + self._platforms: dict[str, MultipanProtocol] = {} + self._store: Store[dict[str, Any]] = Store( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + minor_version=STORAGE_VERSION_MINOR, + ) + + async def async_setup(self) -> None: + """Set up the manager.""" + await async_process_integration_platforms( + self._hass, "silabs_multiprotocol", self._register_multipan_platform + ) + await self.async_load() + + async def _register_multipan_platform( + self, hass: HomeAssistant, integration_domain: str, platform: MultipanProtocol + ) -> None: + """Register a multipan platform.""" + self._platforms[integration_domain] = platform + if self._channel is not None or not await platform.async_using_multipan(hass): + return + + new_channel = await platform.async_get_channel(hass) + if new_channel is None: + return + + _LOGGER.info( + "Setting multipan channel to %s (source: '%s')", + new_channel, + integration_domain, + ) + self.async_set_channel(new_channel) + + async def async_change_channel( + self, channel: int, delay: float + ) -> list[asyncio.Task]: + """Change the channel and notify platforms.""" + self.async_set_channel(channel) + + tasks = [] + + for platform in self._platforms.values(): + if not await platform.async_using_multipan(self._hass): + continue + task = await platform.async_change_channel(self._hass, channel, delay) + if not task: + continue + tasks.append(task) + + return tasks + + @callback + def async_get_channel(self) -> int | None: + """Get the channel.""" + return self._channel + + @callback + def async_set_channel(self, channel: int) -> None: + """Set the channel without notifying platforms. + + This must only be called when first initializing the manager. + """ + self._channel = channel + self.async_schedule_save() + + async def async_load(self) -> None: + """Load the store.""" + data = await self._store.async_load() + + if data is not None: + self._channel = data["channel"] + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the store.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + """Return data to store in a file.""" + data: dict[str, Any] = {} + data["channel"] = self._channel + return data + + +class MultipanProtocol(Protocol): + """Define the format of multipan platforms.""" + + async def async_change_channel( + self, hass: HomeAssistant, channel: int, delay: float + ) -> asyncio.Task | None: + """Set the channel to be used. + + Does nothing if not configured or the multiprotocol add-on is not used. + """ + + async def async_get_channel(self, hass: HomeAssistant) -> int | None: + """Return the channel. + + Returns None if not configured or the multiprotocol add-on is not used. + """ + + async def async_using_multipan(self, hass: HomeAssistant) -> bool: + """Return if the multiprotocol device is used. + + Returns False if not configured. + """ @dataclasses.dataclass @@ -82,6 +216,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Set up the options flow.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.zha.radio_manager import ( + ZhaMultiPANMigrationHelper, + ) + # If we install the add-on we should uninstall it on entry remove. self.install_task: asyncio.Task | None = None self.start_task: asyncio.Task | None = None @@ -117,7 +256,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def _async_get_addon_info(self) -> AddonInfo: """Return and cache Silicon Labs Multiprotocol add-on info.""" - addon_manager: AddonManager = get_addon_manager(self.hass) + addon_manager: AddonManager = await get_addon_manager(self.hass) try: addon_info: AddonInfo = await addon_manager.async_get_addon_info() except AddonError as err: @@ -128,7 +267,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def _async_set_addon_config(self, config: dict) -> None: """Set Silicon Labs Multiprotocol add-on config.""" - addon_manager: AddonManager = get_addon_manager(self.hass) + addon_manager: AddonManager = await get_addon_manager(self.hass) try: await addon_manager.async_set_addon_options(config) except AddonError as err: @@ -137,7 +276,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def _async_install_addon(self) -> None: """Install the Silicon Labs Multiprotocol add-on.""" - addon_manager: AddonManager = get_addon_manager(self.hass) + addon_manager: AddonManager = await get_addon_manager(self.hass) try: await addon_manager.async_schedule_install_addon() finally: @@ -213,6 +352,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Configure the Silicon Labs Multiprotocol add-on.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN + + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.zha.radio_manager import ( + ZhaMultiPANMigrationHelper, + ) + + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.zha.silabs_multiprotocol import ( + async_get_channel as async_get_zha_channel, + ) + addon_info = await self._async_get_addon_info() addon_config = addon_info.options @@ -224,6 +376,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): **dataclasses.asdict(serial_port_settings), } + multipan_channel = DEFAULT_CHANNEL + # Initiate ZHA migration zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN) @@ -247,6 +401,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): _LOGGER.exception("Unexpected exception during ZHA migration") raise AbortFlow("zha_migration_failed") from err + if (zha_channel := await async_get_zha_channel(self.hass)) is not None: + multipan_channel = zha_channel + + # Initialize the shared channel + multipan_manager = await get_addon_manager(self.hass) + multipan_manager.async_set_channel(multipan_channel) + if new_addon_config != addon_config: # Copy the add-on config to keep the objects separate. self.original_addon_config = dict(addon_config) @@ -283,7 +444,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def _async_start_addon(self) -> None: """Start Silicon Labs Multiprotocol add-on.""" - addon_manager: AddonManager = get_addon_manager(self.hass) + addon_manager: AddonManager = await get_addon_manager(self.hass) try: await addon_manager.async_schedule_start_addon() finally: @@ -319,9 +480,73 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): serial_device = (await self._async_serial_port_settings()).device if addon_info.options.get(CONF_ADDON_DEVICE) == serial_device: - return await self.async_step_show_revert_guide() + return await self.async_step_show_addon_menu() return await self.async_step_addon_installed_other_device() + async def async_step_show_addon_menu( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Show menu options for the addon.""" + return self.async_show_menu( + step_id="addon_menu", + menu_options=[ + "reconfigure_addon", + "uninstall_addon", + ], + ) + + async def async_step_reconfigure_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Reconfigure the addon.""" + multipan_manager = await get_addon_manager(self.hass) + + if user_input is None: + channels = [str(x) for x in range(11, 27)] + suggested_channel = DEFAULT_CHANNEL + if (channel := multipan_manager.async_get_channel()) is not None: + suggested_channel = channel + data_schema = vol.Schema( + { + vol.Required( + "channel", + description={"suggested_value": str(suggested_channel)}, + ): SelectSelector( + SelectSelectorConfig( + options=channels, mode=SelectSelectorMode.DROPDOWN + ) + ) + } + ) + return self.async_show_form( + step_id="reconfigure_addon", data_schema=data_schema + ) + + # Change the shared channel + await multipan_manager.async_change_channel( + int(user_input["channel"]), DEFAULT_CHANNEL_CHANGE_DELAY + ) + return await self.async_step_notify_channel_change() + + async def async_step_notify_channel_change( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Notify that the channel change will take about five minutes.""" + if user_input is None: + return self.async_show_form( + step_id="notify_channel_change", + description_placeholders={ + "delay_minutes": str(DEFAULT_CHANNEL_CHANGE_DELAY // 60) + }, + ) + return self.async_create_entry(title="", data={}) + + async def async_step_uninstall_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Uninstall the addon (not implemented).""" + return await self.async_step_show_revert_guide() + async def async_step_show_revert_guide( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -348,7 +573,7 @@ async def check_multi_pan_addon(hass: HomeAssistant) -> None: if not is_hassio(hass): return - addon_manager: AddonManager = get_addon_manager(hass) + addon_manager: AddonManager = await get_addon_manager(hass) try: addon_info: AddonInfo = await addon_manager.async_get_addon_info() except AddonError as err: @@ -375,7 +600,7 @@ async def multi_pan_addon_using_device(hass: HomeAssistant, device_path: str) -> if not is_hassio(hass): return False - addon_manager: AddonManager = get_addon_manager(hass) + addon_manager: AddonManager = await get_addon_manager(hass) addon_info: AddonInfo = await addon_manager.async_get_addon_info() if addon_info.state != AddonState.RUNNING: diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 47549794fc8..60501397557 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -12,15 +12,34 @@ "addon_installed_other_device": { "title": "Multiprotocol support is already enabled for another device" }, + "addon_menu": { + "menu_options": { + "reconfigure_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", + "uninstall_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]" + } + }, "install_addon": { "title": "The Silicon Labs Multiprotocol add-on installation has started" }, + "notify_channel_change": { + "title": "Channel change initiated", + "description": "A Zigbee and Thread channel change has been initiated and will finish in {delay_minutes} minutes." + }, + "reconfigure_addon": { + "title": "Reconfigure IEEE 802.15.4 radio multiprotocol support", + "data": { + "channel": "Channel" + } + }, "show_revert_guide": { "title": "Multiprotocol support is enabled for this device", "description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio" }, "start_addon": { "title": "The Silicon Labs Multiprotocol add-on is starting." + }, + "uninstall_addon": { + "title": "Remove IEEE 802.15.4 radio multiprotocol support." } }, "error": { diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 970f9d97a4c..415df2092a1 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -11,15 +11,34 @@ "addon_installed_other_device": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" }, + "addon_menu": { + "menu_options": { + "reconfigure_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_menu::menu_options::reconfigure_addon%]", + "uninstall_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_menu::menu_options::uninstall_addon%]" + } + }, "install_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" }, + "notify_channel_change": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]" + }, + "reconfigure_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", + "data": { + "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::data::channel%]" + } + }, "show_revert_guide": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" }, "start_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" + }, + "uninstall_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]" } }, "error": { diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index d97b01c7c84..c1069a7e755 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -11,6 +11,12 @@ "addon_installed_other_device": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" }, + "addon_menu": { + "menu_options": { + "reconfigure_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_menu::menu_options::reconfigure_addon%]", + "uninstall_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_menu::menu_options::uninstall_addon%]" + } + }, "hardware_settings": { "title": "Configure hardware settings", "data": { @@ -22,6 +28,10 @@ "install_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" }, + "notify_channel_change": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]" + }, "main_menu": { "menu_options": { "hardware_settings": "[%key:component::homeassistant_yellow::options::step::hardware_settings::title%]", @@ -36,12 +46,21 @@ "reboot_now": "Reboot now" } }, + "reconfigure_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", + "data": { + "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::data::channel%]" + } + }, "show_revert_guide": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" }, "start_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" + }, + "uninstall_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]" } }, "error": { diff --git a/homeassistant/components/otbr/silabs_multiprotocol.py b/homeassistant/components/otbr/silabs_multiprotocol.py new file mode 100644 index 00000000000..9a462c4610b --- /dev/null +++ b/homeassistant/components/otbr/silabs_multiprotocol.py @@ -0,0 +1,87 @@ +"""Silicon Labs Multiprotocol support.""" + +from __future__ import annotations + +import asyncio +import logging + +import aiohttp +from python_otbr_api import tlv_parser +from python_otbr_api.tlv_parser import MeshcopTLVType + +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + is_multiprotocol_url, +) +from homeassistant.components.thread import async_add_dataset +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import DOMAIN +from .util import OTBRData + +_LOGGER = logging.getLogger(__name__) + + +async def async_change_channel(hass: HomeAssistant, channel: int, delay: float) -> None: + """Set the channel to be used. + + Does nothing if not configured. + """ + if DOMAIN not in hass.data: + return + + data: OTBRData = hass.data[DOMAIN] + await data.set_channel(channel, delay) + + # Import the new dataset + dataset_tlvs = await data.get_pending_dataset_tlvs() + if dataset_tlvs is None: + # The activation timer may have expired already + dataset_tlvs = await data.get_active_dataset_tlvs() + if dataset_tlvs is None: + # Don't try to import a None dataset + return + + dataset = tlv_parser.parse_tlv(dataset_tlvs.hex()) + dataset.pop(MeshcopTLVType.DELAYTIMER, None) + dataset.pop(MeshcopTLVType.PENDINGTIMESTAMP, None) + dataset_tlvs_str = tlv_parser.encode_tlv(dataset) + await async_add_dataset(hass, DOMAIN, dataset_tlvs_str) + + +async def async_get_channel(hass: HomeAssistant) -> int | None: + """Return the channel. + + Returns None if not configured. + """ + if DOMAIN not in hass.data: + return None + + data: OTBRData = hass.data[DOMAIN] + + try: + dataset = await data.get_active_dataset() + except ( + HomeAssistantError, + aiohttp.ClientError, + asyncio.TimeoutError, + ) as err: + _LOGGER.warning("Failed to communicate with OTBR %s", err) + return None + + if dataset is None: + return None + + return dataset.channel + + +async def async_using_multipan(hass: HomeAssistant) -> bool: + """Return if the multiprotocol device is used. + + Returns False if not configured. + """ + if DOMAIN not in hass.data: + return False + + data: OTBRData = hass.data[DOMAIN] + return is_multiprotocol_url(data.url) diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 5541ecb6874..5caebba5eb5 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -2,22 +2,22 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -import contextlib import dataclasses from functools import wraps from typing import Any, Concatenate, ParamSpec, TypeVar, cast import python_otbr_api -from python_otbr_api import tlv_parser +from python_otbr_api import PENDING_DATASET_DELAY_TIMER, tlv_parser from python_otbr_api.pskc import compute_pskc from python_otbr_api.tlv_parser import MeshcopTLVType from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + MultiprotocolAddonManager, + get_addon_manager, is_multiprotocol_url, multi_pan_addon_using_device, ) from homeassistant.components.homeassistant_yellow import RADIO_DEVICE as YELLOW_RADIO -from homeassistant.components.zha import api as zha_api from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir @@ -73,11 +73,21 @@ class OTBRData: """Enable or disable the router.""" return await self.api.set_enabled(enabled) + @_handle_otbr_error + async def get_active_dataset(self) -> python_otbr_api.ActiveDataSet | None: + """Get current active operational dataset, or None.""" + return await self.api.get_active_dataset() + @_handle_otbr_error async def get_active_dataset_tlvs(self) -> bytes | None: """Get current active operational dataset in TLVS format, or None.""" return await self.api.get_active_dataset_tlvs() + @_handle_otbr_error + async def get_pending_dataset_tlvs(self) -> bytes | None: + """Get current pending operational dataset in TLVS format, or None.""" + return await self.api.get_pending_dataset_tlvs() + @_handle_otbr_error async def create_active_dataset( self, dataset: python_otbr_api.ActiveDataSet @@ -90,43 +100,27 @@ class OTBRData: """Set current active operational dataset in TLVS format.""" await self.api.set_active_dataset_tlvs(dataset) + @_handle_otbr_error + async def set_channel( + self, channel: int, delay: float = PENDING_DATASET_DELAY_TIMER / 1000 + ) -> None: + """Set current channel.""" + await self.api.set_channel(channel, delay=int(delay * 1000)) + @_handle_otbr_error async def get_extended_address(self) -> bytes: """Get extended address (EUI-64).""" return await self.api.get_extended_address() -def _get_zha_url(hass: HomeAssistant) -> str | None: - """Get ZHA radio path, or None if there's no ZHA config entry.""" - with contextlib.suppress(ValueError): - return zha_api.async_get_radio_path(hass) - return None - - -async def _get_zha_channel(hass: HomeAssistant) -> int | None: - """Get ZHA channel, or None if there's no ZHA config entry.""" - zha_network_settings: zha_api.NetworkBackup | None - with contextlib.suppress(ValueError): - zha_network_settings = await zha_api.async_get_network_settings(hass) - if not zha_network_settings: - return None - channel: int = zha_network_settings.network_info.channel - # ZHA uses channel 0 when no channel is set - return channel or None - - async def get_allowed_channel(hass: HomeAssistant, otbr_url: str) -> int | None: """Return the allowed channel, or None if there's no restriction.""" if not is_multiprotocol_url(otbr_url): # The OTBR is not sharing the radio, no restriction return None - zha_url = _get_zha_url(hass) - if not zha_url or not is_multiprotocol_url(zha_url): - # ZHA is not configured or not sharing the radio with this OTBR, no restriction - return None - - return await _get_zha_channel(hass) + addon_manager: MultiprotocolAddonManager = await get_addon_manager(hass) + return addon_manager.async_get_channel() async def _warn_on_channel_collision( diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index d0f124a0838..91bc2ac42a2 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -96,7 +96,7 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: yellow_radio.manufacturer = "Nabu Casa" # Present the multi-PAN addon as a setup option, if it's available - addon_manager = silabs_multiprotocol_addon.get_addon_manager(hass) + addon_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) try: addon_info = await addon_manager.async_get_addon_info() diff --git a/homeassistant/components/zha/silabs_multiprotocol.py b/homeassistant/components/zha/silabs_multiprotocol.py new file mode 100644 index 00000000000..aec52b4ac75 --- /dev/null +++ b/homeassistant/components/zha/silabs_multiprotocol.py @@ -0,0 +1,81 @@ +"""Silicon Labs Multiprotocol support.""" + +from __future__ import annotations + +import asyncio +import contextlib + +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + is_multiprotocol_url, +) +from homeassistant.core import HomeAssistant + +from . import api + +# The approximate time it takes ZHA to change channels on SiLabs coordinators +ZHA_CHANNEL_CHANGE_TIME_S = 10.27 + + +def _get_zha_url(hass: HomeAssistant) -> str | None: + """Return the ZHA radio path, or None if there's no ZHA config entry.""" + with contextlib.suppress(ValueError): + return api.async_get_radio_path(hass) + return None + + +async def _get_zha_channel(hass: HomeAssistant) -> int | None: + """Get ZHA channel, or None if there's no ZHA config entry.""" + zha_network_settings: api.NetworkBackup | None + with contextlib.suppress(ValueError): + zha_network_settings = await api.async_get_network_settings(hass) + if not zha_network_settings: + return None + channel: int = zha_network_settings.network_info.channel + # ZHA uses channel 0 when no channel is set + return channel or None + + +async def async_change_channel( + hass: HomeAssistant, channel: int, delay: float = 0 +) -> asyncio.Task | None: + """Set the channel to be used. + + Does nothing if not configured. + """ + zha_url = _get_zha_url(hass) + if not zha_url: + # ZHA is not configured + return None + + async def finish_migration() -> None: + """Finish the channel migration.""" + await asyncio.sleep(max(0, delay - ZHA_CHANNEL_CHANGE_TIME_S)) + return await api.async_change_channel(hass, channel) + + return hass.async_create_task(finish_migration()) + + +async def async_get_channel(hass: HomeAssistant) -> int | None: + """Return the channel. + + Returns None if not configured. + """ + zha_url = _get_zha_url(hass) + if not zha_url: + # ZHA is not configured + return None + + return await _get_zha_channel(hass) + + +async def async_using_multipan(hass: HomeAssistant) -> bool: + """Return if the multiprotocol device is used. + + Returns False if not configured. + """ + zha_url = _get_zha_url(hass) + if not zha_url: + # ZHA is not configured + return False + + return is_multiprotocol_url(zha_url) diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index 4add48781a9..60c766c7204 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -1,7 +1,7 @@ """Test fixtures for the Home Assistant Hardware integration.""" from collections.abc import Generator from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -32,6 +32,17 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: yield +@pytest.fixture(autouse=True) +def mock_zha_get_last_network_settings() -> Generator[None, None, None]: + """Mock zha.api.async_get_last_network_settings.""" + + with patch( + "homeassistant.components.zha.api.async_get_last_network_settings", + AsyncMock(return_value=None), + ): + yield + + @pytest.fixture(name="addon_running") def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index a195899136d..83702adcc3a 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -11,12 +11,16 @@ from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.setup import ATTR_COMPONENT from tests.common import ( MockConfigEntry, MockModule, + MockPlatform, + flush_store, mock_config_flow, mock_integration, mock_platform, @@ -96,6 +100,54 @@ def config_flow_handler( yield +class MockMultiprotocolPlatform(MockPlatform): + """A mock multiprotocol platform.""" + + channel = 15 + using_multipan = True + + def __init__(self, **kwargs: Any) -> None: + """Initialize.""" + super().__init__(**kwargs) + self.change_channel_calls = [] + + async def async_change_channel( + self, hass: HomeAssistant, channel: int, delay: float + ) -> None: + """Set the channel to be used.""" + self.change_channel_calls.append((channel, delay)) + + async def async_get_channel(self, hass: HomeAssistant) -> int | None: + """Return the channel.""" + return self.channel + + async def async_using_multipan(self, hass: HomeAssistant) -> bool: + """Return if the multiprotocol device is used.""" + return self.using_multipan + + +@pytest.fixture +def mock_multiprotocol_platform( + hass: HomeAssistant, +) -> Generator[FakeConfigFlow, None, None]: + """Fixture for a test silabs multiprotocol platform.""" + hass.config.components.add(TEST_DOMAIN) + platform = MockMultiprotocolPlatform() + mock_platform(hass, f"{TEST_DOMAIN}.silabs_multiprotocol", platform) + return platform + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema: + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + async def test_option_flow_install_multi_pan_addon( hass: HomeAssistant, addon_store_info, @@ -215,7 +267,13 @@ async def test_option_flow_install_multi_pan_addon_zha( assert result["step_id"] == "configure_addon" install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - result = await hass.config_entries.options.async_configure(result["flow_id"]) + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + assert multipan_manager._channel is None + with patch( + "homeassistant.components.zha.silabs_multiprotocol.async_get_channel", + return_value=11, + ): + result = await hass.config_entries.options.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( @@ -230,6 +288,8 @@ async def test_option_flow_install_multi_pan_addon_zha( } }, ) + # Check the channel is initialized from ZHA + assert multipan_manager._channel == 11 # Check the ZHA config entry data is updated assert zha_config_entry.data == { "device": { @@ -393,7 +453,64 @@ async def test_option_flow_addon_installed_other_device( assert result["type"] == FlowResultType.CREATE_ENTRY -async def test_option_flow_addon_installed_same_device( +@pytest.mark.parametrize( + ("configured_channel", "suggested_channel"), [(None, "15"), (11, "11")] +) +async def test_option_flow_addon_installed_same_device_reconfigure( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + mock_multiprotocol_platform: MockMultiprotocolPlatform, + configured_channel: int | None, + suggested_channel: int, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager._channel = configured_channel + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "reconfigure_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reconfigure_addon" + assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"channel": "14"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "notify_channel_change" + assert result["description_placeholders"] == {"delay_minutes": "5"} + + result = await hass.config_entries.options.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + assert mock_multiprotocol_platform.change_channel_calls == [(14, 300)] + + +async def test_option_flow_addon_installed_same_device_uninstall( hass: HomeAssistant, addon_info, addon_store_info, @@ -417,8 +534,15 @@ async def test_option_flow_addon_installed_same_device( side_effect=Mock(return_value=True), ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "show_revert_guide" + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "show_revert_guide" result = await hass.config_entries.options.async_configure(result["flow_id"], {}) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -806,3 +930,80 @@ def test_is_multiprotocol_url() -> None: "http://core-silabs-multiprotocol:8081" ) assert not silabs_multiprotocol_addon.is_multiprotocol_url("/dev/ttyAMA1") + + +@pytest.mark.parametrize( + ( + "initial_multipan_channel", + "platform_using_multipan", + "platform_channel", + "new_multipan_channel", + ), + [ + (None, True, 15, 15), + (None, False, 15, None), + (11, True, 15, 11), + (None, True, None, None), + ], +) +async def test_import_channel( + hass: HomeAssistant, + initial_multipan_channel: int | None, + platform_using_multipan: bool, + platform_channel: int | None, + new_multipan_channel: int | None, +) -> None: + """Test channel is initialized from first platform.""" + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager._channel = initial_multipan_channel + + mock_multiprotocol_platform = MockMultiprotocolPlatform() + mock_multiprotocol_platform.channel = platform_channel + mock_multiprotocol_platform.using_multipan = platform_using_multipan + + hass.config.components.add(TEST_DOMAIN) + mock_platform( + hass, f"{TEST_DOMAIN}.silabs_multiprotocol", mock_multiprotocol_platform + ) + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: TEST_DOMAIN}) + await hass.async_block_till_done() + + assert multipan_manager.async_get_channel() == new_multipan_channel + + +@pytest.mark.parametrize( + ( + "platform_using_multipan", + "expected_calls", + ), + [ + (True, [(15, 10)]), + (False, []), + ], +) +async def test_change_channel( + hass: HomeAssistant, + mock_multiprotocol_platform: MockMultiprotocolPlatform, + platform_using_multipan: bool, + expected_calls: list[int], +) -> None: + """Test channel is initialized from first platform.""" + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + mock_multiprotocol_platform.using_multipan = platform_using_multipan + + await multipan_manager.async_change_channel(15, 10) + assert mock_multiprotocol_platform.change_channel_calls == expected_calls + + +async def test_load_preferences(hass: HomeAssistant) -> None: + """Make sure that we can load/save data correctly.""" + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + assert multipan_manager._channel != 11 + multipan_manager.async_set_channel(11) + + await flush_store(multipan_manager._store) + + multipan_manager2 = silabs_multiprotocol_addon.MultiprotocolAddonManager(hass) + await multipan_manager2.async_setup() + + assert multipan_manager._channel == multipan_manager2._channel diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 7fcc1f86880..3677b4ea8f1 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,6 +1,6 @@ """Test fixtures for the Home Assistant SkyConnect integration.""" from collections.abc import Generator -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -34,6 +34,17 @@ def mock_zha(): yield +@pytest.fixture(autouse=True) +def mock_zha_get_last_network_settings() -> Generator[None, None, None]: + """Mock zha.api.async_get_last_network_settings.""" + + with patch( + "homeassistant.components.zha.api.async_get_last_network_settings", + AsyncMock(return_value=None), + ): + yield + + @pytest.fixture(name="addon_running") def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index bc48c6b01fd..e4a666f9f04 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -1,7 +1,7 @@ """Test fixtures for the Home Assistant Yellow integration.""" from collections.abc import Generator from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -32,6 +32,17 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: yield +@pytest.fixture(autouse=True) +def mock_zha_get_last_network_settings() -> Generator[None, None, None]: + """Mock zha.api.async_get_last_network_settings.""" + + with patch( + "homeassistant.components.zha.api.async_get_last_network_settings", + AsyncMock(return_value=None), + ): + yield + + @pytest.fixture(name="addon_running") def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index f0b3ca0a18d..bb3b474519e 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -1,9 +1,10 @@ """Test fixtures for the Open Thread Border Router integration.""" -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from homeassistant.components import otbr +from homeassistant.core import HomeAssistant from . import CONFIG_ENTRY_DATA, DATASET_CH16 @@ -31,3 +32,12 @@ async def otbr_config_entry_fixture(hass): @pytest.fixture(autouse=True) def use_mocked_zeroconf(mock_async_zeroconf): """Mock zeroconf in all tests.""" + + +@pytest.fixture(name="multiprotocol_addon_manager_mock") +def multiprotocol_addon_manager_mock_fixture(hass: HomeAssistant): + """Mock the Silicon Labs Multiprotocol add-on manager.""" + mock_manager = Mock() + mock_manager.async_get_channel = Mock(return_value=None) + with patch.dict(hass.data, {"silabs_multiprotocol_addon_manager": mock_manager}): + yield mock_manager diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index faec90282df..cfb47a28bcf 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from http import HTTPStatus from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import patch import aiohttp import pytest @@ -309,7 +309,9 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred( async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + multiprotocol_addon_manager_mock, ) -> None: """Test the hassio discovery flow when the border router has no dataset. @@ -321,8 +323,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) - networksettings = Mock() - networksettings.network_info.channel = 15 + multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 with patch( "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", @@ -330,13 +331,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( ), patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=networksettings, - ): + ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 3d646287ce1..990c015244f 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -1,7 +1,7 @@ """Test the Open Thread Border Router integration.""" import asyncio from http import HTTPStatus -from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, patch import aiohttp import pytest @@ -59,7 +59,9 @@ async def test_import_dataset(hass: HomeAssistant) -> None: ) -async def test_import_share_radio_channel_collision(hass: HomeAssistant) -> None: +async def test_import_share_radio_channel_collision( + hass: HomeAssistant, multiprotocol_addon_manager_mock +) -> None: """Test the active dataset is imported at setup. This imports a dataset with different channel than ZHA when ZHA and OTBR share @@ -67,8 +69,7 @@ async def test_import_share_radio_channel_collision(hass: HomeAssistant) -> None """ issue_registry = ir.async_get(hass) - networksettings = Mock() - networksettings.network_info.channel = 15 + multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -81,13 +82,7 @@ async def test_import_share_radio_channel_collision(hass: HomeAssistant) -> None "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 ), patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add, patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=networksettings, - ): + ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex()) @@ -99,7 +94,7 @@ async def test_import_share_radio_channel_collision(hass: HomeAssistant) -> None @pytest.mark.parametrize("dataset", [DATASET_CH15, DATASET_NO_CHANNEL]) async def test_import_share_radio_no_channel_collision( - hass: HomeAssistant, dataset: bytes + hass: HomeAssistant, multiprotocol_addon_manager_mock, dataset: bytes ) -> None: """Test the active dataset is imported at setup. @@ -107,8 +102,7 @@ async def test_import_share_radio_no_channel_collision( """ issue_registry = ir.async_get(hass) - networksettings = Mock() - networksettings.network_info.channel = 15 + multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -121,13 +115,7 @@ async def test_import_share_radio_no_channel_collision( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset ), patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add, patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=networksettings, - ): + ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex()) diff --git a/tests/components/otbr/test_silabs_multiprotocol.py b/tests/components/otbr/test_silabs_multiprotocol.py new file mode 100644 index 00000000000..8dd07db6f22 --- /dev/null +++ b/tests/components/otbr/test_silabs_multiprotocol.py @@ -0,0 +1,175 @@ +"""Test OTBR Silicon Labs Multiprotocol support.""" +from unittest.mock import patch + +import pytest +from python_otbr_api import ActiveDataSet, tlv_parser + +from homeassistant.components import otbr +from homeassistant.components.otbr import ( + silabs_multiprotocol as otbr_silabs_multiprotocol, +) +from homeassistant.components.thread import dataset_store +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import DATASET_CH16 + +OTBR_MULTIPAN_URL = "http://core-silabs-multiprotocol:8081" +OTBR_NON_MULTIPAN_URL = "/dev/ttyAMA1" +DATASET_CH16_PENDING = ( + "0E080000000000020000" # ACTIVETIMESTAMP + "340400006699" # DELAYTIMER + "000300000F" # CHANNEL + "35060004001FFFE0" # CHANNELMASK + "0208F642646DA209B1C0" # EXTPANID + "0708FDF57B5A0FE2AAF6" # MESHLOCALPREFIX + "0510DE98B5BA1A528FEE049D4B4B01835375" # NETWORKKEY + "030D4F70656E546872656164204841" # NETWORKNAME + "010225A4" # PANID + "0410F5DD18371BFD29E1A601EF6FFAD94C03" # PSKC + "0C0402A0F7F8" # SECURITYPOLICY +) + + +async def test_async_change_channel(hass: HomeAssistant, otbr_config_entry) -> None: + """Test test_async_change_channel.""" + + store = await dataset_store.async_get_store(hass) + assert len(store.datasets) == 1 + assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() + + with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, patch( + "python_otbr_api.OTBR.get_pending_dataset_tlvs", + return_value=bytes.fromhex(DATASET_CH16_PENDING), + ): + await otbr_silabs_multiprotocol.async_change_channel(hass, 15, delay=5 * 300) + mock_set_channel.assert_awaited_once_with(15, delay=5 * 300 * 1000) + + pending_dataset = tlv_parser.parse_tlv(DATASET_CH16_PENDING) + pending_dataset.pop(tlv_parser.MeshcopTLVType.DELAYTIMER) + + assert len(store.datasets) == 1 + assert list(store.datasets.values())[0].tlv == tlv_parser.encode_tlv( + pending_dataset + ) + + +async def test_async_change_channel_no_pending( + hass: HomeAssistant, otbr_config_entry +) -> None: + """Test test_async_change_channel when the pending dataset already expired.""" + + store = await dataset_store.async_get_store(hass) + assert len(store.datasets) == 1 + assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() + + with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", + return_value=bytes.fromhex(DATASET_CH16_PENDING), + ), patch( + "python_otbr_api.OTBR.get_pending_dataset_tlvs", + return_value=None, + ): + await otbr_silabs_multiprotocol.async_change_channel(hass, 15, delay=5 * 300) + mock_set_channel.assert_awaited_once_with(15, delay=5 * 300 * 1000) + + pending_dataset = tlv_parser.parse_tlv(DATASET_CH16_PENDING) + pending_dataset.pop(tlv_parser.MeshcopTLVType.DELAYTIMER) + + assert len(store.datasets) == 1 + assert list(store.datasets.values())[0].tlv == tlv_parser.encode_tlv( + pending_dataset + ) + + +async def test_async_change_channel_no_update( + hass: HomeAssistant, otbr_config_entry +) -> None: + """Test test_async_change_channel when we didn't get a dataset from the OTBR.""" + + store = await dataset_store.async_get_store(hass) + assert len(store.datasets) == 1 + assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() + + with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", + return_value=None, + ), patch( + "python_otbr_api.OTBR.get_pending_dataset_tlvs", + return_value=None, + ): + await otbr_silabs_multiprotocol.async_change_channel(hass, 15, delay=5 * 300) + mock_set_channel.assert_awaited_once_with(15, delay=5 * 300 * 1000) + + assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() + + +async def test_async_change_channel_no_otbr(hass: HomeAssistant) -> None: + """Test async_change_channel when otbr is not configured.""" + + with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel: + await otbr_silabs_multiprotocol.async_change_channel(hass, 16, delay=0) + mock_set_channel.assert_not_awaited() + + +async def test_async_get_channel(hass: HomeAssistant, otbr_config_entry) -> None: + """Test test_async_get_channel.""" + + with patch( + "python_otbr_api.OTBR.get_active_dataset", + return_value=ActiveDataSet(channel=11), + ) as mock_get_active_dataset: + assert await otbr_silabs_multiprotocol.async_get_channel(hass) == 11 + mock_get_active_dataset.assert_awaited_once_with() + + +async def test_async_get_channel_no_dataset( + hass: HomeAssistant, otbr_config_entry +) -> None: + """Test test_async_get_channel.""" + + with patch( + "python_otbr_api.OTBR.get_active_dataset", + return_value=None, + ) as mock_get_active_dataset: + assert await otbr_silabs_multiprotocol.async_get_channel(hass) is None + mock_get_active_dataset.assert_awaited_once_with() + + +async def test_async_get_channel_error(hass: HomeAssistant, otbr_config_entry) -> None: + """Test test_async_get_channel.""" + + with patch( + "python_otbr_api.OTBR.get_active_dataset", + side_effect=HomeAssistantError, + ) as mock_get_active_dataset: + assert await otbr_silabs_multiprotocol.async_get_channel(hass) is None + mock_get_active_dataset.assert_awaited_once_with() + + +async def test_async_get_channel_no_otbr(hass: HomeAssistant) -> None: + """Test test_async_get_channel when otbr is not configured.""" + + with patch("python_otbr_api.OTBR.get_active_dataset") as mock_get_active_dataset: + await otbr_silabs_multiprotocol.async_get_channel(hass) + mock_get_active_dataset.assert_not_awaited() + + +@pytest.mark.parametrize( + ("url", "expected"), + [(OTBR_MULTIPAN_URL, True), (OTBR_NON_MULTIPAN_URL, False)], +) +async def test_async_using_multipan( + hass: HomeAssistant, otbr_config_entry, url: str, expected: bool +) -> None: + """Test async_change_channel when otbr is not configured.""" + data: otbr.OTBRData = hass.data[otbr.DOMAIN] + data.url = url + + assert await otbr_silabs_multiprotocol.async_using_multipan(hass) is expected + + +async def test_async_using_multipan_no_otbr(hass: HomeAssistant) -> None: + """Test async_change_channel when otbr is not configured.""" + + assert await otbr_silabs_multiprotocol.async_using_multipan(hass) is False diff --git a/tests/components/otbr/test_util.py b/tests/components/otbr/test_util.py index af5306b3581..f8ed79b91ee 100644 --- a/tests/components/otbr/test_util.py +++ b/tests/components/otbr/test_util.py @@ -1,5 +1,4 @@ """Test OTBR Utility functions.""" -from unittest.mock import Mock, patch from homeassistant.components import otbr from homeassistant.core import HomeAssistant @@ -8,51 +7,19 @@ OTBR_MULTIPAN_URL = "http://core-silabs-multiprotocol:8081" OTBR_NON_MULTIPAN_URL = "/dev/ttyAMA1" -async def test_get_allowed_channel(hass: HomeAssistant) -> None: +async def test_get_allowed_channel( + hass: HomeAssistant, multiprotocol_addon_manager_mock +) -> None: """Test get_allowed_channel.""" - zha_networksettings = Mock() - zha_networksettings.network_info.channel = 15 - - # OTBR multipan + No ZHA -> no restriction + # OTBR multipan + No configured channel -> no restriction + multiprotocol_addon_manager_mock.async_get_channel.return_value = None assert await otbr.util.get_allowed_channel(hass, OTBR_MULTIPAN_URL) is None - # OTBR multipan + ZHA multipan empty settings -> no restriction - with patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=None, - ): - assert await otbr.util.get_allowed_channel(hass, OTBR_MULTIPAN_URL) is None + # OTBR multipan + multipan using channel 15 -> 15 + multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 + assert await otbr.util.get_allowed_channel(hass, OTBR_MULTIPAN_URL) == 15 - # OTBR multipan + ZHA not multipan using channel 15 -> no restriction - with patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="/dev/ttyAMA1", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=zha_networksettings, - ): - assert await otbr.util.get_allowed_channel(hass, OTBR_MULTIPAN_URL) is None - - # OTBR multipan + ZHA multipan using channel 15 -> 15 - with patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=zha_networksettings, - ): - assert await otbr.util.get_allowed_channel(hass, OTBR_MULTIPAN_URL) == 15 - - # OTBR not multipan + ZHA multipan using channel 15 -> no restriction - with patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=zha_networksettings, - ): - assert await otbr.util.get_allowed_channel(hass, OTBR_NON_MULTIPAN_URL) is None + # OTBR no multipan + multipan using channel 15 -> no restriction + multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 + assert await otbr.util.get_allowed_channel(hass, OTBR_NON_MULTIPAN_URL) is None diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index bfc3f09d6fe..1feebe9c02c 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -1,5 +1,5 @@ """Test OTBR Websocket API.""" -from unittest.mock import Mock, patch +from unittest.mock import patch import pytest import python_otbr_api @@ -273,6 +273,7 @@ async def test_set_network_no_entry( async def test_set_network_channel_conflict( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, + multiprotocol_addon_manager_mock, otbr_config_entry, websocket_client, ) -> None: @@ -281,24 +282,16 @@ async def test_set_network_channel_conflict( dataset_store = await thread.dataset_store.async_get_store(hass) dataset_id = list(dataset_store.datasets)[0] - networksettings = Mock() - networksettings.network_info.channel = 15 + multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 - with patch( - "homeassistant.components.otbr.util.zha_api.async_get_radio_path", - return_value="socket://core-silabs-multiprotocol:9999", - ), patch( - "homeassistant.components.otbr.util.zha_api.async_get_network_settings", - return_value=networksettings, - ): - await websocket_client.send_json_auto_id( - { - "type": "otbr/set_network", - "dataset_id": dataset_id, - } - ) + await websocket_client.send_json_auto_id( + { + "type": "otbr/set_network", + "dataset_id": dataset_id, + } + ) - msg = await websocket_client.receive_json() + msg = await websocket_client.receive_json() assert not msg["success"] assert msg["error"]["code"] == "channel_conflict" diff --git a/tests/components/zha/test_silabs_multiprotocol.py b/tests/components/zha/test_silabs_multiprotocol.py new file mode 100644 index 00000000000..beae0230901 --- /dev/null +++ b/tests/components/zha/test_silabs_multiprotocol.py @@ -0,0 +1,118 @@ +"""Test ZHA Silicon Labs Multiprotocol support.""" +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import call, patch + +import pytest +import zigpy.backups +import zigpy.state + +from homeassistant.components import zha +from homeassistant.components.zha import api, silabs_multiprotocol +from homeassistant.core import HomeAssistant + +if TYPE_CHECKING: + from zigpy.application import ControllerApplication + + +@pytest.fixture(autouse=True) +def required_platform_only(): + """Only set up the required and required base platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", ()): + yield + + +async def test_async_get_channel_active(hass: HomeAssistant, setup_zha) -> None: + """Test reading channel with an active ZHA installation.""" + await setup_zha() + + assert await silabs_multiprotocol.async_get_channel(hass) == 15 + + +async def test_async_get_channel_missing( + hass: HomeAssistant, setup_zha, zigpy_app_controller: ControllerApplication +) -> None: + """Test reading channel with an inactive ZHA installation, no valid channel.""" + await setup_zha() + + gateway = api._get_gateway(hass) + await zha.async_unload_entry(hass, gateway.config_entry) + + # Network settings were never loaded for whatever reason + zigpy_app_controller.state.network_info = zigpy.state.NetworkInfo() + zigpy_app_controller.state.node_info = zigpy.state.NodeInfo() + + with patch( + "bellows.zigbee.application.ControllerApplication.__new__", + return_value=zigpy_app_controller, + ): + assert await silabs_multiprotocol.async_get_channel(hass) is None + + +async def test_async_get_channel_no_zha(hass: HomeAssistant) -> None: + """Test reading channel with no ZHA config entries and no database.""" + assert await silabs_multiprotocol.async_get_channel(hass) is None + + +async def test_async_using_multipan_active(hass: HomeAssistant, setup_zha) -> None: + """Test async_using_multipan with an active ZHA installation.""" + await setup_zha() + + assert await silabs_multiprotocol.async_using_multipan(hass) is False + + +async def test_async_using_multipan_no_zha(hass: HomeAssistant) -> None: + """Test async_using_multipan with no ZHA config entries and no database.""" + assert await silabs_multiprotocol.async_using_multipan(hass) is False + + +async def test_change_channel( + hass: HomeAssistant, setup_zha, zigpy_app_controller: ControllerApplication +) -> None: + """Test changing the channel.""" + await setup_zha() + + with patch.object( + zigpy_app_controller, "move_network_to_channel", autospec=True + ) as mock_move_network_to_channel: + task = await silabs_multiprotocol.async_change_channel(hass, 20) + await task + + assert mock_move_network_to_channel.mock_calls == [call(20)] + + +async def test_change_channel_no_zha( + hass: HomeAssistant, zigpy_app_controller: ControllerApplication +) -> None: + """Test changing the channel with no ZHA config entries and no database.""" + with patch.object( + zigpy_app_controller, "move_network_to_channel", autospec=True + ) as mock_move_network_to_channel: + task = await silabs_multiprotocol.async_change_channel(hass, 20) + assert task is None + + assert mock_move_network_to_channel.mock_calls == [] + + +@pytest.mark.parametrize(("delay", "sleep"), [(0, 0), (5, 0), (15, 15 - 10.27)]) +async def test_change_channel_delay( + hass: HomeAssistant, + setup_zha, + zigpy_app_controller: ControllerApplication, + delay: float, + sleep: float, +) -> None: + """Test changing the channel with a delay.""" + await setup_zha() + + with patch.object( + zigpy_app_controller, "move_network_to_channel", autospec=True + ) as mock_move_network_to_channel, patch( + "homeassistant.components.zha.silabs_multiprotocol.asyncio.sleep", autospec=True + ) as mock_sleep: + task = await silabs_multiprotocol.async_change_channel(hass, 20, delay=delay) + await task + + assert mock_move_network_to_channel.mock_calls == [call(20)] + assert mock_sleep.mock_calls == [call(sleep)] From 44274e5323e34de674fba6c0eb3c6e8323acbeb4 Mon Sep 17 00:00:00 2001 From: Sebastian Heiden Date: Thu, 1 Jun 2023 18:04:00 +0200 Subject: [PATCH 09/66] Fix LaMetric Config Flow for SKY (#93483) Co-authored-by: Franck Nijhof --- .../components/lametric/config_flow.py | 6 +- tests/components/lametric/conftest.py | 12 +++- .../lametric/fixtures/device_sa5.json | 71 +++++++++++++++++++ tests/components/lametric/test_config_flow.py | 52 ++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 tests/components/lametric/fixtures/device_sa5.json diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 8e9da5851cf..1dad190d706 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -248,6 +248,10 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key} ) + notify_sound: Sound | None = None + if device.model != "sa5": + notify_sound = Sound(sound=NotificationSound.WIN) + await lametric.notify( notification=Notification( priority=NotificationPriority.CRITICAL, @@ -255,7 +259,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): model=Model( cycles=2, frames=[Simple(text="Connected to Home Assistant!", icon=7956)], - sound=Sound(sound=NotificationSound.WIN), + sound=notify_sound, ), ) ) diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index 177204b6f24..b3a9f2d8665 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -67,8 +67,14 @@ def mock_lametric_cloud() -> Generator[MagicMock, None, None]: @pytest.fixture -def mock_lametric() -> Generator[MagicMock, None, None]: - """Return a mocked LaMetric client.""" +def device_fixture() -> str: + """Return the device fixture for a specific device.""" + return "device" + + +@pytest.fixture +def mock_lametric(request, device_fixture: str) -> Generator[MagicMock, None, None]: + """Return a mocked LaMetric TIME client.""" with patch( "homeassistant.components.lametric.coordinator.LaMetricDevice", autospec=True ) as lametric_mock, patch( @@ -79,7 +85,7 @@ def mock_lametric() -> Generator[MagicMock, None, None]: lametric.api_key = "mock-api-key" lametric.host = "127.0.0.1" lametric.device.return_value = Device.parse_raw( - load_fixture("device.json", DOMAIN) + load_fixture(f"{device_fixture}.json", DOMAIN) ) yield lametric diff --git a/tests/components/lametric/fixtures/device_sa5.json b/tests/components/lametric/fixtures/device_sa5.json new file mode 100644 index 00000000000..47120f672ef --- /dev/null +++ b/tests/components/lametric/fixtures/device_sa5.json @@ -0,0 +1,71 @@ +{ + "audio": { + "volume": 100, + "volume_limit": { + "max": 100, + "min": 0 + }, + "volume_range": { + "max": 100, + "min": 0 + } + }, + "bluetooth": { + "active": true, + "address": "AA:BB:CC:DD:EE:FF", + "available": true, + "discoverable": true, + "low_energy": { + "active": true, + "advertising": true, + "connectable": true + }, + "name": "SKY0123", + "pairable": false + }, + "display": { + "brightness": 66, + "brightness_limit": { + "max": 100, + "min": 2 + }, + "brightness_mode": "manual", + "brightness_range": { + "max": 100, + "min": 0 + }, + "height": 8, + "on": true, + "screensaver": { + "enabled": true, + "modes": { + "screen_off": { + "enabled": false + }, + "time_based": { + "enabled": false + } + }, + "widget": "" + }, + "type": "mixed", + "width": 64 + }, + "id": "12345", + "mode": "manual", + "model": "sa5", + "name": "spyfly's LaMetric SKY", + "os_version": "3.0.13", + "serial_number": "SA52100000123TBNC", + "wifi": { + "active": true, + "mac": "AA:BB:CC:DD:EE:FF", + "available": true, + "encryption": "WPA", + "ssid": "IoT", + "ip": "127.0.0.1", + "mode": "dhcp", + "netmask": "255.255.255.0", + "strength": 58 + } +} diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index 8fd0ef061ac..0fa3a2d9838 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -6,6 +6,9 @@ from demetriek import ( LaMetricConnectionError, LaMetricConnectionTimeoutError, LaMetricError, + Notification, + NotificationSound, + Sound, ) import pytest @@ -238,6 +241,10 @@ async def test_full_manual( assert len(mock_lametric.device.mock_calls) == 1 assert len(mock_lametric.notify.mock_calls) == 1 + + notification: Notification = mock_lametric.notify.mock_calls[0][2]["notification"] + assert notification.model.sound == Sound(sound=NotificationSound.WIN) + assert len(mock_setup_entry.mock_calls) == 1 @@ -894,3 +901,48 @@ async def test_reauth_manual( assert len(mock_lametric.device.mock_calls) == 1 assert len(mock_lametric.notify.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_setup_entry") +@pytest.mark.parametrize("device_fixture", ["device_sa5"]) +async def test_reauth_manual_sky( + hass: HomeAssistant, + mock_lametric: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with manual entry for LaMetric Sky.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_API_KEY: "mock-api-key"} + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + assert len(mock_lametric.device.mock_calls) == 1 + assert len(mock_lametric.notify.mock_calls) == 1 + + notification: Notification = mock_lametric.notify.mock_calls[0][2]["notification"] + assert notification.model.sound is None From 7f6c0fdd4cb184a988413aebf006aed352181498 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 1 Jun 2023 16:18:49 +0200 Subject: [PATCH 10/66] Raise exception instead of hide in logs on zha write (#93571) Raise exception instead of hide in logs Write request that failed parsing of data would fail, yet display as successful in the gui. --- homeassistant/components/zha/core/device.py | 28 +++++++++++-------- homeassistant/components/zha/websocket_api.py | 3 ++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 139acb23923..311e876bbc0 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -740,9 +740,15 @@ class ZHADevice(LogMixin): manufacturer=None, ): """Write a value to a zigbee attribute for a cluster in this entity.""" - cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) - if cluster is None: - return None + try: + cluster: Cluster = self.async_get_cluster( + endpoint_id, cluster_id, cluster_type + ) + except KeyError as exc: + raise ValueError( + f"Cluster {cluster_id} not found on endpoint {endpoint_id} while" + f" writing attribute {attribute} with value {value}" + ) from exc try: response = await cluster.write_attributes( @@ -758,15 +764,13 @@ class ZHADevice(LogMixin): ) return response except zigpy.exceptions.ZigbeeException as exc: - self.debug( - "failed to set attribute: %s %s %s %s %s", - f"{ATTR_VALUE}: {value}", - f"{ATTR_ATTRIBUTE}: {attribute}", - f"{ATTR_CLUSTER_ID}: {cluster_id}", - f"{ATTR_ENDPOINT_ID}: {endpoint_id}", - exc, - ) - return None + raise HomeAssistantError( + f"Failed to set attribute: " + f"{ATTR_VALUE}: {value} " + f"{ATTR_ATTRIBUTE}: {attribute} " + f"{ATTR_CLUSTER_ID}: {cluster_id} " + f"{ATTR_ENDPOINT_ID}: {endpoint_id}" + ) from exc async def issue_cluster_command( self, diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 2d4126861b4..019a5c50238 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -1302,6 +1302,9 @@ def async_load_api(hass: HomeAssistant) -> None: cluster_type=cluster_type, manufacturer=manufacturer, ) + else: + raise ValueError(f"Device with IEEE {str(ieee)} not found") + _LOGGER.debug( ( "Set attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s:" From 50bd9e9fddd9db4dcd312df466496ba8abb7d578 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Jun 2023 12:31:17 -0500 Subject: [PATCH 11/66] Make RestoreStateData.async_get_instance backwards compatible (#93924) --- homeassistant/helpers/restore_state.py | 44 ++++++--- tests/helpers/test_restore_state.py | 122 ++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index e34e3c86324..ab3b93cf3c4 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -16,6 +16,7 @@ import homeassistant.util.dt as dt_util from . import start from .entity import Entity from .event import async_track_time_interval +from .frame import report from .json import JSONEncoder from .storage import Store @@ -96,7 +97,9 @@ class StoredState: async def async_load(hass: HomeAssistant) -> None: """Load the restore state task.""" - hass.data[DATA_RESTORE_STATE] = await RestoreStateData.async_get_instance(hass) + restore_state = RestoreStateData(hass) + await restore_state.async_setup() + hass.data[DATA_RESTORE_STATE] = restore_state @callback @@ -108,25 +111,26 @@ def async_get(hass: HomeAssistant) -> RestoreStateData: class RestoreStateData: """Helper class for managing the helper saved data.""" - @staticmethod - async def async_get_instance(hass: HomeAssistant) -> RestoreStateData: - """Get the instance of this data helper.""" - data = RestoreStateData(hass) - await data.async_load() - - async def hass_start(hass: HomeAssistant) -> None: - """Start the restore state task.""" - data.async_setup_dump() - - start.async_at_start(hass, hass_start) - - return data - @classmethod async def async_save_persistent_states(cls, hass: HomeAssistant) -> None: """Dump states now.""" await async_get(hass).async_dump_states() + @classmethod + async def async_get_instance(cls, hass: HomeAssistant) -> RestoreStateData: + """Return the instance of this class.""" + # Nothing should actually be calling this anymore, but we'll keep it + # around for a while to avoid breaking custom components. + # + # In fact they should not be accessing this at all. + report( + "restore_state.RestoreStateData.async_get_instance is deprecated, " + "and not intended to be called by custom components; Please" + "refactor your code to use RestoreEntity instead;" + " restore_state.async_get(hass) can be used in the meantime", + ) + return async_get(hass) + def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" self.hass: HomeAssistant = hass @@ -136,6 +140,16 @@ class RestoreStateData: self.last_states: dict[str, StoredState] = {} self.entities: dict[str, RestoreEntity] = {} + async def async_setup(self) -> None: + """Set up up the instance of this data helper.""" + await self.async_load() + + async def hass_start(hass: HomeAssistant) -> None: + """Start the restore state task.""" + self.async_setup_dump() + + start.async_at_start(self.hass, hass_start) + async def async_load(self) -> None: """Load the instance of this data helper.""" try: diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index cf6a078d137..b5ce7afade0 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,12 +1,19 @@ """The tests for the Restore component.""" +from collections.abc import Coroutine from datetime import datetime, timedelta +import logging from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch + +import pytest from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.reload import async_get_platform_without_config_entry from homeassistant.helpers.restore_state import ( DATA_RESTORE_STATE, STORAGE_KEY, @@ -16,9 +23,20 @@ from homeassistant.helpers.restore_state import ( async_get, async_load, ) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import ( + MockModule, + MockPlatform, + async_fire_time_changed, + mock_entity_platform, + mock_integration, +) + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "test_domain" +PLATFORM = "test_platform" async def test_caching_data(hass: HomeAssistant) -> None: @@ -68,6 +86,20 @@ async def test_caching_data(hass: HomeAssistant) -> None: assert mock_write_data.called +async def test_async_get_instance_backwards_compatibility(hass: HomeAssistant) -> None: + """Test async_get_instance backwards compatibility.""" + await async_load(hass) + data = async_get(hass) + # When called from core it should raise + with pytest.raises(RuntimeError): + await RestoreStateData.async_get_instance(hass) + + # When called from a component it should not raise + # but it should report + with patch("homeassistant.helpers.restore_state.report"): + assert data is await RestoreStateData.async_get_instance(hass) + + async def test_periodic_write(hass: HomeAssistant) -> None: """Test that we write periodiclly but not after stop.""" data = async_get(hass) @@ -401,3 +433,89 @@ async def test_restoring_invalid_entity_id( state = await entity.async_get_last_state() assert state is None + + +async def test_restore_entity_end_to_end( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test restoring an entity end-to-end.""" + component_setup = Mock(return_value=True) + + setup_called = [] + + entity_id = "test_domain.unnamed_device" + data = async_get(hass) + now = dt_util.utcnow() + data.last_states = { + entity_id: StoredState(State(entity_id, "stored"), None, now), + } + + class MockRestoreEntity(RestoreEntity): + """Mock restore entity.""" + + def __init__(self): + """Initialize the mock entity.""" + self._state: str | None = None + + @property + def state(self): + """Return the state.""" + return self._state + + async def async_added_to_hass(self) -> Coroutine[Any, Any, None]: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._state = (await self.async_get_last_state()).state + + async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up the test platform.""" + async_add_entities([MockRestoreEntity()]) + setup_called.append(True) + + mock_integration(hass, MockModule(DOMAIN, setup=component_setup)) + mock_integration(hass, MockModule(PLATFORM, dependencies=[DOMAIN])) + + mock_platform = MockPlatform(async_setup_platform=async_setup_platform) + mock_entity_platform(hass, f"{DOMAIN}.{PLATFORM}", mock_platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": PLATFORM, "sensors": None}}) + await hass.async_block_till_done() + assert component_setup.called + + assert f"{DOMAIN}.{PLATFORM}" in hass.config.components + assert len(setup_called) == 1 + + platform = async_get_platform_without_config_entry(hass, PLATFORM, DOMAIN) + assert platform.platform_name == PLATFORM + assert platform.domain == DOMAIN + assert hass.states.get(entity_id).state == "stored" + + await data.async_dump_states() + await hass.async_block_till_done() + + storage_data = hass_storage[STORAGE_KEY]["data"] + assert len(storage_data) == 1 + assert storage_data[0]["state"]["entity_id"] == entity_id + assert storage_data[0]["state"]["state"] == "stored" + + await platform.async_reset() + + assert hass.states.get(entity_id) is None + + # Make sure the entity still gets saved to restore state + # even though the platform has been reset since it should + # not be expired yet. + await data.async_dump_states() + await hass.async_block_till_done() + + storage_data = hass_storage[STORAGE_KEY]["data"] + assert len(storage_data) == 1 + assert storage_data[0]["state"]["entity_id"] == entity_id + assert storage_data[0]["state"]["state"] == "stored" From 358700af4e6b58000646ce618a85a7792920b320 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 Jun 2023 19:23:42 +0200 Subject: [PATCH 12/66] Update frontend to 20230601.1 (#93927) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index bde1977b1c1..838294f7ba5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230601.0"] + "requirements": ["home-assistant-frontend==20230601.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c8c085b761c..17efae77498 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.1.0 hass-nabucasa==0.67.1 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230601.0 +home-assistant-frontend==20230601.1 home-assistant-intents==2023.5.30 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 89bf881e716..1eadb01faa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230601.0 +home-assistant-frontend==20230601.1 # homeassistant.components.conversation home-assistant-intents==2023.5.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ca7cb47c34..7ea12c24bd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -716,7 +716,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230601.0 +home-assistant-frontend==20230601.1 # homeassistant.components.conversation home-assistant-intents==2023.5.30 From 5ac3bb9e9bf0f992459ea205700c9f83e858389c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Jun 2023 12:14:03 -0500 Subject: [PATCH 13/66] Fix onvif cameras that use basic auth with no password (#93928) --- homeassistant/components/onvif/__init__.py | 37 +++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index a834a8f2df6..ea6cd542fea 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,10 +1,11 @@ """The ONVIF integration.""" import asyncio +from contextlib import suppress from http import HTTPStatus import logging from httpx import RequestError -from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError +from onvif.exceptions import ONVIFError from onvif.util import is_auth_error, stringify_onvif_error from zeep.exceptions import Fault, TransportError @@ -120,31 +121,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, device.platforms) -async def _get_snapshot_auth(device): +async def _get_snapshot_auth(device: ONVIFDevice) -> str | None: """Determine auth type for snapshots.""" - if not device.capabilities.snapshot or not (device.username and device.password): - return HTTP_DIGEST_AUTHENTICATION + if not device.capabilities.snapshot: + return None - try: - snapshot = await device.device.get_snapshot(device.profiles[0].token) + for basic_auth in (False, True): + method = HTTP_BASIC_AUTHENTICATION if basic_auth else HTTP_DIGEST_AUTHENTICATION + with suppress(ONVIFError): + if await device.device.get_snapshot(device.profiles[0].token, basic_auth): + return method - if snapshot: - return HTTP_DIGEST_AUTHENTICATION - return HTTP_BASIC_AUTHENTICATION - except (ONVIFAuthError, ONVIFTimeoutError): - return HTTP_BASIC_AUTHENTICATION - except ONVIFError: - return HTTP_DIGEST_AUTHENTICATION + return None -async def async_populate_snapshot_auth(hass, device, entry): +async def async_populate_snapshot_auth( + hass: HomeAssistant, device: ONVIFDevice, entry: ConfigEntry +) -> None: """Check if digest auth for snapshots is possible.""" - auth = await _get_snapshot_auth(device) - new_data = {**entry.data, CONF_SNAPSHOT_AUTH: auth} - hass.config_entries.async_update_entry(entry, data=new_data) + if auth := await _get_snapshot_auth(device): + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_SNAPSHOT_AUTH: auth} + ) -async def async_populate_options(hass, entry): +async def async_populate_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Populate default options for device.""" options = { CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS, From 4e19843152c3e12d6a9534ae2c0b4cfad8cbd449 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Jun 2023 12:15:16 -0500 Subject: [PATCH 14/66] Bump python-onvif-zeep to 3.1.9 (#93930) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index fd00d28b832..e92e80a9a68 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==3.1.8", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==3.1.9", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1eadb01faa0..ef0d50c94b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1271,7 +1271,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==3.1.8 +onvif-zeep-async==3.1.9 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ea12c24bd2..104483b6f1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -961,7 +961,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==3.1.8 +onvif-zeep-async==3.1.9 # homeassistant.components.opengarage open-garage==0.2.0 From 3317ea7d95892d9b38004bf86b07e5ddf98acb70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Jun 2023 12:20:32 -0500 Subject: [PATCH 15/66] Bump pyunifiprotect to 4.9.1 (#93931) --- 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 fcb30cdba5f..a2bb76c92b7 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.9.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.9.1", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index ef0d50c94b0..befb12ec528 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2171,7 +2171,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.9.0 +pyunifiprotect==4.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 104483b6f1b..e7aac0f8314 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.9.0 +pyunifiprotect==4.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From a98094eda62a06a4672a8c5e23836a66052ea56c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Jun 2023 15:07:11 -0400 Subject: [PATCH 16/66] Bumped version to 2023.6.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 37eb0d02c60..e6dbcc96d0d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 9d075644101..04197ba7b1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b1" +version = "2023.6.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e58ea00ce6d5be4aa9100a3d9b4fd0db303036e1 Mon Sep 17 00:00:00 2001 From: Tudor Sandu Date: Fri, 2 Jun 2023 03:10:57 +0300 Subject: [PATCH 17/66] Fix states not being translated in voice assistants (#93572) Fix states not being translated --- homeassistant/components/conversation/default_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 5cb4487de65..44b13522412 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -278,13 +278,13 @@ class DefaultAgent(AbstractConversationAgent): all_states = matched + unmatched domains = {state.domain for state in all_states} translations = await translation.async_get_translations( - self.hass, language, "state", domains + self.hass, language, "entity_component", domains ) # Use translated state names for state in all_states: device_class = state.attributes.get("device_class", "_") - key = f"component.{state.domain}.state.{device_class}.{state.state}" + key = f"component.{state.domain}.entity_component.{device_class}.state.{state.state}" state.state = translations.get(key, state.state) # Get first matched or unmatched state. From 964af88c21e538354f0ee9c9d6eca2b0fe7c81ff Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 2 Jun 2023 13:44:36 +0100 Subject: [PATCH 18/66] Make Riemann sum sensors restore last valid state (#93674) * keep last valid state * keep last valid state * typo * increase coverage * better error handling * debug messages * increase coverage * remove random log * don't expose last_valid_state as an attribute --- .../components/integration/sensor.py | 119 ++++++++++++++++-- tests/components/integration/test_sensor.py | 96 +++++++++++++- 2 files changed, 204 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 7e60f2c509c..b28b426d3af 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -1,16 +1,19 @@ """Numeric integration of data coming from a source sensor over time.""" from __future__ import annotations -from decimal import Decimal, DecimalException +from dataclasses import dataclass +from decimal import Decimal, DecimalException, InvalidOperation import logging -from typing import Final +from typing import Any, Final +from typing_extensions import Self import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + RestoreSensor, SensorDeviceClass, - SensorEntity, + SensorExtraStoredData, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -28,7 +31,6 @@ from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -79,6 +81,53 @@ PLATFORM_SCHEMA = vol.All( ) +@dataclass +class IntegrationSensorExtraStoredData(SensorExtraStoredData): + """Object to hold extra stored data.""" + + source_entity: str | None + last_valid_state: Decimal | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the utility sensor data.""" + data = super().as_dict() + data["source_entity"] = self.source_entity + data["last_valid_state"] = ( + str(self.last_valid_state) if self.last_valid_state else None + ) + return data + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> Self | None: + """Initialize a stored sensor state from a dict.""" + extra = SensorExtraStoredData.from_dict(restored) + if extra is None: + return None + + source_entity = restored.get(ATTR_SOURCE_ID) + + try: + last_valid_state = ( + Decimal(str(restored.get("last_valid_state"))) + if restored.get("last_valid_state") + else None + ) + except InvalidOperation: + # last_period is corrupted + _LOGGER.error("Could not use last_valid_state") + return None + + if last_valid_state is None: + return None + + return cls( + extra.native_value, + extra.native_unit_of_measurement, + source_entity, + last_valid_state, + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -129,7 +178,7 @@ async def async_setup_platform( # pylint: disable-next=hass-invalid-inheritance # needs fixing -class IntegrationSensor(RestoreEntity, SensorEntity): +class IntegrationSensor(RestoreSensor): """Representation of an integration sensor.""" _attr_state_class = SensorStateClass.TOTAL @@ -160,7 +209,8 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_time = UNIT_TIME[unit_time] self._unit_time_str = unit_time self._attr_icon = "mdi:chart-histogram" - self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} + self._source_entity: str = source_entity + self._last_valid_state: Decimal | None = None def _unit(self, source_unit: str) -> str: """Derive unit from the source sensor, SI prefix and time unit.""" @@ -175,10 +225,28 @@ class IntegrationSensor(RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - if (state := await self.async_get_last_state()) is not None: - if state.state == STATE_UNAVAILABLE: - self._attr_available = False - elif state.state != STATE_UNKNOWN: + + if (last_sensor_data := await self.async_get_last_sensor_data()) is not None: + self._state = ( + Decimal(str(last_sensor_data.native_value)) + if last_sensor_data.native_value + else last_sensor_data.last_valid_state + ) + self._attr_native_value = last_sensor_data.native_value + self._unit_of_measurement = last_sensor_data.native_unit_of_measurement + self._last_valid_state = last_sensor_data.last_valid_state + + _LOGGER.debug( + "Restored state %s and last_valid_state %s", + self._state, + self._last_valid_state, + ) + elif (state := await self.async_get_last_state()) is not None: + # legacy to be removed on 2023.10 (we are keeping this to avoid losing data during the transition) + if state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]: + if state.state == STATE_UNAVAILABLE: + self._attr_available = False + else: try: self._state = Decimal(state.state) except (DecimalException, ValueError) as err: @@ -295,6 +363,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._state += integral else: self._state = integral + self._last_valid_state = self._state self.async_write_ha_state() self.async_on_remove( @@ -314,3 +383,33 @@ class IntegrationSensor(RestoreEntity, SensorEntity): def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._unit_of_measurement + + @property + def extra_state_attributes(self) -> dict[str, str] | None: + """Return the state attributes of the sensor.""" + state_attr = { + ATTR_SOURCE_ID: self._source_entity, + } + + return state_attr + + @property + def extra_restore_state_data(self) -> IntegrationSensorExtraStoredData: + """Return sensor specific state data to be restored.""" + return IntegrationSensorExtraStoredData( + self.native_value, + self.native_unit_of_measurement, + self._source_entity, + self._last_valid_state, + ) + + async def async_get_last_sensor_data( + self, + ) -> IntegrationSensorExtraStoredData | None: + """Restore Utility Meter Sensor Extra Stored Data.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + + return IntegrationSensorExtraStoredData.from_dict( + restored_last_extra_data.as_dict() + ) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 515ae990deb..355d13c84d6 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import mock_restore_cache +from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data @pytest.mark.parametrize("method", ["trapezoidal", "left", "right"]) @@ -163,6 +163,100 @@ async def test_restore_state(hass: HomeAssistant) -> None: assert state.state == "100.00" assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY + assert state.attributes.get("last_good_state") is None + + +async def test_restore_unavailable_state(hass: HomeAssistant) -> None: + """Test integration sensor state is restored correctly.""" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.integration", + STATE_UNAVAILABLE, + { + "device_class": SensorDeviceClass.ENERGY, + "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, + }, + ), + { + "native_value": None, + "native_unit_of_measurement": "kWh", + "source_entity": "sensor.power", + "last_valid_state": "100.00", + }, + ), + ], + ) + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "round": 2, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state + assert state.state == "100.00" + + +@pytest.mark.parametrize( + "extra_attributes", + [ + { + "native_unit_of_measurement": "kWh", + "source_entity": "sensor.power", + "last_valid_state": "100.00", + }, + { + "native_value": None, + "native_unit_of_measurement": "kWh", + "source_entity": "sensor.power", + "last_valid_state": "None", + }, + ], +) +async def test_restore_unavailable_state_failed( + hass: HomeAssistant, extra_attributes +) -> None: + """Test integration sensor state is restored correctly.""" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.integration", + STATE_UNAVAILABLE, + { + "device_class": SensorDeviceClass.ENERGY, + "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, + }, + ), + extra_attributes, + ), + ], + ) + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.power", + "round": 2, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state + assert state.state == STATE_UNAVAILABLE async def test_restore_state_failed(hass: HomeAssistant) -> None: From d9149407d8e5887988a22909eb6b74a5b025ffc3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 1 Jun 2023 22:24:53 -0500 Subject: [PATCH 19/66] Update pyipp to 0.13.0 (#93886) --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 59f8c32c210..e93f9832722 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], "quality_scale": "platinum", - "requirements": ["pyipp==0.12.1"], + "requirements": ["pyipp==0.13.0"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index befb12ec528..be7a22e02b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1708,7 +1708,7 @@ pyintesishome==1.8.0 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.12.1 +pyipp==0.13.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7aac0f8314..cf5d78235a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1254,7 +1254,7 @@ pyinsteon==1.4.2 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.12.1 +pyipp==0.13.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 From 3d2ad2fd85706896ca8198eedfa86f8d6cbad455 Mon Sep 17 00:00:00 2001 From: automaton82 Date: Thu, 1 Jun 2023 16:23:26 -0400 Subject: [PATCH 20/66] Update netdata to 1.1.0, set longer timeout (#93937) --- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netdata/sensor.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 2d7604765c4..99410ce033d 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/netdata", "iot_class": "local_polling", "loggers": ["netdata"], - "requirements": ["netdata==1.0.1"] + "requirements": ["netdata==1.1.0"] } diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 6606604ac90..1ab7a48e1b3 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -66,7 +66,7 @@ async def async_setup_platform( port = config[CONF_PORT] resources = config[CONF_RESOURCES] - netdata = NetdataData(Netdata(host, port=port)) + netdata = NetdataData(Netdata(host, port=port, timeout=20.0)) await netdata.async_update() if netdata.api.metrics is None: diff --git a/requirements_all.txt b/requirements_all.txt index be7a22e02b9..38b328ab848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1182,7 +1182,7 @@ ndms2_client==0.1.2 nessclient==0.10.0 # homeassistant.components.netdata -netdata==1.0.1 +netdata==1.1.0 # homeassistant.components.discovery netdisco==3.0.0 From cc02d1dfc4c4b8a850d014cd705597a06b74ef64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Jun 2023 18:54:25 -0500 Subject: [PATCH 21/66] Fix august aiohttp session being closed out from under it (#93942) * Fix august aiohttp session being closed out from under it fixes #93941 * Fix august aiohttp session being closed out from under it fixes #93941 * Fix august aiohttp session being closed out from under it fixes #93941 --- homeassistant/components/august/__init__.py | 9 ++++-- .../components/august/config_flow.py | 29 +++++++++++++++++-- homeassistant/components/august/gateway.py | 10 ++----- tests/components/august/test_gateway.py | 2 +- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 8be7d8dd2d1..8738b58dab9 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -23,7 +23,7 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, HomeAssistantError, ) -from homeassistant.helpers import device_registry as dr, discovery_flow +from homeassistant.helpers import aiohttp_client, device_registry as dr, discovery_flow from .activity import ActivityStream from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS @@ -44,8 +44,11 @@ YALEXS_BLE_DOMAIN = "yalexs_ble" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up August from a config entry.""" - - august_gateway = AugustGateway(hass) + # Create an aiohttp session instead of using the default one since the + # default one is likely to trigger august's WAF if another integration + # is also using Cloudflare + session = aiohttp_client.async_create_clientsession(hass) + august_gateway = AugustGateway(hass, session) try: await august_gateway.async_setup(entry.data) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 58f1c2fc976..670d1608421 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -4,13 +4,16 @@ from dataclasses import dataclass import logging from typing import Any +import aiohttp import voluptuous as vol from yalexs.authenticator import ValidationResult from yalexs.const import BRANDS, DEFAULT_BRAND from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client from .const import ( CONF_ACCESS_TOKEN_CACHE_FILE, @@ -80,6 +83,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Store an AugustGateway().""" self._august_gateway: AugustGateway | None = None + self._aiohttp_session: aiohttp.ClientSession | None = None self._user_auth_details: dict[str, Any] = {} self._needs_reset = True self._mode = None @@ -87,7 +91,6 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - self._august_gateway = AugustGateway(self.hass) return await self.async_step_user_validate() async def async_step_user_validate(self, user_input=None): @@ -151,12 +154,30 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) + @callback + def _async_get_gateway(self) -> AugustGateway: + """Set up the gateway.""" + if self._august_gateway is not None: + return self._august_gateway + # Create an aiohttp session instead of using the default one since the + # default one is likely to trigger august's WAF if another integration + # is also using Cloudflare + self._aiohttp_session = aiohttp_client.async_create_clientsession(self.hass) + self._august_gateway = AugustGateway(self.hass, self._aiohttp_session) + return self._august_gateway + + @callback + def _async_shutdown_gateway(self) -> None: + """Shutdown the gateway.""" + if self._aiohttp_session is not None: + self._aiohttp_session.detach() + self._august_gateway = None + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._user_auth_details = dict(entry_data) self._mode = "reauth" self._needs_reset = True - self._august_gateway = AugustGateway(self.hass) return await self.async_step_reauth_validate() async def async_step_reauth_validate(self, user_input=None): @@ -206,7 +227,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_auth_or_validate(self) -> ValidateResult: """Authenticate or validate.""" user_auth_details = self._user_auth_details - gateway = self._august_gateway + gateway = self._async_get_gateway() assert gateway is not None await self._async_reset_access_token_cache_if_needed( gateway, @@ -239,6 +260,8 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_update_or_create_entry(self, info: dict[str, Any]) -> FlowResult: """Update existing entry or create a new one.""" + self._async_shutdown_gateway() + existing_entry = await self.async_set_unique_id( self._user_auth_details[CONF_USERNAME] ) diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 9dcf96f057a..badff721d10 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -7,7 +7,7 @@ import logging import os from typing import Any -from aiohttp import ClientError, ClientResponseError +from aiohttp import ClientError, ClientResponseError, ClientSession from yalexs.api_async import ApiAsync from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync from yalexs.authenticator_common import Authentication @@ -16,7 +16,6 @@ from yalexs.exceptions import AugustApiAIOHTTPError from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import aiohttp_client from .const import ( CONF_ACCESS_TOKEN_CACHE_FILE, @@ -35,12 +34,9 @@ _LOGGER = logging.getLogger(__name__) class AugustGateway: """Handle the connection to August.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, aiohttp_session: ClientSession) -> None: """Init the connection.""" - # Create an aiohttp session instead of using the default one since the - # default one is likely to trigger august's WAF if another integration - # is also using Cloudflare - self._aiohttp_session = aiohttp_client.async_create_clientsession(hass) + self._aiohttp_session = aiohttp_session self._token_refresh_lock = asyncio.Lock() self._access_token_cache_file: str | None = None self._hass: HomeAssistant = hass diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index d0e18c0bed4..2a364304c4b 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -35,7 +35,7 @@ async def _patched_refresh_access_token( "original_token", 1234, AuthenticationState.AUTHENTICATED ) ) - august_gateway = AugustGateway(hass) + august_gateway = AugustGateway(hass, MagicMock()) mocked_config = _mock_get_config() await august_gateway.async_setup(mocked_config[DOMAIN]) await august_gateway.async_authenticate() From 9dd3e6cab884ca5399c24bfc54381c02f8055d37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Jun 2023 18:54:44 -0500 Subject: [PATCH 22/66] Bump aiohomekit to 2.6.4 (#93943) changelog: https://github.com/Jc2k/aiohomekit/compare/2.6.3...2.6.4 mostly additional logging to help track down #93891 --- 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 9db26d4c8e0..89261df8751 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==2.6.3"], + "requirements": ["aiohomekit==2.6.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 38b328ab848..f3b05058c8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.3 +aiohomekit==2.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf5d78235a2..bd35ab61341 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.3 +aiohomekit==2.6.4 # homeassistant.components.emulated_hue # homeassistant.components.http From 3d4ba15a95ee3d9e7498a659fcb531412dddcfaa Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Jun 2023 06:09:53 -0400 Subject: [PATCH 23/66] Make Z-Wave device IBT4ZWAVE discoverable as a cover (#93946) * Make Z-Wave device IBT4ZWAVE discoverable as a cover * Test device class --- homeassistant/components/zwave_js/cover.py | 4 +- .../components/zwave_js/discovery.py | 10 + tests/components/zwave_js/conftest.py | 16 +- .../fixtures/cover_nice_ibt4zwave_state.json | 1410 +++++++++++++++++ tests/components/zwave_js/test_cover.py | 64 + 5 files changed, 1502 insertions(+), 2 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/cover_nice_ibt4zwave_state.json diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 4b3113af202..9a8cb203c05 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -305,8 +305,10 @@ class ZWaveMultilevelSwitchCover(CoverPositionMixin): self._attr_device_class = CoverDeviceClass.WINDOW if self.info.platform_hint and self.info.platform_hint.startswith("shutter"): self._attr_device_class = CoverDeviceClass.SHUTTER - if self.info.platform_hint and self.info.platform_hint.startswith("blind"): + elif self.info.platform_hint and self.info.platform_hint.startswith("blind"): self._attr_device_class = CoverDeviceClass.BLIND + elif self.info.platform_hint and self.info.platform_hint.startswith("gate"): + self._attr_device_class = CoverDeviceClass.GATE class ZWaveTiltCover(ZWaveMultilevelSwitchCover, CoverTiltMixin): diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 62fc665c72e..c6aa14bceb6 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -374,6 +374,16 @@ DISCOVERY_SCHEMAS = [ ) ], ), + # Fibaro Nice BiDi-ZWave (IBT4ZWAVE) + ZWaveDiscoverySchema( + platform=Platform.COVER, + hint="gate", + manufacturer_id={0x0441}, + product_id={0x1000}, + product_type={0x2400}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + required_values=[SWITCH_MULTILEVEL_TARGET_VALUE_SCHEMA], + ), # Qubino flush shutter ZWaveDiscoverySchema( platform=Platform.COVER, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 860e5742c80..1751bd0c0ca 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -630,6 +630,12 @@ def energy_production_state_fixture(): return json.loads(load_fixture("zwave_js/energy_production_state.json")) +@pytest.fixture(name="nice_ibt4zwave_state", scope="session") +def nice_ibt4zwave_state_fixture(): + """Load a Nice IBT4ZWAVE cover node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_nice_ibt4zwave_state.json")) + + # model fixtures @@ -1200,8 +1206,16 @@ def indicator_test_fixture(client, indicator_test_state): @pytest.fixture(name="energy_production") -def energy_prodution_fixture(client, energy_production_state): +def energy_production_fixture(client, energy_production_state): """Mock a mock node with Energy Production CC.""" node = Node(client, copy.deepcopy(energy_production_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="nice_ibt4zwave") +def nice_ibt4zwave_fixture(client, nice_ibt4zwave_state): + """Mock a Nice IBT4ZWAVE cover node.""" + node = Node(client, copy.deepcopy(nice_ibt4zwave_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/cover_nice_ibt4zwave_state.json b/tests/components/zwave_js/fixtures/cover_nice_ibt4zwave_state.json new file mode 100644 index 00000000000..eab42c321fe --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_nice_ibt4zwave_state.json @@ -0,0 +1,1410 @@ +{ + "nodeId": 72, + "index": 0, + "installerIcon": 7680, + "userIcon": 7680, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 1089, + "productId": 4096, + "productType": 9216, + "firmwareVersion": "7.0", + "zwavePlusVersion": 2, + "name": "Portail", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/data/db/devices/0x0441/ibt4zwave.json", + "isEmbedded": true, + "manufacturer": "NICE Spa", + "manufacturerId": 1089, + "label": "IBT4ZWAVE", + "description": "BusT4-Z-Wave interface", + "devices": [ + { + "productType": 9216, + "productId": 4096 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Install the external antenna before powering the device and adding to the Z-Wave network for the device to automatically detect and enable it (use only antennas and cables compliant with technical specification).\n\n01. Set the Z-Wave gateway into adding mode (see the Z-Wave gateway\u2019s manual)\n02. On the IBT4ZWAVE press and release the S1 button 3 times x3 S1\n03. LEDs on the IBT4ZW AVE will start slow flashing alter nately\n04. If you are adding in Security S2 Authenticated, input the underlined part\nof the DSK (label on the box) DSK: XXXXX-XXXXX-XXXXX-XXXXX XXXXX-XXXXX-XXXXX-XXXXX\n05. When the adding process ends, the LEDs on the IBT4ZWAVE will show adding and antenna status (Table 1 in manual)", + "exclusion": "01. Set the Z-Wave gateway into remove mode (see the Z-Wave gateway\u2019s manual)\n02. On the IBT4ZWAVE press and release the S1 button 3 times x3 S1\n03. LEDs on the IBT4ZW AVE will start slow flashing alternately\n04. Wait for the removing process to end", + "reset": "01. Press and hold the S1 button\n03. Wait 3 seconds\n04. LEDs will show adding and antenna status (Table 1 in manual) for 3 seconds\n05. LEDs will turn off for 3 seconds\n06. LEDs will show selected antenna (Table 2 in manual) for 3 seconds\n07. When both LEDs light up simultaneously, release the button\n08. Press and release the S1 button\n09. Both LEDs will flash once at the end of the procedure", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/3837/IBT4ZWAVE-T-v0.7.pdf" + } + }, + "label": "IBT4ZWAVE", + "interviewAttempts": 2, + "endpoints": [ + { + "nodeId": 72, + "index": 0, + "installerIcon": 7680, + "userIcon": 7680, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [32, 38], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": false + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Open", + "propertyName": "Open", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Open)", + "ccSpecific": { + "switchType": 3 + }, + "valueChangeOptions": ["transitionDuration"] + }, + "value": false, + "nodeId": 72 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Close", + "propertyName": "Close", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Close)", + "ccSpecific": { + "switchType": 3 + }, + "valueChangeOptions": ["transitionDuration"] + }, + "value": true, + "nodeId": 72 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 4278190080, + "propertyName": "Alarm Configuration - 1st Slot Notification Type", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 1st Slot Notification Type", + "default": 0, + "min": 0, + "max": 22, + "states": { + "0": "Disabled", + "1": "Smoke Alarm", + "2": "CO Alarm", + "3": "CO2 Alarm", + "4": "Heat Alarm", + "5": "Water Alarm", + "6": "Access Control", + "7": "Home Security", + "8": "Power Management", + "9": "System", + "10": "Emergency Alarm", + "11": "Clock", + "12": "Appliance", + "13": "Home Health", + "14": "Siren", + "15": "Water Valve", + "16": "Weather Alarm", + "17": "Irrigation", + "18": "Gas Alarm", + "19": "Pest Control", + "20": "Light Sensor", + "21": "Water Quality Monitoring", + "22": "Home Monitoring" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 16711680, + "propertyName": "Alarm Configuration - 1st Slot Notification Event", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 1st Slot Notification Event", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "255": "Any Notification," + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 65280, + "propertyName": "Alarm Configuration - 1st Slot Notification Event Parameter", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 1st Slot Notification Event Parameter", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 255, + "propertyName": "Alarm Configuration - 1st Slot Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 1st Slot Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No Action", + "1": "Open", + "2": "Close" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 4278190080, + "propertyName": "Alarm Configuration - 2nd Slot Notification Type", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 2nd Slot Notification Type", + "default": 5, + "min": 0, + "max": 22, + "states": { + "0": "Disabled", + "1": "Smoke Alarm", + "2": "CO Alarm", + "3": "CO2 Alarm", + "4": "Heat Alarm", + "5": "Water Alarm", + "6": "Access Control", + "7": "Home Security", + "8": "Power Management", + "9": "System", + "10": "Emergency Alarm", + "11": "Clock", + "12": "Appliance", + "13": "Home Health", + "14": "Siren", + "15": "Water Valve", + "16": "Weather Alarm", + "17": "Irrigation", + "18": "Gas Alarm", + "19": "Pest Control", + "20": "Light Sensor", + "21": "Water Quality Monitoring", + "22": "Home Monitoring" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 16711680, + "propertyName": "Alarm Configuration - 2nd Slot Notification Event", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 2nd Slot Notification Event", + "default": 255, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "255": "Any Notification," + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 65280, + "propertyName": "Alarm Configuration - 2nd Slot Notification Event Parameter", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 2nd Slot Notification Event Parameter", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 255, + "propertyName": "Alarm Configuration - 2nd Slot Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 2nd Slot Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No Action", + "1": "Open", + "2": "Close" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 4278190080, + "propertyName": "Alarm Configuration - 3rd Slot Notification Type", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 3rd Slot Notification Type", + "default": 1, + "min": 0, + "max": 22, + "states": { + "0": "Disabled", + "1": "Smoke Alarm", + "2": "CO Alarm", + "3": "CO2 Alarm", + "4": "Heat Alarm", + "5": "Water Alarm", + "6": "Access Control", + "7": "Home Security", + "8": "Power Management", + "9": "System", + "10": "Emergency Alarm", + "11": "Clock", + "12": "Appliance", + "13": "Home Health", + "14": "Siren", + "15": "Water Valve", + "16": "Weather Alarm", + "17": "Irrigation", + "18": "Gas Alarm", + "19": "Pest Control", + "20": "Light Sensor", + "21": "Water Quality Monitoring", + "22": "Home Monitoring" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 16711680, + "propertyName": "Alarm Configuration - 3rd Slot Notification Event", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 3rd Slot Notification Event", + "default": 255, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "255": "Any Notification," + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 65280, + "propertyName": "Alarm Configuration - 3rd Slot Notification Event Parameter", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 3rd Slot Notification Event Parameter", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 255, + "propertyName": "Alarm Configuration - 3rd Slot Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 3rd Slot Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No Action", + "1": "Open", + "2": "Close" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 4278190080, + "propertyName": "Alarm Configuration - 4th Slot Notification Type", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 4th Slot Notification Type", + "default": 2, + "min": 0, + "max": 22, + "states": { + "0": "Disabled", + "1": "Smoke Alarm", + "2": "CO Alarm", + "3": "CO2 Alarm", + "4": "Heat Alarm", + "5": "Water Alarm", + "6": "Access Control", + "7": "Home Security", + "8": "Power Management", + "9": "System", + "10": "Emergency Alarm", + "11": "Clock", + "12": "Appliance", + "13": "Home Health", + "14": "Siren", + "15": "Water Valve", + "16": "Weather Alarm", + "17": "Irrigation", + "18": "Gas Alarm", + "19": "Pest Control", + "20": "Light Sensor", + "21": "Water Quality Monitoring", + "22": "Home Monitoring" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 16711680, + "propertyName": "Alarm Configuration - 4th Slot Notification Event", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 4th Slot Notification Event", + "default": 255, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "255": "Any Notification," + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 65280, + "propertyName": "Alarm Configuration - 4th Slot Notification Event Parameter", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 4th Slot Notification Event Parameter", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 255, + "propertyName": "Alarm Configuration - 4th Slot Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 4th Slot Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No Action", + "1": "Open", + "2": "Close" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 4278190080, + "propertyName": "Alarm Configuration - 5th Slot Notification Type", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 5th Slot Notification Type", + "default": 4, + "min": 0, + "max": 22, + "states": { + "0": "Disabled", + "1": "Smoke Alarm", + "2": "CO Alarm", + "3": "CO2 Alarm", + "4": "Heat Alarm", + "5": "Water Alarm", + "6": "Access Control", + "7": "Home Security", + "8": "Power Management", + "9": "System", + "10": "Emergency Alarm", + "11": "Clock", + "12": "Appliance", + "13": "Home Health", + "14": "Siren", + "15": "Water Valve", + "16": "Weather Alarm", + "17": "Irrigation", + "18": "Gas Alarm", + "19": "Pest Control", + "20": "Light Sensor", + "21": "Water Quality Monitoring", + "22": "Home Monitoring" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 16711680, + "propertyName": "Alarm Configuration - 5th Slot Notification Event", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 5th Slot Notification Event", + "default": 255, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "255": "Any Notification," + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 65280, + "propertyName": "Alarm Configuration - 5th Slot Notification Event Parameter", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 5th Slot Notification Event Parameter", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyKey": 255, + "propertyName": "Alarm Configuration - 5th Slot Action", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Alarm Configuration - 5th Slot Action", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "No Action", + "1": "Open", + "2": "Close" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Barrier control status", + "propertyName": "Access Control", + "propertyKeyName": "Barrier control status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Barrier control status", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "76": "Barrier associated with non Z-Wave remote control" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Hardware status", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Hardware status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "3": "System hardware failure (with failure code)" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Barrier safety beam obstacle status", + "propertyName": "Access Control", + "propertyKeyName": "Barrier safety beam obstacle status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Barrier safety beam obstacle status", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "72": "Barrier safety beam obstacle" + } + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 1089 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 9216 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 4096 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "2": "NoOperationPossible" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "rf", + "propertyName": "rf", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "RF protection state", + "states": { + "0": "Unprotected", + "1": "NoControl" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "exclusiveControlNodeId", + "propertyName": "exclusiveControlNodeId", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "7.13" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["7.0"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version" + }, + "value": "7.13.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version" + }, + "value": "10.13.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number" + }, + "value": 175 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version" + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "7.13.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number" + }, + "value": 175 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version" + }, + "value": "7.0.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymetic On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + } + }, + "value": 0 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [32, 38], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0441:0x2400:0x1000:7.0", + "statistics": { + "commandsTX": 254, + "commandsRX": 224, + "commandsDroppedRX": 47, + "commandsDroppedTX": 85, + "timeoutResponse": 4, + "rtt": 18.7 + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index ae38c82a75c..502f2413c99 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -1,4 +1,6 @@ """Test the Z-Wave JS cover platform.""" +import logging + from zwave_js_server.const import ( CURRENT_STATE_PROPERTY, CURRENT_VALUE_PROPERTY, @@ -24,6 +26,7 @@ from homeassistant.components.cover import ( CoverDeviceClass, CoverEntityFeature, ) +from homeassistant.components.zwave_js.const import LOGGER from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -45,6 +48,7 @@ BLIND_COVER_ENTITY = "cover.window_blind_controller" SHUTTER_COVER_ENTITY = "cover.flush_shutter" AEOTEC_SHUTTER_COVER_ENTITY = "cover.nano_shutter_v_3" FIBARO_SHUTTER_COVER_ENTITY = "cover.fgr_222_test_cover" +LOGGER.setLevel(logging.DEBUG) async def test_window_cover( @@ -795,3 +799,63 @@ async def test_iblinds_v3_cover( assert args["value"] is False client.async_send_command.reset_mock() + + +async def test_nice_ibt4zwave_cover( + hass: HomeAssistant, client, nice_ibt4zwave, integration +) -> None: + """Test Nice IBT4ZWAVE cover.""" + entity_id = "cover.portail" + state = hass.states.get(entity_id) + assert state + # This device has no state because there is no position value + assert state.state == STATE_CLOSED + assert state.attributes[ATTR_SUPPORTED_FEATURES] == ( + CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.STOP + ) + assert ATTR_CURRENT_POSITION in state.attributes + assert state.attributes[ATTR_CURRENT_POSITION] == 0 + assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GATE + + await hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 72 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 38, + "property": "targetValue", + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 72 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 38, + "property": "targetValue", + } + assert args["value"] == 99 + + client.async_send_command.reset_mock() From 177cd0f6975b86c91b95282037075ec84f049735 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Jun 2023 06:12:32 -0400 Subject: [PATCH 24/66] Improve logic for zwave_js.lock.is_locked attr (#93947) --- homeassistant/components/zwave_js/lock.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index ff4cb84b47c..5457916a1e1 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -99,14 +99,17 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): @property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" - if self.info.primary_value.value is None: + value = self.info.primary_value + if value.value is None or ( + value.command_class == CommandClass.DOOR_LOCK + and value.value == DoorLockMode.UNKNOWN + ): # guard missing value return None - return int( - LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[ - CommandClass(self.info.primary_value.command_class) - ] - ) == int(self.info.primary_value.value) + return ( + LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[CommandClass(value.command_class)] + == self.info.primary_value.value + ) async def _set_lock_state(self, target_state: str, **kwargs: Any) -> None: """Set the lock state.""" From 32f7f39ecace97bbf996e73298e307e853cfc135 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Jun 2023 08:46:29 -0400 Subject: [PATCH 25/66] Bumped version to 2023.6.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e6dbcc96d0d..1ac0da081df 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 04197ba7b1d..ac39a138984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b2" +version = "2023.6.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6ff55a650556c192d464611ef539b01afcb23de0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 3 Jun 2023 05:35:11 +0200 Subject: [PATCH 26/66] Add scan interval to Command Line (#93752) * Add scan interval * Handle previous not complete * Fix faulty text * Add tests * lingering * Cool down * Fix tests --- .../components/command_line/__init__.py | 25 ++++++- .../components/command_line/binary_sensor.py | 47 +++++++++++- .../components/command_line/const.py | 4 ++ .../components/command_line/cover.py | 71 +++++++++++++++---- .../components/command_line/sensor.py | 61 ++++++++++++---- .../components/command_line/switch.py | 60 +++++++++++++--- .../command_line/test_binary_sensor.py | 63 ++++++++++++++++ tests/components/command_line/test_cover.py | 58 +++++++++++++++ tests/components/command_line/test_sensor.py | 56 +++++++++++++++ tests/components/command_line/test_switch.py | 59 +++++++++++++++ 10 files changed, 463 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 651094db7f1..c9c18fe54a8 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -11,16 +11,24 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, + SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL, +) +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL, ) -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, DOMAIN as SENSOR_DOMAIN, + SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL, STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA, ) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL, +) from homeassistant.const import ( CONF_COMMAND, CONF_COMMAND_CLOSE, @@ -34,6 +42,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, @@ -74,6 +83,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL + ): vol.All(cv.time_period, cv.positive_timedelta), } ) COVER_SCHEMA = vol.Schema( @@ -86,6 +98,9 @@ COVER_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), } ) NOTIFY_SCHEMA = vol.Schema( @@ -106,6 +121,9 @@ SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA, + vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), } ) SWITCH_SCHEMA = vol.Schema( @@ -118,6 +136,9 @@ SWITCH_SCHEMA = vol.Schema( vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), } ) COMBINED_SCHEMA = vol.Schema( diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 18b3cf71eb0..9c5a1ce1bbe 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -1,6 +1,7 @@ """Support for custom shell commands to retrieve values.""" from __future__ import annotations +import asyncio from datetime import timedelta import voluptuous as vol @@ -18,17 +19,19 @@ from homeassistant.const import ( CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .sensor import CommandSensorData DEFAULT_NAME = "Binary Command Sensor" @@ -84,6 +87,9 @@ async def async_setup_platform( value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT] unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID) + scan_interval: timedelta = binary_sensor_config.get( + CONF_SCAN_INTERVAL, SCAN_INTERVAL + ) if value_template is not None: value_template.hass = hass data = CommandSensorData(hass, command, command_timeout) @@ -98,6 +104,7 @@ async def async_setup_platform( payload_off, value_template, unique_id, + scan_interval, ) ], True, @@ -107,6 +114,8 @@ async def async_setup_platform( class CommandBinarySensor(BinarySensorEntity): """Representation of a command line binary sensor.""" + _attr_should_poll = False + def __init__( self, data: CommandSensorData, @@ -116,6 +125,7 @@ class CommandBinarySensor(BinarySensorEntity): payload_off: str, value_template: Template | None, unique_id: str | None, + scan_interval: timedelta, ) -> None: """Initialize the Command line binary sensor.""" self.data = data @@ -126,8 +136,39 @@ class CommandBinarySensor(BinarySensorEntity): self._payload_off = payload_off self._value_template = value_template self._attr_unique_id = unique_id + self._scan_interval = scan_interval + self._process_updates: asyncio.Lock | None = None - async def async_update(self) -> None: + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + await self._update_entity_state(None) + self.async_on_remove( + async_track_time_interval( + self.hass, + self._update_entity_state, + self._scan_interval, + name=f"Command Line Binary Sensor - {self.name}", + cancel_on_shutdown=True, + ), + ) + + async def _update_entity_state(self, now) -> None: + """Update the state of the entity.""" + if self._process_updates is None: + self._process_updates = asyncio.Lock() + if self._process_updates.locked(): + LOGGER.warning( + "Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s", + self.name, + self._scan_interval, + ) + return + + async with self._process_updates: + await self._async_update() + + async def _async_update(self) -> None: """Get the latest data and updates the state.""" await self.hass.async_add_executor_job(self.data.update) value = self.data.value @@ -141,3 +182,5 @@ class CommandBinarySensor(BinarySensorEntity): self._attr_is_on = True elif value == self._payload_off: self._attr_is_on = False + + self.async_write_ha_state() diff --git a/homeassistant/components/command_line/const.py b/homeassistant/components/command_line/const.py index 4394f388910..ff51cb7e331 100644 --- a/homeassistant/components/command_line/const.py +++ b/homeassistant/components/command_line/const.py @@ -1,7 +1,11 @@ """Allows to configure custom shell commands to turn a value for a sensor.""" +import logging + from homeassistant.const import Platform +LOGGER = logging.getLogger(__package__) + CONF_COMMAND_TIMEOUT = "command_timeout" DEFAULT_TIMEOUT = 15 DOMAIN = "command_line" diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 4503ceb8e56..2d2dc8c5fc2 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -1,7 +1,8 @@ """Support for command line covers.""" from __future__ import annotations -import logging +import asyncio +from datetime import timedelta from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -19,21 +20,23 @@ from homeassistant.const import ( CONF_COVERS, CONF_FRIENDLY_NAME, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .utils import call_shell_with_timeout, check_output_or_log -_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=15) COVER_SCHEMA = vol.Schema( { @@ -97,11 +100,12 @@ async def async_setup_platform( value_template, device_config[CONF_COMMAND_TIMEOUT], device_config.get(CONF_UNIQUE_ID), + device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) ) if not covers: - _LOGGER.error("No covers added") + LOGGER.error("No covers added") return async_add_entities(covers) @@ -110,6 +114,8 @@ async def async_setup_platform( class CommandCover(CoverEntity): """Representation a command line cover.""" + _attr_should_poll = False + def __init__( self, name: str, @@ -120,6 +126,7 @@ class CommandCover(CoverEntity): value_template: Template | None, timeout: int, unique_id: str | None, + scan_interval: timedelta, ) -> None: """Initialize the cover.""" self._attr_name = name @@ -131,17 +138,32 @@ class CommandCover(CoverEntity): self._value_template = value_template self._timeout = timeout self._attr_unique_id = unique_id - self._attr_should_poll = bool(command_state) + self._scan_interval = scan_interval + self._process_updates: asyncio.Lock | None = None + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + if self._command_state: + self.async_on_remove( + async_track_time_interval( + self.hass, + self._update_entity_state, + self._scan_interval, + name=f"Command Line Cover - {self.name}", + cancel_on_shutdown=True, + ), + ) def _move_cover(self, command: str) -> bool: """Execute the actual commands.""" - _LOGGER.info("Running command: %s", command) + LOGGER.info("Running command: %s", command) returncode = call_shell_with_timeout(command, self._timeout) success = returncode == 0 if not success: - _LOGGER.error( + LOGGER.error( "Command failed (with return code %s): %s", returncode, command ) @@ -165,12 +187,27 @@ class CommandCover(CoverEntity): def _query_state(self) -> str | None: """Query for the state.""" if self._command_state: - _LOGGER.info("Running state value command: %s", self._command_state) + LOGGER.info("Running state value command: %s", self._command_state) return check_output_or_log(self._command_state, self._timeout) if TYPE_CHECKING: return None - async def async_update(self) -> None: + async def _update_entity_state(self, now) -> None: + """Update the state of the entity.""" + if self._process_updates is None: + self._process_updates = asyncio.Lock() + if self._process_updates.locked(): + LOGGER.warning( + "Updating Command Line Cover %s took longer than the scheduled update interval %s", + self.name, + self._scan_interval, + ) + return + + async with self._process_updates: + await self._async_update() + + async def _async_update(self) -> None: """Update device state.""" if self._command_state: payload = str(await self.hass.async_add_executor_job(self._query_state)) @@ -181,15 +218,19 @@ class CommandCover(CoverEntity): self._state = None if payload: self._state = int(payload) + await self.async_update_ha_state(True) - def open_cover(self, **kwargs: Any) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - self._move_cover(self._command_open) + await self.hass.async_add_executor_job(self._move_cover, self._command_open) + await self._update_entity_state(None) - def close_cover(self, **kwargs: Any) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - self._move_cover(self._command_close) + await self.hass.async_add_executor_job(self._move_cover, self._command_close) + await self._update_entity_state(None) - def stop_cover(self, **kwargs: Any) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - self._move_cover(self._command_stop) + await self.hass.async_add_executor_job(self._move_cover, self._command_stop) + await self._update_entity_state(None) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 1689b136f2f..f42ac062081 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -1,10 +1,10 @@ """Allows to configure custom shell commands to turn a value for a sensor.""" from __future__ import annotations +import asyncio from collections.abc import Mapping from datetime import timedelta import json -import logging import voluptuous as vol @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_COMMAND, CONF_DEVICE_CLASS, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, @@ -28,15 +29,14 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .utils import check_output_or_log -_LOGGER = logging.getLogger(__name__) - CONF_JSON_ATTRIBUTES = "json_attributes" DEFAULT_NAME = "Command Sensor" @@ -88,6 +88,7 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) + scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) data = CommandSensorData(hass, command, command_timeout) async_add_entities( @@ -99,15 +100,17 @@ async def async_setup_platform( value_template, json_attributes, unique_id, + scan_interval, ) - ], - True, + ] ) class CommandSensor(SensorEntity): """Representation of a sensor that is using shell commands.""" + _attr_should_poll = False + def __init__( self, data: CommandSensorData, @@ -116,6 +119,7 @@ class CommandSensor(SensorEntity): value_template: Template | None, json_attributes: list[str] | None, unique_id: str | None, + scan_interval: timedelta, ) -> None: """Initialize the sensor.""" self._attr_name = name @@ -126,8 +130,39 @@ class CommandSensor(SensorEntity): self._value_template = value_template self._attr_native_unit_of_measurement = unit_of_measurement self._attr_unique_id = unique_id + self._scan_interval = scan_interval + self._process_updates: asyncio.Lock | None = None - async def async_update(self) -> None: + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + await self._update_entity_state(None) + self.async_on_remove( + async_track_time_interval( + self.hass, + self._update_entity_state, + self._scan_interval, + name=f"Command Line Sensor - {self.name}", + cancel_on_shutdown=True, + ), + ) + + async def _update_entity_state(self, now) -> None: + """Update the state of the entity.""" + if self._process_updates is None: + self._process_updates = asyncio.Lock() + if self._process_updates.locked(): + LOGGER.warning( + "Updating Command Line Sensor %s took longer than the scheduled update interval %s", + self.name, + self._scan_interval, + ) + return + + async with self._process_updates: + await self._async_update() + + async def _async_update(self) -> None: """Get the latest data and updates the state.""" await self.hass.async_add_executor_job(self.data.update) value = self.data.value @@ -144,11 +179,11 @@ class CommandSensor(SensorEntity): if k in json_dict } else: - _LOGGER.warning("JSON result was not a dictionary") + LOGGER.warning("JSON result was not a dictionary") except ValueError: - _LOGGER.warning("Unable to parse output as JSON: %s", value) + LOGGER.warning("Unable to parse output as JSON: %s", value) else: - _LOGGER.warning("Empty reply found when expecting JSON data") + LOGGER.warning("Empty reply found when expecting JSON data") if self._value_template is None: self._attr_native_value = None return @@ -163,6 +198,8 @@ class CommandSensor(SensorEntity): else: self._attr_native_value = value + self.async_write_ha_state() + class CommandSensorData: """The class for handling the data retrieval.""" @@ -191,7 +228,7 @@ class CommandSensorData: args_to_render = {"arguments": args} rendered_args = args_compiled.render(args_to_render) except TemplateError as ex: - _LOGGER.exception("Error rendering command template: %s", ex) + LOGGER.exception("Error rendering command template: %s", ex) return else: rendered_args = None @@ -203,5 +240,5 @@ class CommandSensorData: # Template used. Construct the string used in the shell command = f"{prog} {rendered_args}" - _LOGGER.debug("Running command: %s", command) + LOGGER.debug("Running command: %s", command) self.value = check_output_or_log(command, self.timeout) diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 7936bacd432..1a3dd39a342 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -1,7 +1,8 @@ """Support for custom shell commands to turn a switch on/off.""" from __future__ import annotations -import logging +import asyncio +from datetime import timedelta from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -20,6 +21,7 @@ from homeassistant.const import ( CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_SWITCHES, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -27,16 +29,17 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .utils import call_shell_with_timeout, check_output_or_log -_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=30) SWITCH_SCHEMA = vol.Schema( { @@ -112,11 +115,12 @@ async def async_setup_platform( device_config.get(CONF_COMMAND_STATE), value_template, device_config[CONF_COMMAND_TIMEOUT], + device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) ) if not switches: - _LOGGER.error("No switches added") + LOGGER.error("No switches added") return async_add_entities(switches) @@ -125,6 +129,8 @@ async def async_setup_platform( class CommandSwitch(ManualTriggerEntity, SwitchEntity): """Representation a switch that can be toggled using shell commands.""" + _attr_should_poll = False + def __init__( self, config: ConfigType, @@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): command_state: str | None, value_template: Template | None, timeout: int, + scan_interval: timedelta, ) -> None: """Initialize the switch.""" super().__init__(self.hass, config) @@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): self._command_state = command_state self._value_template = value_template self._timeout = timeout - self._attr_should_poll = bool(command_state) + self._scan_interval = scan_interval + self._process_updates: asyncio.Lock | None = None + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + if self._command_state: + self.async_on_remove( + async_track_time_interval( + self.hass, + self._update_entity_state, + self._scan_interval, + name=f"Command Line Cover - {self.name}", + cancel_on_shutdown=True, + ), + ) async def _switch(self, command: str) -> bool: """Execute the actual commands.""" - _LOGGER.info("Running command: %s", command) + LOGGER.info("Running command: %s", command) success = ( await self.hass.async_add_executor_job( @@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): ) if not success: - _LOGGER.error("Command failed: %s", command) + LOGGER.error("Command failed: %s", command) return success def _query_state_value(self, command: str) -> str | None: """Execute state command for return value.""" - _LOGGER.info("Running state value command: %s", command) + LOGGER.info("Running state value command: %s", command) return check_output_or_log(command, self._timeout) def _query_state_code(self, command: str) -> bool: """Execute state command for return code.""" - _LOGGER.info("Running state code command: %s", command) + LOGGER.info("Running state code command: %s", command) return ( call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0 ) @@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): if TYPE_CHECKING: return None - async def async_update(self) -> None: + async def _update_entity_state(self, now) -> None: + """Update the state of the entity.""" + if self._process_updates is None: + self._process_updates = asyncio.Lock() + if self._process_updates.locked(): + LOGGER.warning( + "Updating Command Line Switch %s took longer than the scheduled update interval %s", + self.name, + self._scan_interval, + ) + return + + async with self._process_updates: + await self._async_update() + + async def _async_update(self) -> None: """Update device state.""" if self._command_state: payload = str(await self.hass.async_add_executor_job(self._query_state)) @@ -201,15 +238,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): if payload or value: self._attr_is_on = (value or payload).lower() == "true" self._process_manual_data(payload) + await self.async_update_ha_state(True) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if await self._switch(self._command_on) and not self._command_state: self._attr_is_on = True self.async_schedule_update_ha_state() + await self._update_entity_state(None) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if await self._switch(self._command_off) and not self._command_state: self._attr_is_on = False self.async_schedule_update_ha_state() + await self._update_entity_state(None) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 6f79b6bdacf..eb6b52a66be 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -1,17 +1,24 @@ """The tests for the Command line Binary sensor platform.""" from __future__ import annotations +import asyncio +from datetime import timedelta from typing import Any +from unittest.mock import patch import pytest from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.command_line.binary_sensor import CommandBinarySensor from homeassistant.components.command_line.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed async def test_setup_platform_yaml(hass: HomeAssistant) -> None: @@ -189,3 +196,59 @@ async def test_return_code( ) await hass.async_block_till_done() assert "return code 33" in caplog.text + + +async def test_updating_to_often( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling updating when command already running.""" + called = [] + + class MockCommandBinarySensor(CommandBinarySensor): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.binary_sensor.CommandBinarySensor", + side_effect=MockCommandBinarySensor, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + "scan_interval": 0.1, + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert len(called) == 1 + assert ( + "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" + not in caplog.text + ) + + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(called) == 2 + assert ( + "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" + in caplog.text + ) + + await asyncio.sleep(0.2) diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 057e632c325..d977c202b04 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -1,6 +1,8 @@ """The tests the cover command line platform.""" from __future__ import annotations +import asyncio +from datetime import timedelta import os import tempfile from unittest.mock import patch @@ -9,6 +11,7 @@ import pytest from homeassistant import config as hass_config, setup from homeassistant.components.command_line import DOMAIN +from homeassistant.components.command_line.cover import CommandCover from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, @@ -320,3 +323,58 @@ async def test_unique_id( assert entity_registry.async_get_entity_id( "cover", "command_line", "not-so-unique-anymore" ) + + +async def test_updating_to_often( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling updating when command already running.""" + called = [] + + class MockCommandCover(CommandCover): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.cover.CommandCover", + side_effect=MockCommandCover, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "cover": { + "command_state": "echo 1", + "value_template": "{{ value }}", + "name": "Test", + "scan_interval": 0.1, + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert len(called) == 0 + assert ( + "Updating Command Line Cover Test took longer than the scheduled update interval" + not in caplog.text + ) + + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(called) == 1 + assert ( + "Updating Command Line Cover Test took longer than the scheduled update interval" + in caplog.text + ) + + await asyncio.sleep(0.2) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 7491e7011f5..87360d0e251 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Command line sensor platform.""" from __future__ import annotations +import asyncio from datetime import timedelta from typing import Any from unittest.mock import patch @@ -9,6 +10,7 @@ import pytest from homeassistant import setup from homeassistant.components.command_line import DOMAIN +from homeassistant.components.command_line.sensor import CommandSensor from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -530,3 +532,57 @@ async def test_unique_id( assert entity_registry.async_get_entity_id( "sensor", "command_line", "not-so-unique-anymore" ) + + +async def test_updating_to_often( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling updating when command already running.""" + called = [] + + class MockCommandSensor(CommandSensor): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.sensor.CommandSensor", + side_effect=MockCommandSensor, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo 1", + "scan_interval": 0.1, + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert len(called) == 1 + assert ( + "Updating Command Line Sensor Test took longer than the scheduled update interval" + not in caplog.text + ) + + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(called) == 2 + assert ( + "Updating Command Line Sensor Test took longer than the scheduled update interval" + in caplog.text + ) + + await asyncio.sleep(0.2) diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 017c453aa8b..88a87588375 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -1,6 +1,8 @@ """The tests for the Command line switch platform.""" from __future__ import annotations +import asyncio +from datetime import timedelta import json import os import subprocess @@ -11,6 +13,7 @@ import pytest from homeassistant import setup from homeassistant.components.command_line import DOMAIN +from homeassistant.components.command_line.switch import CommandSwitch from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, @@ -637,3 +640,59 @@ async def test_templating(hass: HomeAssistant) -> None: assert entity_state.attributes.get("icon") == "mdi:on" assert entity_state2.state == STATE_ON assert entity_state2.attributes.get("icon") == "mdi:on" + + +async def test_updating_to_often( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling updating when command already running.""" + called = [] + + class MockCommandSwitch(CommandSwitch): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.switch.CommandSwitch", + side_effect=MockCommandSwitch, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "switch": { + "command_state": "echo 1", + "command_on": "echo 2", + "command_off": "echo 3", + "name": "Test", + "scan_interval": 0.1, + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert len(called) == 0 + assert ( + "Updating Command Line Switch Test took longer than the scheduled update interval" + not in caplog.text + ) + + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(called) == 1 + assert ( + "Updating Command Line Switch Test took longer than the scheduled update interval" + in caplog.text + ) + + await asyncio.sleep(0.2) From f92298c6fc092c6149daeebfed8cf58681912d38 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 2 Jun 2023 16:18:58 -0400 Subject: [PATCH 27/66] Catch Google Sheets api error (#93979) --- .../components/google_sheets/__init__.py | 10 +++++- tests/components/google_sheets/test_init.py | 35 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index 803b737283b..590c7bd0c90 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -7,13 +7,18 @@ import aiohttp from google.auth.exceptions import RefreshError from google.oauth2.credentials import Credentials from gspread import Client +from gspread.exceptions import APIError from gspread.utils import ValueInputOption import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, @@ -93,6 +98,9 @@ async def async_setup_service(hass: HomeAssistant) -> None: except RefreshError as ex: entry.async_start_reauth(hass) raise ex + except APIError as ex: + raise HomeAssistantError("Failed to write data") from ex + worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title)) row_data = {"created": str(datetime.now())} | call.data[DATA] columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), []) diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index 50c82ac5109..8f7ce7603e8 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -6,7 +6,9 @@ import time from typing import Any from unittest.mock import patch +from gspread.exceptions import APIError import pytest +from requests.models import Response from homeassistant.components.application_credentials import ( ClientCredential, @@ -15,7 +17,7 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.google_sheets import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceNotFound +from homeassistant.exceptions import HomeAssistantError, ServiceNotFound from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -212,6 +214,37 @@ async def test_append_sheet( assert len(mock_client.mock_calls) == 8 +async def test_append_sheet_api_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test append to sheet service call API error.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + response = Response() + response.status_code = 503 + + with pytest.raises(HomeAssistantError), patch( + "homeassistant.components.google_sheets.Client.request", + side_effect=APIError(response), + ): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry.entry_id, + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) + + async def test_append_sheet_invalid_config_entry( hass: HomeAssistant, setup_integration: ComponentSetup, From bb2a89f0653c414aec885824b7c68df01bbecd2b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Jun 2023 23:35:41 -0400 Subject: [PATCH 28/66] Bumped version to 2023.6.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1ac0da081df..638a4afe037 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index ac39a138984..006692082b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b3" +version = "2023.6.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9aeba6221b01a0232b133ddc95ea86d1956a7247 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 3 Jun 2023 06:25:39 -0700 Subject: [PATCH 29/66] Fix error in tibber while fetching latest statistics (#93998) --- homeassistant/components/tibber/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index a2f1db7536f..242c2179a05 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -606,7 +606,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]): ) last_stats = await get_instance(self.hass).async_add_executor_job( - get_last_statistics, self.hass, 1, statistic_id, True, {} + get_last_statistics, self.hass, 1, statistic_id, True, set() ) if not last_stats: From 2a99fea1debe2f728db65f27fba562d109f80c76 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 1 Jun 2023 11:46:59 +0200 Subject: [PATCH 30/66] Add video id to youtube sensor state attributes (#93668) * Add video id to state attributes * Make extra state attributes not optional * Revert "Make extra state attributes not optional" This reverts commit d2f9e936c809dd50a5e4bbdaa181c9c9ddd3d217. --- homeassistant/components/youtube/sensor.py | 13 +++++++++++++ tests/components/youtube/test_sensor.py | 1 + 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index 6c75ef3bf8c..7f92ec0786a 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -18,6 +18,7 @@ from .const import ( ATTR_SUBSCRIBER_COUNT, ATTR_THUMBNAIL, ATTR_TITLE, + ATTR_VIDEO_ID, COORDINATOR, DOMAIN, ) @@ -30,6 +31,7 @@ class YouTubeMixin: value_fn: Callable[[Any], StateType] entity_picture_fn: Callable[[Any], str] + attributes_fn: Callable[[Any], dict[str, Any]] | None @dataclass @@ -44,6 +46,9 @@ SENSOR_TYPES = [ icon="mdi:youtube", value_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_TITLE], entity_picture_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_THUMBNAIL], + attributes_fn=lambda channel: { + ATTR_VIDEO_ID: channel[ATTR_LATEST_VIDEO][ATTR_VIDEO_ID] + }, ), YouTubeSensorEntityDescription( key="subscribers", @@ -52,6 +57,7 @@ SENSOR_TYPES = [ native_unit_of_measurement="subscribers", value_fn=lambda channel: channel[ATTR_SUBSCRIBER_COUNT], entity_picture_fn=lambda channel: channel[ATTR_ICON], + attributes_fn=None, ), ] @@ -84,3 +90,10 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): def entity_picture(self) -> str: """Return the value reported by the sensor.""" return self.entity_description.entity_picture_fn(self._channel) + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the extra state attributes.""" + if self.entity_description.attributes_fn: + return self.entity_description.attributes_fn(self._channel) + return None diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index f4dbd9cc3a5..1363a4468a7 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -25,6 +25,7 @@ async def test_sensor(hass: HomeAssistant, setup_integration: ComponentSetup) -> state.attributes["entity_picture"] == "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg" ) + assert state.attributes["video_id"] == "wysukDrMdqU" state = hass.states.get("sensor.google_for_developers_subscribers") assert state From 4f00cc9faa3c14a0c106ef276ccf7b0741deca93 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 3 Jun 2023 20:35:57 +0200 Subject: [PATCH 31/66] Show the sensor state using the coordinatordata instead of initial data (#94008) * Show the sensor state using the coordinatordata instead of initial data * Add test * Remove part --- homeassistant/components/youtube/entity.py | 17 +- homeassistant/components/youtube/sensor.py | 14 +- tests/components/youtube/__init__.py | 35 ++- .../youtube/fixtures/get_channel_2.json | 6 + .../fixtures/get_playlist_items_2.json | 215 ++++++++++++++++++ tests/components/youtube/test_sensor.py | 34 ++- 6 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 tests/components/youtube/fixtures/get_playlist_items_2.json diff --git a/homeassistant/components/youtube/entity.py b/homeassistant/components/youtube/entity.py index cdc2f98faac..2f9238dec26 100644 --- a/homeassistant/components/youtube/entity.py +++ b/homeassistant/components/youtube/entity.py @@ -1,9 +1,6 @@ """Entity representing a YouTube account.""" from __future__ import annotations -from typing import Any - -from homeassistant.const import ATTR_ID from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -21,20 +18,18 @@ class YouTubeChannelEntity(CoordinatorEntity): self, coordinator: YouTubeDataUpdateCoordinator, description: EntityDescription, - channel: dict[str, Any], + channel_id: str, ) -> None: - """Initialize a Google Mail entity.""" + """Initialize a YouTube entity.""" super().__init__(coordinator) self.entity_description = description self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{channel[ATTR_ID]}_{description.key}" + f"{coordinator.config_entry.entry_id}_{channel_id}_{description.key}" ) + self._channel_id = channel_id self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={ - (DOMAIN, f"{coordinator.config_entry.entry_id}_{channel[ATTR_ID]}") - }, + identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{channel_id}")}, manufacturer=MANUFACTURER, - name=channel[ATTR_TITLE], + name=coordinator.data[channel_id][ATTR_TITLE], ) - self._channel = channel diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index 7f92ec0786a..c605b960475 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -70,8 +70,8 @@ async def async_setup_entry( COORDINATOR ] async_add_entities( - YouTubeSensor(coordinator, sensor_type, channel) - for channel in coordinator.data.values() + YouTubeSensor(coordinator, sensor_type, channel_id) + for channel_id in coordinator.data for sensor_type in SENSOR_TYPES ) @@ -84,16 +84,20 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" - return self.entity_description.value_fn(self._channel) + return self.entity_description.value_fn(self.coordinator.data[self._channel_id]) @property def entity_picture(self) -> str: """Return the value reported by the sensor.""" - return self.entity_description.entity_picture_fn(self._channel) + return self.entity_description.entity_picture_fn( + self.coordinator.data[self._channel_id] + ) @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the extra state attributes.""" if self.entity_description.attributes_fn: - return self.entity_description.attributes_fn(self._channel) + return self.entity_description.attributes_fn( + self.coordinator.data[self._channel_id] + ) return None diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 289a5e8793f..391ff4b3a22 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -20,6 +20,10 @@ class MockRequest: class MockChannels: """Mock object for channels.""" + def __init__(self, fixture: str): + """Initialize mock channels.""" + self._fixture = fixture + def list( self, part: str, @@ -28,12 +32,16 @@ class MockChannels: maxResults: int | None = None, ) -> MockRequest: """Return a fixture.""" - return MockRequest(fixture="youtube/get_channel.json") + return MockRequest(fixture=self._fixture) class MockPlaylistItems: """Mock object for playlist items.""" + def __init__(self, fixture: str): + """Initialize mock playlist items.""" + self._fixture = fixture + def list( self, part: str, @@ -41,28 +49,43 @@ class MockPlaylistItems: maxResults: int | None = None, ) -> MockRequest: """Return a fixture.""" - return MockRequest(fixture="youtube/get_playlist_items.json") + return MockRequest(fixture=self._fixture) class MockSubscriptions: """Mock object for subscriptions.""" + def __init__(self, fixture: str): + """Initialize mock subscriptions.""" + self._fixture = fixture + def list(self, part: str, mine: bool, maxResults: int | None = None) -> MockRequest: """Return a fixture.""" - return MockRequest(fixture="youtube/get_subscriptions.json") + return MockRequest(fixture=self._fixture) class MockService: """Service which returns mock objects.""" + def __init__( + self, + channel_fixture: str = "youtube/get_channel.json", + playlist_items_fixture: str = "youtube/get_playlist_items.json", + subscriptions_fixture: str = "youtube/get_subscriptions.json", + ): + """Initialize mock service.""" + self._channel_fixture = channel_fixture + self._playlist_items_fixture = playlist_items_fixture + self._subscriptions_fixture = subscriptions_fixture + def channels(self) -> MockChannels: """Return a mock object.""" - return MockChannels() + return MockChannels(self._channel_fixture) def playlistItems(self) -> MockPlaylistItems: """Return a mock object.""" - return MockPlaylistItems() + return MockPlaylistItems(self._playlist_items_fixture) def subscriptions(self) -> MockSubscriptions: """Return a mock object.""" - return MockSubscriptions() + return MockSubscriptions(self._subscriptions_fixture) diff --git a/tests/components/youtube/fixtures/get_channel_2.json b/tests/components/youtube/fixtures/get_channel_2.json index 81da13f7d19..24e71ad91ab 100644 --- a/tests/components/youtube/fixtures/get_channel_2.json +++ b/tests/components/youtube/fixtures/get_channel_2.json @@ -36,6 +36,12 @@ "totalItemCount": 6178, "newItemCount": 0, "activityType": "all" + }, + "statistics": { + "viewCount": "214141263", + "subscriberCount": "2290000", + "hiddenSubscriberCount": false, + "videoCount": "5798" } } ] diff --git a/tests/components/youtube/fixtures/get_playlist_items_2.json b/tests/components/youtube/fixtures/get_playlist_items_2.json new file mode 100644 index 00000000000..2311d7219c2 --- /dev/null +++ b/tests/components/youtube/fixtures/get_playlist_items_2.json @@ -0,0 +1,215 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "nextPageToken": "EAAaBlBUOkNBVQ", + "items": [ + { + "kind": "youtube#playlistItem", + "etag": "pU0v49jXONlQfIJEX7ldINttRYM", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LmhsZUxsY0h3UUxN", + "snippet": { + "publishedAt": "2023-05-10T22:30:48Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "Google I/O 2023 Developer Keynote in 5 minutes", + "description": "Discover what’s new from Google, including top takeaways and highlights announced at Google I/O 2023. From deep investments in the largest mobile platform, to breakthroughs in AI, learn about the latest capabilities in mobile, web, Cloud, AI, and more. \n\nCatch the full Developer Keynote →https://goo.gle/dev-keynote-23 \nWatch all the Keynotes from Google I/O 2023→ https://goo.gle/IO23_keynotes\nWatch all the Google I/O 2023 Sessions → https://goo.gle/IO23_all \n\n0:00 - Welcome\n0:25 - MakerSuite\n0:49 - Android Studio Bot\n1:38 - Large screens\n2:04 - Wear OS\n2:34 - WebGPU\n2:58 - Baseline\n3:27 - MediaPipe\n3:57 - Duet AI for Google Cloud\n4:59 - Closing\n\nSubscribe to Google Developers → https://goo.gle/developers\n\n#GoogleIO #developers", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/hleLlcHwQLM/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/hleLlcHwQLM/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/hleLlcHwQLM/hqdefault.jpg", + "width": 480, + "height": 360 + }, + "standard": { + "url": "https://i.ytimg.com/vi/hleLlcHwQLM/sddefault.jpg", + "width": 640, + "height": 480 + }, + "maxres": { + "url": "https://i.ytimg.com/vi/hleLlcHwQLM/maxresdefault.jpg", + "width": 1280, + "height": 720 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 1, + "resourceId": { + "kind": "youtube#video", + "videoId": "hleLlcHwQLM" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "hleLlcHwQLM", + "videoPublishedAt": "2023-05-10T22:30:48Z" + } + }, + { + "kind": "youtube#playlistItem", + "etag": "fht9mKDuIBXcO75k21ZB_gC_4vM", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LmxNS2p0U0Z1amN3", + "snippet": { + "publishedAt": "2023-05-10T21:25:47Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Pay and Wallet in less than 1 minute", + "description": "A quick recap on the latest updates to Google Pay and Wallet from Google I/O 2023.\n\nTo learn more about what's new in Google Pay and Wallet, check out the keynote → https://goo.gle/IO23_paywallet\n\nSubscribe to Google Developers → https://goo.gle/developers\n\n#GoogleIO", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/lMKjtSFujcw/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/lMKjtSFujcw/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/lMKjtSFujcw/hqdefault.jpg", + "width": 480, + "height": 360 + }, + "standard": { + "url": "https://i.ytimg.com/vi/lMKjtSFujcw/sddefault.jpg", + "width": 640, + "height": 480 + }, + "maxres": { + "url": "https://i.ytimg.com/vi/lMKjtSFujcw/maxresdefault.jpg", + "width": 1280, + "height": 720 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 2, + "resourceId": { + "kind": "youtube#video", + "videoId": "lMKjtSFujcw" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "lMKjtSFujcw", + "videoPublishedAt": "2023-05-10T21:25:47Z" + } + }, + { + "kind": "youtube#playlistItem", + "etag": "nYKXoKd8eePAZ_xFa3dL5ZmvM5c", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LmMwbXFCdVhQcnBB", + "snippet": { + "publishedAt": "2023-05-10T20:47:57Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "Developers guide to BigQuery export for Google Analytics 4", + "description": "With Google Analytics 4 (GA4), anyone can set up export of granular measurement data to BigQuery.\n\nIn this session, you will learn how to use the BigQuery export for solving business problems, doing complex reporting, implementing advanced use cases with ML models, and creating custom audiences by joining with first-party data. You can use this framework for detailed or large-scale data analysis. We will also share some best practices to get you started.\n\nResources:\nDevelopers guide to BigQuery export for Google Analytics 4 → https://goo.gle/ga-io23\n\nSpeaker: Minhaz Kazi\n\nWatch more:\nWatch all the Technical Sessions from Google I/O 2023 → https://goo.gle/IO23_sessions\nWatch more Mobile Sessions → https://goo.gle/IO23_mobile\nWatch more Web Sessions → https://goo.gle/IO23_web\nAll Google I/O 2023 Sessions → https://goo.gle/IO23_all\n\nSubscribe to Google Developers → https://goo.gle/developers\n\n#GoogleIO", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/c0mqBuXPrpA/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/c0mqBuXPrpA/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/c0mqBuXPrpA/hqdefault.jpg", + "width": 480, + "height": 360 + }, + "standard": { + "url": "https://i.ytimg.com/vi/c0mqBuXPrpA/sddefault.jpg", + "width": 640, + "height": 480 + }, + "maxres": { + "url": "https://i.ytimg.com/vi/c0mqBuXPrpA/maxresdefault.jpg", + "width": 1280, + "height": 720 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 3, + "resourceId": { + "kind": "youtube#video", + "videoId": "c0mqBuXPrpA" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "c0mqBuXPrpA", + "videoPublishedAt": "2023-05-10T20:47:57Z" + } + }, + { + "kind": "youtube#playlistItem", + "etag": "--gb8pSHDwp9c-fyjhZ0K2DklLE", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Ll9uOXh3dVRPUmFz", + "snippet": { + "publishedAt": "2023-05-10T20:46:29Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Home - American Sign Language", + "description": "To watch this Session without American Sign Language (ASL) interpretation, please click here → https://goo.gle/IO23_homekey\n\nDiscover how your connected devices can do more with Google Home using Matter and Automations.\n\nResources:\nGoogle Home Developer Center → https://goo.gle/3KcD5xr\n\nDiscover how your connected devices can do more with Google Home using Matter and Automations\nGoogle Home APIs Developer Preview → https://goo.gle/3UakRl0\nAutomations Developer Preview → https://goo.gle/3KgEcMy\n\nSpeakers: Taylor Lehman, Indu Ramamurthi\n\nWatch more:\nWatch more Mobile Sessions → https://goo.gle/IO23_mobile\nAll Google I/O 2023 Sessions → https://goo.gle/IO23_all\n\nSubscribe to Google Developers → https://goo.gle/developers\n\n#GoogleIO", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/_n9xwuTORas/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/_n9xwuTORas/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/_n9xwuTORas/hqdefault.jpg", + "width": 480, + "height": 360 + }, + "standard": { + "url": "https://i.ytimg.com/vi/_n9xwuTORas/sddefault.jpg", + "width": 640, + "height": 480 + }, + "maxres": { + "url": "https://i.ytimg.com/vi/_n9xwuTORas/maxresdefault.jpg", + "width": 1280, + "height": 720 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 4, + "resourceId": { + "kind": "youtube#video", + "videoId": "_n9xwuTORas" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "_n9xwuTORas", + "videoPublishedAt": "2023-05-10T20:46:29Z" + } + } + ], + "pageInfo": { + "totalResults": 5798, + "resultsPerPage": 5 + } +} diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index 1363a4468a7..3462e291af8 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -9,9 +9,11 @@ from homeassistant.components.youtube import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from ...common import async_fire_time_changed +from . import MockService from .conftest import TOKEN, ComponentSetup +from tests.common import async_fire_time_changed + async def test_sensor(hass: HomeAssistant, setup_integration: ComponentSetup) -> None: """Test sensor.""" @@ -37,6 +39,36 @@ async def test_sensor(hass: HomeAssistant, setup_integration: ComponentSetup) -> ) +async def test_sensor_updating( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test updating sensor.""" + await setup_integration() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state + assert state.attributes["video_id"] == "wysukDrMdqU" + + with patch( + "homeassistant.components.youtube.api.build", + return_value=MockService( + playlist_items_fixture="youtube/get_playlist_items_2.json" + ), + ): + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state + assert state.name == "Google for Developers Latest upload" + assert state.state == "Google I/O 2023 Developer Keynote in 5 minutes" + assert ( + state.attributes["entity_picture"] + == "https://i.ytimg.com/vi/hleLlcHwQLM/sddefault.jpg" + ) + assert state.attributes["video_id"] == "hleLlcHwQLM" + + async def test_sensor_reauth_trigger( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: From aff4d537a7cba673f144b709945a43f4b9967ad7 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 5 Jun 2023 02:07:37 +0200 Subject: [PATCH 32/66] Bump xiaomi-ble to 0.17.2 (#94011) Bump xiaomi-ble Co-authored-by: J. Nick Koston --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/test_binary_sensor.py | 4 ++-- tests/components/xiaomi_ble/test_config_flow.py | 12 ++++++------ 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 4d5cddd9517..69a95ea8a9c 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -16,5 +16,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.17.0"] + "requirements": ["xiaomi-ble==0.17.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f3b05058c8f..67c3b34656d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2677,7 +2677,7 @@ wyoming==0.0.1 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.17.0 +xiaomi-ble==0.17.2 # homeassistant.components.knx xknx==2.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd35ab61341..f278b20011e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1947,7 +1947,7 @@ wyoming==0.0.1 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.17.0 +xiaomi-ble==0.17.2 # homeassistant.components.knx xknx==2.10.0 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 9345660f21c..235be5c6cd8 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -253,10 +253,10 @@ async def test_smoke(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") + smoke_sensor = hass.states.get("binary_sensor.smoke_detector_9cbc_smoke") smoke_sensor_attribtes = smoke_sensor.attributes assert smoke_sensor.state == STATE_ON - assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Smoke Detector 9CBC Smoke" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 3f537c2afa0..97aa878e1fb 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -248,7 +248,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" + assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -284,7 +284,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" + assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -320,7 +320,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" + assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -501,7 +501,7 @@ async def test_async_step_user_with_found_devices_v4_encryption( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" + assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -549,7 +549,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" + assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -599,7 +599,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" + assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" From 902bd521d275beecc1221a2b3d54a1d63348ba64 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 4 Jun 2023 01:49:18 -0700 Subject: [PATCH 33/66] Android TV Remote: Abort zeroconf if mac address is missing (#94026) Abort zeroconf if mac address is missing --- .../androidtv_remote/config_flow.py | 3 ++- .../androidtv_remote/test_config_flow.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py index 24b64c622a9..f7e1078d3fa 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = discovery_info.host self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.") self.mac = discovery_info.properties.get("bt") - assert self.mac + if not self.mac: + return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(format_mac(self.mac)) self._abort_if_unique_id_configured( updates={CONF_HOST: self.host, CONF_NAME: self.name} diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index ea1f4abfc1d..ec368081a95 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -712,6 +712,30 @@ async def test_zeroconf_flow_already_configured_host_not_changed_no_reload_entry assert len(mock_setup_entry.mock_calls) == 0 +async def test_zeroconf_flow_abort_if_mac_is_missing( + hass: HomeAssistant, +) -> None: + """Test when mac is missing in the zeroconf discovery we abort.""" + host = "1.2.3.4" + name = "My Android TV" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host=host, + addresses=[host], + port=6466, + hostname=host, + type="mock_type", + name=name + "._androidtvremote2._tcp.local.", + properties={}, + ), + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + async def test_reauth_flow_success( hass: HomeAssistant, mock_setup_entry: AsyncMock, From 5a63079c808e49b61025d9a91cebe59293a6f565 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Jun 2023 20:53:34 +0200 Subject: [PATCH 34/66] Remove update_before_add from binary_sensor in Command Line (#94040) Remove update_before_add --- homeassistant/components/command_line/binary_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 9c5a1ce1bbe..8abe401ec9c 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -107,7 +107,6 @@ async def async_setup_platform( scan_interval, ) ], - True, ) From 4a31cb0ad8dcdff7700815aebec805cec9d8d1b4 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Mon, 5 Jun 2023 02:06:38 +0200 Subject: [PATCH 35/66] Update pynuki to 1.6.2 (#94041) chore(component/nuki): update pynuki to 1.6.2 --- homeassistant/components/nuki/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 8b87816fb7d..b84bee660c1 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nuki", "iot_class": "local_polling", "loggers": ["pynuki"], - "requirements": ["pynuki==1.6.1"] + "requirements": ["pynuki==1.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67c3b34656d..f2038c111fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1831,7 +1831,7 @@ pynina==0.3.0 pynobo==1.6.0 # homeassistant.components.nuki -pynuki==1.6.1 +pynuki==1.6.2 # homeassistant.components.nut pynut2==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f278b20011e..44419f52828 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ pynina==0.3.0 pynobo==1.6.0 # homeassistant.components.nuki -pynuki==1.6.1 +pynuki==1.6.2 # homeassistant.components.nut pynut2==2.1.2 From 580065e946ff40375a93480b71ecc20632a212a0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 4 Jun 2023 18:35:17 -0400 Subject: [PATCH 36/66] Fix zwave_js.update entity restore logic (#94043) --- homeassistant/components/zwave_js/update.py | 25 ++++++++------ tests/components/zwave_js/test_update.py | 36 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 8403e28a68b..5b7c157552a 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -42,6 +42,7 @@ PARALLEL_UPDATES = 1 UPDATE_DELAY_STRING = "delay" UPDATE_DELAY_INTERVAL = 5 # In minutes +ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware" @dataclass @@ -53,7 +54,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): def as_dict(self) -> dict[str, Any]: """Return a dict representation of the extra data.""" return { - "latest_version_firmware": asdict(self.latest_version_firmware) + ATTR_LATEST_VERSION_FIRMWARE: asdict(self.latest_version_firmware) if self.latest_version_firmware else None } @@ -61,7 +62,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): @classmethod def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredData: """Initialize the extra data from a dict.""" - if not (firmware_dict := data["latest_version_firmware"]): + if not (firmware_dict := data[ATTR_LATEST_VERSION_FIRMWARE]): return cls(None) return cls(NodeFirmwareUpdateInfo.from_dict(firmware_dict)) @@ -326,20 +327,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) # If we have a complete previous state, use that to set the latest version - if (state := await self.async_get_last_state()) and ( - extra_data := await self.async_get_last_extra_data() + if ( + (state := await self.async_get_last_state()) + and (latest_version := state.attributes.get(ATTR_LATEST_VERSION)) + is not None + and (extra_data := await self.async_get_last_extra_data()) ): - self._attr_latest_version = state.attributes[ATTR_LATEST_VERSION] + self._attr_latest_version = latest_version self._latest_version_firmware = ( ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( extra_data.as_dict() ).latest_version_firmware ) - # If we have no state to restore, we can set the latest version to installed - # so that the entity starts as off. If we have partial restore data due to an - # upgrade to an HA version where this feature is released from one that is not - # the entity will start in an unknown state until we can correct on next update - elif not state: + # If we have no state or latest version to restore, we can set the latest + # version to installed so that the entity starts as off. If we have partial + # restore data due to an upgrade to an HA version where this feature is released + # from one that is not the entity will start in an unknown state until we can + # correct on next update + elif not state or not latest_version: self._attr_latest_version = self._attr_installed_version # Spread updates out in 5 minute increments to avoid flooding the network diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 1a783f06bea..6a8cbdd724a 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -778,6 +778,42 @@ async def test_update_entity_full_restore_data_no_update_available( assert state.attributes[ATTR_LATEST_VERSION] == "10.7" +async def test_update_entity_no_latest_version( + hass: HomeAssistant, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test entity with no `latest_version` attr restores state.""" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + UPDATE_ENTITY, + STATE_OFF, + { + ATTR_INSTALLED_VERSION: "10.7", + ATTR_LATEST_VERSION: None, + ATTR_SKIPPED_VERSION: None, + }, + ), + {"latest_version_firmware": None}, + ) + ], + ) + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] is None + assert state.attributes[ATTR_LATEST_VERSION] == "10.7" + + async def test_update_entity_unload_asleep_node( hass: HomeAssistant, client, wallmote_central_scene, integration ) -> None: From dbd5511e5e5f23bfcae9f45efdac236391389aca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Jun 2023 21:14:44 -0500 Subject: [PATCH 37/66] Bump zeroconf to 0.64.0 (#94052) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 442f3297467..85cf503bb0d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.63.0"] + "requirements": ["zeroconf==0.64.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 17efae77498..80e1a95ba9f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.63.0 +zeroconf==0.64.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index f2038c111fb..252db35f280 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2728,7 +2728,7 @@ zamg==0.2.2 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.63.0 +zeroconf==0.64.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44419f52828..b837e61dc47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1986,7 +1986,7 @@ youless-api==1.0.1 zamg==0.2.2 # homeassistant.components.zeroconf -zeroconf==0.63.0 +zeroconf==0.64.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 4bb6fec1d6ad7b893617db4a7d479785b8ea4dfe Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 5 Jun 2023 15:52:40 -0400 Subject: [PATCH 38/66] Don't add Roborock switches if it is not supported (#94069) * don't add switches if it is not supported * don't create entity unless if it is valid * Raise on other exceptions * rework valid_enties --- homeassistant/components/roborock/switch.py | 52 +++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 6971ff9d900..d8ff50430cb 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -1,9 +1,11 @@ """Support for Roborock switch.""" +import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging from typing import Any +from roborock.exceptions import RoborockException from roborock.roborock_typing import RoborockCommand from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -30,6 +32,8 @@ class RoborockSwitchDescriptionMixin: evaluate_value: Callable[[dict], bool] # Sets the status of the switch set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]] + # Check support of this feature + check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]] @dataclass @@ -45,6 +49,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0} ), get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS), + check_support=lambda data: data.api.send_command( + RoborockCommand.GET_CHILD_LOCK_STATUS + ), evaluate_value=lambda data: data["lock_status"] == 1, key="child_lock", translation_key="child_lock", @@ -56,6 +63,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0} ), get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS), + check_support=lambda data: data.api.send_command( + RoborockCommand.GET_FLOW_LED_STATUS + ), evaluate_value=lambda data: data["status"] == 1, key="status_indicator", translation_key="status_indicator", @@ -75,16 +85,38 @@ async def async_setup_entry( coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ config_entry.entry_id ] - async_add_entities( - ( - RoborockSwitchEntity( - f"{description.key}_{slugify(device_id)}", - coordinator, - description, - ) - for device_id, coordinator in coordinators.items() - for description in SWITCH_DESCRIPTIONS + possible_entities: list[ + tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription] + ] = [ + (device_id, coordinator, description) + for device_id, coordinator in coordinators.items() + for description in SWITCH_DESCRIPTIONS + ] + # We need to check if this function is supported by the device. + results = await asyncio.gather( + *( + description.check_support(coordinator) + for _, coordinator, description in possible_entities ), + return_exceptions=True, + ) + valid_entities: list[RoborockSwitchEntity] = [] + for posible_entity, result in zip(possible_entities, results): + if isinstance(result, Exception): + if not isinstance(result, RoborockException): + raise result + _LOGGER.debug("Not adding entity because of %s", result) + else: + valid_entities.append( + RoborockSwitchEntity( + f"{posible_entity[2].key}_{slugify(posible_entity[0])}", + posible_entity[1], + posible_entity[2], + result, + ) + ) + async_add_entities( + valid_entities, True, ) @@ -99,10 +131,12 @@ class RoborockSwitchEntity(RoborockEntity, SwitchEntity): unique_id: str, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockSwitchDescription, + initial_value: bool, ) -> None: """Create a switch entity.""" self.entity_description = entity_description super().__init__(unique_id, coordinator.device_info, coordinator.api) + self._attr_is_on = initial_value async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" From eb036af41066b874210691ccbfe8a795a2cd788c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 5 Jun 2023 12:49:01 -0500 Subject: [PATCH 39/66] Bump intents to 2023.6.5 (#94077) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 06666af815a..01276d56081 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.0.6", "home-assistant-intents==2023.5.30"] + "requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80e1a95ba9f..0e568b7d908 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ hass-nabucasa==0.67.1 hassil==1.0.6 home-assistant-bluetooth==1.10.0 home-assistant-frontend==20230601.1 -home-assistant-intents==2023.5.30 +home-assistant-intents==2023.6.5 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 252db35f280..7d3aa7eb86e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -927,7 +927,7 @@ holidays==0.21.13 home-assistant-frontend==20230601.1 # homeassistant.components.conversation -home-assistant-intents==2023.5.30 +home-assistant-intents==2023.6.5 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b837e61dc47..a843f7b9996 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -719,7 +719,7 @@ holidays==0.21.13 home-assistant-frontend==20230601.1 # homeassistant.components.conversation -home-assistant-intents==2023.5.30 +home-assistant-intents==2023.6.5 # homeassistant.components.home_connect homeconnect==0.7.2 From 28e0f5e104c7fbb6f5b0ea83945782cac66ea85f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Jun 2023 20:16:11 +0200 Subject: [PATCH 40/66] Update frontend to 20230605.0 (#94083) Co-authored-by: Paulus Schoutsen --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 838294f7ba5..b82ece33315 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230601.1"] + "requirements": ["home-assistant-frontend==20230605.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0e568b7d908..1fe097ed1ac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.1.0 hass-nabucasa==0.67.1 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230601.1 +home-assistant-frontend==20230605.0 home-assistant-intents==2023.6.5 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7d3aa7eb86e..0ed2cb08113 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230601.1 +home-assistant-frontend==20230605.0 # homeassistant.components.conversation home-assistant-intents==2023.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a843f7b9996..bc6bf1e9e72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -716,7 +716,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230601.1 +home-assistant-frontend==20230605.0 # homeassistant.components.conversation home-assistant-intents==2023.6.5 From ee8f63b9c95de984a5b271b247f22b2d38a66354 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Jun 2023 21:54:51 +0200 Subject: [PATCH 41/66] Fix reload service in Command Line (#94085) Fix multi platform reload service in command line --- homeassistant/components/command_line/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index c9c18fe54a8..906e28052da 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -173,7 +173,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: platforms: list[Platform] = [] for platform_config in command_line_config: for platform, _config in platform_config.items(): - platforms.append(PLATFORM_MAPPING[platform]) + if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms: + platforms.append(mapped_platform) _LOGGER.debug( "Loading config %s for platform %s", platform_config, From 7a6327d7e29984742cf4237bf05a93608277ea62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Jun 2023 16:13:07 -0400 Subject: [PATCH 42/66] Bumped version to 2023.6.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 638a4afe037..8b872c061c9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 006692082b5..29411420d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b4" +version = "2023.6.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2c43672a8ad676e6c9f3da4d5f2451c366ac52ab Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 1 Jun 2023 13:53:41 -0400 Subject: [PATCH 43/66] Include port info in the ZHA websocket settings response (#93934) --- homeassistant/components/zha/websocket_api.py | 2 ++ tests/components/zha/test_websocket_api.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 019a5c50238..28e115c0ec4 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeVar, cast import voluptuous as vol import zigpy.backups +from zigpy.config import CONF_DEVICE from zigpy.config.validators import cv_boolean from zigpy.types.named import EUI64 from zigpy.zcl.clusters.security import IasAce @@ -1136,6 +1137,7 @@ async def websocket_get_network_settings( msg[ID], { "radio_type": async_get_radio_type(hass, zha_gateway.config_entry).name, + "device": zha_gateway.application_controller.config[CONF_DEVICE], "settings": backup.as_dict(), }, ) diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 720cfaaac9b..5250b62a9b0 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -744,6 +744,7 @@ async def test_get_network_settings( assert msg["success"] assert "radio_type" in msg["result"] assert "network_info" in msg["result"]["settings"] + assert "path" in msg["result"]["device"] async def test_list_network_backups( From f373f1abd53c6a4471ff8c0fa45e1c36a5288976 Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 6 Jun 2023 02:25:25 -0400 Subject: [PATCH 44/66] Add missing translation keys for Roborock mop intensity (#94088) --- homeassistant/components/roborock/strings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 1a65b636dfc..00ebd3833a8 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -90,8 +90,11 @@ "name": "Mop intensity", "state": { "off": "Off", + "low": "Low", "mild": "Mild", + "medium": "Medium", "moderate": "Moderate", + "high": "High", "intense": "Intense", "custom": "Custom" } From e00012289d9a5b0bb8840226d99d512ad737e9f1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Jun 2023 09:08:17 +0200 Subject: [PATCH 45/66] Bump aiounifi to v48 - Fix fail to initialise due to board_rev not exist (#94093) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f43e3030916..f48191e471a 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==47"], + "requirements": ["aiounifi==48"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 0ed2cb08113..b42f634c506 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==47 +aiounifi==48 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc6bf1e9e72..0fd0fb8db9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -275,7 +275,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==47 +aiounifi==48 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From e6fcc6b73c04a567ea697bc9d09258290444b92d Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Tue, 6 Jun 2023 07:32:07 +0100 Subject: [PATCH 46/66] fix: Bump melnor-bluetooth to fix deadlock (#94098) --- homeassistant/components/melnor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index 87a5583fa4f..185899a9656 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", - "requirements": ["melnor-bluetooth==0.0.22"] + "requirements": ["melnor-bluetooth==0.0.24"] } diff --git a/requirements_all.txt b/requirements_all.txt index b42f634c506..ad022200384 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1122,7 +1122,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.22 +melnor-bluetooth==0.0.24 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fd0fb8db9f..f28a2b43ba5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.22 +melnor-bluetooth==0.0.24 # homeassistant.components.meteo_france meteofrance-api==1.2.0 From 49388eab3ae4affc4d8a2d6915a7d46997f0cddb Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 6 Jun 2023 21:24:36 -0400 Subject: [PATCH 47/66] Add diagnostics to Roborock (#94099) * Add diagnostics * Update homeassistant/components/roborock/models.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * adds snapshot --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/roborock/diagnostics.py | 38 +++ homeassistant/components/roborock/models.py | 10 + tests/components/roborock/conftest.py | 5 +- tests/components/roborock/mock_data.py | 4 + .../roborock/snapshots/test_diagnostics.ambr | 303 ++++++++++++++++++ tests/components/roborock/test_diagnostics.py | 23 ++ 6 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/roborock/diagnostics.py create mode 100644 tests/components/roborock/snapshots/test_diagnostics.ambr create mode 100644 tests/components/roborock/test_diagnostics.py diff --git a/homeassistant/components/roborock/diagnostics.py b/homeassistant/components/roborock/diagnostics.py new file mode 100644 index 00000000000..e5fcc834267 --- /dev/null +++ b/homeassistant/components/roborock/diagnostics.py @@ -0,0 +1,38 @@ +"""Support for the Airzone diagnostics.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics.util import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator + +TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"] + +TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ + config_entry.entry_id + ] + + return { + "config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG), + "coordinators": { + f"**REDACTED-{i}**": { + "roborock_device_info": async_redact_data( + coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD + ), + "api": coordinator.api.diagnostic_data, + } + for i, coordinator in enumerate(coordinators.values()) + }, + } diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index a30c84ce1da..c1d32df2d6d 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -1,5 +1,6 @@ """Roborock Models.""" from dataclasses import dataclass +from typing import Any from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.roborock_typing import DeviceProp @@ -13,3 +14,12 @@ class RoborockHassDeviceInfo: network_info: NetworkInfo product: HomeDataProduct props: DeviceProp + + def as_dict(self) -> dict[str, dict[str, Any]]: + """Turn RoborockHassDeviceInfo into a dictionary.""" + return { + "device": self.device.as_dict(), + "network_info": self.network_info.as_dict(), + "product": self.product.as_dict(), + "props": self.props.as_dict(), + } diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index b311f84f94a..d9c11bead74 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .mock_data import BASE_URL, HOME_DATA, PROP, USER_DATA, USER_EMAIL +from .mock_data import BASE_URL, HOME_DATA, NETWORK_INFO, PROP, USER_DATA, USER_EMAIL from tests.common import MockConfigEntry @@ -54,7 +54,8 @@ async def setup_entry( "homeassistant.components.roborock.RoborockApiClient.get_home_data", return_value=HOME_DATA, ), patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking" + "homeassistant.components.roborock.RoborockMqttClient.get_networking", + return_value=NETWORK_INFO, ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", return_value=PROP, diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 15e69cee9d9..8155c10fdbd 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1,6 +1,8 @@ """Mock data for Roborock tests.""" from __future__ import annotations +import datetime + from roborock.containers import ( CleanRecord, CleanSummary, @@ -320,6 +322,8 @@ DND_TIMER = DnDTimer.from_dict( "enabled": 1, } ) +DND_TIMER.start_time = datetime.datetime(year=2023, month=6, day=1, hour=22) +DND_TIMER.end_time = datetime.datetime(year=2023, month=6, day=2, hour=7) STATUS = S7Status.from_dict( { diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..5cb9b109368 --- /dev/null +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -0,0 +1,303 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry': dict({ + 'base_url': 'https://usiot.roborock.com', + 'user_data': dict({ + 'avatarurl': 'https://files.roborock.com/iottest/default_avatar.png', + 'country': 'US', + 'countrycode': '1', + 'nickname': 'user_nickname', + 'region': 'us', + 'rriot': dict({ + 'h': 'abc123', + 'k': 'abc123', + 'r': dict({ + 'a': 'https://api-us.roborock.com', + 'l': 'https://wood-us.roborock.com', + 'm': 'ssl://mqtt-us-2.roborock.com:8883', + 'r': 'US', + }), + 's': 'abc123', + 'u': 'abc123', + }), + 'rruid': '**REDACTED**', + 'token': '**REDACTED**', + 'tokentype': '', + 'tuyaDeviceState': 2, + 'uid': '**REDACTED**', + }), + 'username': '**REDACTED**', + }), + 'coordinators': dict({ + '**REDACTED-0**': dict({ + 'api': dict({ + }), + 'roborock_device_info': dict({ + 'device': dict({ + 'activeTime': 1672364449, + 'deviceStatus': dict({ + '120': 0, + '121': 8, + '122': 100, + '123': 102, + '124': 203, + '125': 94, + '126': 90, + '127': 87, + '128': 0, + '133': 1, + }), + 'duid': '**REDACTED**', + 'extra': '{"RRPhotoPrivacyVersion": "1"}', + 'featureSet': '2234201184108543', + 'fv': '02.56.02', + 'iconUrl': '', + 'localKey': '**REDACTED**', + 'name': 'Roborock S7 MaxV', + 'newFeatureSet': '0000000000002041', + 'online': True, + 'productId': 'abc123', + 'pv': '1.0', + 'roomId': 2362003, + 'share': False, + 'silentOtaSwitch': True, + 'sn': 'abc123', + 'timeZoneId': 'America/Los_Angeles', + 'tuyaMigrated': False, + }), + 'network_info': dict({ + 'bssid': '**REDACTED**', + 'ip': '123.232.12.1', + 'mac': '**REDACTED**', + 'rssi': 90, + 'ssid': 'wifi', + }), + 'product': dict({ + 'capability': 0, + 'category': 'robot.vacuum.cleaner', + 'code': 'a27', + 'id': 'abc123', + 'model': 'roborock.vacuum.a27', + 'name': 'Roborock S7 MaxV', + 'schema': list([ + dict({ + 'code': 'rpc_request', + 'id': '101', + 'mode': 'rw', + 'name': 'rpc_request', + 'type': 'RAW', + }), + dict({ + 'code': 'rpc_response', + 'id': '102', + 'mode': 'rw', + 'name': 'rpc_response', + 'type': 'RAW', + }), + dict({ + 'code': 'error_code', + 'id': '120', + 'mode': 'ro', + 'name': '错误代码', + 'type': 'ENUM', + }), + dict({ + 'code': 'state', + 'id': '121', + 'mode': 'ro', + 'name': '设备状态', + 'type': 'ENUM', + }), + dict({ + 'code': 'battery', + 'id': '122', + 'mode': 'ro', + 'name': '设备电量', + 'type': 'ENUM', + }), + dict({ + 'code': 'fan_power', + 'id': '123', + 'mode': 'rw', + 'name': '清扫模式', + 'type': 'ENUM', + }), + dict({ + 'code': 'water_box_mode', + 'id': '124', + 'mode': 'rw', + 'name': '拖地模式', + 'type': 'ENUM', + }), + dict({ + 'code': 'main_brush_life', + 'id': '125', + 'mode': 'rw', + 'name': '主刷寿命', + 'type': 'VALUE', + }), + dict({ + 'code': 'side_brush_life', + 'id': '126', + 'mode': 'rw', + 'name': '边刷寿命', + 'type': 'VALUE', + }), + dict({ + 'code': 'filter_life', + 'id': '127', + 'mode': 'rw', + 'name': '滤网寿命', + 'type': 'VALUE', + }), + dict({ + 'code': 'additional_props', + 'id': '128', + 'mode': 'ro', + 'name': '额外状态', + 'type': 'RAW', + }), + dict({ + 'code': 'task_complete', + 'id': '130', + 'mode': 'ro', + 'name': '完成事件', + 'type': 'RAW', + }), + dict({ + 'code': 'task_cancel_low_power', + 'id': '131', + 'mode': 'ro', + 'name': '电量不足任务取消', + 'type': 'RAW', + }), + dict({ + 'code': 'task_cancel_in_motion', + 'id': '132', + 'mode': 'ro', + 'name': '运动中任务取消', + 'type': 'RAW', + }), + dict({ + 'code': 'charge_status', + 'id': '133', + 'mode': 'ro', + 'name': '充电状态', + 'type': 'RAW', + }), + dict({ + 'code': 'drying_status', + 'id': '134', + 'mode': 'ro', + 'name': '烘干状态', + 'type': 'RAW', + }), + ]), + }), + 'props': dict({ + 'cleanSummary': dict({ + 'cleanArea': 1159182500, + 'cleanCount': 31, + 'cleanTime': 74382, + 'dustCollectionCount': 25, + 'records': list([ + 1672543330, + 1672458041, + ]), + 'squareMeterCleanArea': 1159.2, + }), + 'consumable': dict({ + 'cleaningBrushWorkTimes': 65, + 'dustCollectionWorkTimes': 25, + 'filterElementWorkTime': 0, + 'filterTimeLeft': 465618, + 'filterWorkTime': 74382, + 'mainBrushTimeLeft': 1005618, + 'mainBrushWorkTime': 74382, + 'sensorDirtyTime': 74382, + 'sensorTimeLeft': 33618, + 'sideBrushTimeLeft': 645618, + 'sideBrushWorkTime': 74382, + 'strainerWorkTimes': 65, + }), + 'dndTimer': dict({ + 'enabled': 1, + 'endHour': 7, + 'endMinute': 0, + 'endTime': '2023-06-02T07:00:00', + 'startHour': 22, + 'startMinute': 0, + 'startTime': '2023-06-01T22:00:00', + }), + 'lastCleanRecord': dict({ + 'area': 20965000, + 'avoidCount': 19, + 'begin': 1672543330, + 'cleanType': 3, + 'complete': 1, + 'duration': 1176, + 'dustCollectionStatus': 1, + 'end': 1672544638, + 'error': 0, + 'finishReason': 56, + 'mapFlag': 0, + 'squareMeterArea': 21.0, + 'startType': 2, + 'washCount': 2, + }), + 'status': dict({ + 'adbumperStatus': list([ + 0, + 0, + 0, + ]), + 'autoDustCollection': 1, + 'avoidCount': 19, + 'backType': -1, + 'battery': 100, + 'cameraStatus': 3457, + 'chargeStatus': 1, + 'cleanArea': 20965000, + 'cleanTime': 1176, + 'collisionAvoidStatus': 1, + 'debugMode': 0, + 'dndEnabled': 0, + 'dockErrorStatus': 0, + 'dockType': 3, + 'dustCollectionStatus': 0, + 'errorCode': 0, + 'fanPower': 102, + 'homeSecEnablePassword': 0, + 'homeSecStatus': 0, + 'inCleaning': 0, + 'inFreshState': 1, + 'inReturning': 0, + 'isExploring': 0, + 'isLocating': 0, + 'labStatus': 1, + 'lockStatus': 0, + 'mapPresent': 1, + 'mapStatus': 3, + 'mopForbiddenEnable': 1, + 'mopMode': 300, + 'msgSeq': 458, + 'msgVer': 2, + 'squareMeterCleanArea': 21.0, + 'state': 8, + 'switchMapMode': 0, + 'unsaveMapFlag': 0, + 'unsaveMapReason': 0, + 'washPhase': 0, + 'washReady': 0, + 'waterBoxCarriageStatus': 1, + 'waterBoxMode': 203, + 'waterBoxStatus': 1, + 'waterShortageStatus': 0, + }), + }), + }), + }), + }), + }) +# --- diff --git a/tests/components/roborock/test_diagnostics.py b/tests/components/roborock/test_diagnostics.py new file mode 100644 index 00000000000..a10cbcf057e --- /dev/null +++ b/tests/components/roborock/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Tests for the diagnostics data provided by the Roborock integration.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + bypass_api_fixture, + setup_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics for config entry.""" + result = await get_diagnostics_for_config_entry(hass, hass_client, setup_entry) + + assert isinstance(result, dict) + assert result == snapshot From 7a658117bb5dd20762d86ccaa1db00e42b169789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 6 Jun 2023 08:23:48 +0200 Subject: [PATCH 48/66] Update aioairzone to v0.6.3 and fix issue with latest firmware update (#94100) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_climate.py | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 9fbdd95518e..637066629db 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.6.1"] + "requirements": ["aioairzone==0.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ad022200384..20417e19ead 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -119,7 +119,7 @@ aioairq==0.2.4 aioairzone-cloud==0.1.7 # homeassistant.components.airzone -aioairzone==0.6.1 +aioairzone==0.6.3 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f28a2b43ba5..39c7507aa97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioairq==0.2.4 aioairzone-cloud==0.1.7 # homeassistant.components.airzone -aioairzone==0.6.1 +aioairzone==0.6.3 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index f51dd318240..2c66adcb974 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -1,14 +1,12 @@ """The climate tests for the Airzone platform.""" from unittest.mock import patch -from aioairzone.common import OperationMode from aioairzone.const import ( API_COOL_SET_POINT, API_DATA, API_HEAT_SET_POINT, API_MAX_TEMP, API_MIN_TEMP, - API_MODE, API_ON, API_SET_POINT, API_SPEED, @@ -336,7 +334,6 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: { API_SYSTEM_ID: 1, API_ZONE_ID: 1, - API_MODE: OperationMode.COOLING.value, API_ON: 1, } ] From 3e239962472e9f0186f5fe0c80b614c689058130 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 4 Jun 2023 20:02:13 -0400 Subject: [PATCH 49/66] Bump Roborock to 0.21.0 (#94035) bump to 21.0 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 44a4cba89c9..41e4a359e2e 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.17.0"] + "requirements": ["python-roborock==0.21.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 20417e19ead..3386be032bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2121,7 +2121,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.17.0 +python-roborock==0.21.0 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39c7507aa97..fc89c8f82ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1541,7 +1541,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.2 # homeassistant.components.roborock -python-roborock==0.17.0 +python-roborock==0.21.0 # homeassistant.components.smarttub python-smarttub==0.0.33 From 286de1f051bfd85487a129a5883dd41210b8624c Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 6 Jun 2023 20:42:59 -0400 Subject: [PATCH 50/66] Bump python-roborock to 23.4 (#94111) * bump to 23.0 * bump to 23.4 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 41e4a359e2e..0cd437278cf 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.21.0"] + "requirements": ["python-roborock==0.23.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3386be032bf..0d833845c22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2121,7 +2121,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.21.0 +python-roborock==0.23.4 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc89c8f82ad..00c2152a648 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1541,7 +1541,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.2 # homeassistant.components.roborock -python-roborock==0.21.0 +python-roborock==0.23.4 # homeassistant.components.smarttub python-smarttub==0.0.33 From 0e50baf0072d4447afaa88ff173936bb5ea427a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jun 2023 19:42:11 -0500 Subject: [PATCH 51/66] Verify persistant notifications can be dismissed by the id they are created with (#94112) --- .../persistent_notification/test_init.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 7fd8b00d3e1..1e083814e0e 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -212,3 +212,27 @@ async def test_ws_get_subscribe( assert msg["event"] event = msg["event"] assert event["type"] == "removed" + + +async def test_manual_notification_id_round_trip(hass: HomeAssistant) -> None: + """Test that a manual notification id can be round tripped.""" + notifications = pn._async_get_or_create_notifications(hass) + assert len(notifications) == 0 + + await hass.services.async_call( + pn.DOMAIN, + "create", + {"notification_id": "synology_diskstation_hub_notification", "message": "test"}, + blocking=True, + ) + + assert len(notifications) == 1 + + await hass.services.async_call( + pn.DOMAIN, + "dismiss", + {"notification_id": "synology_diskstation_hub_notification"}, + blocking=True, + ) + + assert len(notifications) == 0 From 2b39550e5549f32413db57b3fabeef0c761ea170 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Jun 2023 02:04:22 +0200 Subject: [PATCH 52/66] Update frontend to 20230606.0 (#94119) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b82ece33315..47bdf60b8e7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230605.0"] + "requirements": ["home-assistant-frontend==20230606.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1fe097ed1ac..f02f852aa32 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.1.0 hass-nabucasa==0.67.1 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230605.0 +home-assistant-frontend==20230606.0 home-assistant-intents==2023.6.5 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0d833845c22..10fa122d30c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230605.0 +home-assistant-frontend==20230606.0 # homeassistant.components.conversation home-assistant-intents==2023.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00c2152a648..0eac0f7fbce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -716,7 +716,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230605.0 +home-assistant-frontend==20230606.0 # homeassistant.components.conversation home-assistant-intents==2023.6.5 From 6a573b507e0d55d0c32556411cb9b4262cf9bdb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jun 2023 19:40:32 -0500 Subject: [PATCH 53/66] Remove `mark_read` service from persistent_notification (#94122) * Remove mark_read from persistent_notification Nothing on the frontend uses this, and the service is not documented There is not much point in keeping this as the notifications are no longer stored in the state machine * adjust * adjust --- .../persistent_notification/__init__.py | 32 ---------- .../persistent_notification/services.yaml | 12 ---- .../persistent_notification/test_init.py | 61 ------------------- 3 files changed, 105 deletions(-) diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 81a9bc9de4d..fe8849c7788 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -29,8 +29,6 @@ ATTR_NOTIFICATION_ID: Final = "notification_id" ATTR_TITLE: Final = "title" ATTR_STATUS: Final = "status" -STATUS_UNREAD = "unread" -STATUS_READ = "read" # Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9 EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated" @@ -43,7 +41,6 @@ class Notification(TypedDict): message: str notification_id: str title: str | None - status: str class UpdateType(StrEnum): @@ -98,7 +95,6 @@ def async_create( notifications[notification_id] = { ATTR_MESSAGE: message, ATTR_NOTIFICATION_ID: notification_id, - ATTR_STATUS: STATUS_UNREAD, ATTR_TITLE: title, ATTR_CREATED_AT: dt_util.utcnow(), } @@ -135,7 +131,6 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the persistent notification component.""" - notifications = _async_get_or_create_notifications(hass) @callback def create_service(call: ServiceCall) -> None: @@ -152,29 +147,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle the dismiss notification service call.""" async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID]) - @callback - def mark_read_service(call: ServiceCall) -> None: - """Handle the mark_read notification service call.""" - notification_id = call.data.get(ATTR_NOTIFICATION_ID) - if notification_id not in notifications: - _LOGGER.error( - ( - "Marking persistent_notification read failed: " - "Notification ID %s not found" - ), - notification_id, - ) - return - - notification = notifications[notification_id] - notification[ATTR_STATUS] = STATUS_READ - async_dispatcher_send( - hass, - SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED, - UpdateType.UPDATED, - {notification_id: notification}, - ) - hass.services.async_register( DOMAIN, "create", @@ -192,10 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION ) - hass.services.async_register( - DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION - ) - websocket_api.async_register_command(hass, websocket_get_notifications) websocket_api.async_register_command(hass, websocket_subscribe_notifications) diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 5695a3c3b82..60dbf5c864a 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -33,15 +33,3 @@ dismiss: example: 1234 selector: text: - -mark_read: - name: Mark read - description: Mark a notification read. - fields: - notification_id: - name: Notification ID - description: Target ID of the notification, which should be mark read. - required: true - example: 1234 - selector: - text: diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 1e083814e0e..4f0851dc477 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -25,7 +25,6 @@ async def test_create(hass: HomeAssistant) -> None: assert len(notifications) == 1 notification = notifications[list(notifications)[0]] - assert notification["status"] == pn.STATUS_UNREAD assert notification["message"] == "Hello World 2" assert notification["title"] == "2 beers" assert notification["created_at"] is not None @@ -66,39 +65,6 @@ async def test_dismiss_notification(hass: HomeAssistant) -> None: assert len(notifications) == 0 -async def test_mark_read(hass: HomeAssistant) -> None: - """Ensure notification is marked as Read.""" - notifications = pn._async_get_or_create_notifications(hass) - assert len(notifications) == 0 - - await hass.services.async_call( - pn.DOMAIN, - "create", - {"notification_id": "Beer 2", "message": "test"}, - blocking=True, - ) - - assert len(notifications) == 1 - notification = notifications[list(notifications)[0]] - assert notification["status"] == pn.STATUS_UNREAD - - await hass.services.async_call( - pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True - ) - - assert len(notifications) == 1 - notification = notifications[list(notifications)[0]] - assert notification["status"] == pn.STATUS_READ - - await hass.services.async_call( - pn.DOMAIN, - "dismiss", - {"notification_id": "Beer 2"}, - blocking=True, - ) - assert len(notifications) == 0 - - async def test_ws_get_notifications( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -128,19 +94,8 @@ async def test_ws_get_notifications( assert notification["notification_id"] == "Beer 2" assert notification["message"] == "test" assert notification["title"] is None - assert notification["status"] == pn.STATUS_UNREAD assert notification["created_at"] is not None - # Mark Read - await hass.services.async_call( - pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"} - ) - await client.send_json({"id": 7, "type": "persistent_notification/get"}) - msg = await client.receive_json() - notifications = msg["result"] - assert len(notifications) == 1 - assert notifications[0]["status"] == pn.STATUS_READ - # Dismiss pn.async_dismiss(hass, "Beer 2") await client.send_json({"id": 8, "type": "persistent_notification/get"}) @@ -186,24 +141,8 @@ async def test_ws_get_subscribe( assert notification["notification_id"] == "Beer 2" assert notification["message"] == "test" assert notification["title"] is None - assert notification["status"] == pn.STATUS_UNREAD assert notification["created_at"] is not None - # Mark Read - await hass.services.async_call( - pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"} - ) - msg = await client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == "event" - assert msg["event"] - event = msg["event"] - assert event["type"] == "updated" - notifications = event["notifications"] - assert len(notifications) == 1 - notification = notifications[list(notifications)[0]] - assert notification["status"] == pn.STATUS_READ - # Dismiss pn.async_dismiss(hass, "Beer 2") msg = await client.receive_json() From 26e08abb9a1fb3799503a3f61f44c8f4174814d9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:44:29 -0400 Subject: [PATCH 54/66] Revert "Increase Zigbee command retries (#93877)" (#94123) --- .../zha/core/cluster_handlers/__init__.py | 4 - .../zha/test_alarm_control_panel.py | 5 - tests/components/zha/test_device_action.py | 4 +- tests/components/zha/test_discover.py | 2 +- tests/components/zha/test_light.py | 98 ++++++++----------- tests/components/zha/test_switch.py | 4 +- 6 files changed, 46 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index ec29e4e53eb..7863b043455 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -45,8 +45,6 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -DEFAULT_REQUEST_RETRIES = 3 - class AttrReportConfig(TypedDict, total=True): """Configuration to report for the attributes.""" @@ -80,8 +78,6 @@ def decorate_command(cluster_handler, command): @wraps(command) async def wrapper(*args, **kwds): - kwds.setdefault("tries", DEFAULT_REQUEST_RETRIES) - try: result = await command(*args, **kwds) cluster_handler.debug( diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 34ce746e128..319301cf7dc 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -96,7 +96,6 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, - tries=3, ) # disarm from HA @@ -135,7 +134,6 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.Emergency, - tries=3, ) # reset the panel @@ -159,7 +157,6 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, - tries=3, ) # arm_night from HA @@ -180,7 +177,6 @@ async def test_alarm_control_panel( 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, - tries=3, ) # reset the panel @@ -278,6 +274,5 @@ async def reset_alarm_panel(hass, cluster, entity_id): 0, security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, - tries=3, ) cluster.client_command.reset_mock() diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 9d9a4bc2a54..f1ab44f69eb 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -328,7 +328,7 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None: 5, expect_reply=False, manufacturer=4151, - tries=3, + tries=1, tsn=None, ) in cluster.request.call_args_list @@ -345,7 +345,7 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None: 5, expect_reply=False, manufacturer=4151, - tries=3, + tries=1, tsn=None, ) in cluster.request.call_args_list diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index a87d624ec00..236a3c4ad86 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -131,7 +131,7 @@ async def test_devices( ), expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) ] diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 5ea71573a27..c4751f7e7f6 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -553,7 +553,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -589,7 +589,7 @@ async def test_transitions( transition_time=35, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -600,7 +600,7 @@ async def test_transitions( transition_time=35, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -637,7 +637,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -674,7 +674,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -685,7 +685,7 @@ async def test_transitions( transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_level.request.call_args_list[1] == call( @@ -696,7 +696,7 @@ async def test_transitions( transition_time=10, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -758,7 +758,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -769,7 +769,7 @@ async def test_transitions( transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_level.request.call_args_list[1] == call( @@ -780,7 +780,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -838,7 +838,7 @@ async def test_transitions( dev1_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -850,7 +850,7 @@ async def test_transitions( transition_time=0, # no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -910,7 +910,7 @@ async def test_transitions( transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -968,7 +968,7 @@ async def test_transitions( transition_time=1, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev2_cluster_color.request.call_args == call( @@ -979,7 +979,7 @@ async def test_transitions( transition_time=1, # sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev2_cluster_level.request.call_args_list[1] == call( @@ -990,7 +990,7 @@ async def test_transitions( transition_time=10, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -1121,7 +1121,7 @@ async def test_transitions( transition_time=20, # transition time expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -1151,7 +1151,7 @@ async def test_transitions( transition_time=1, # transition time - sengled light uses default minimum expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -1184,7 +1184,7 @@ async def test_transitions( eWeLink_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -1195,7 +1195,7 @@ async def test_transitions( transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -1261,7 +1261,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: dev1_cluster_on_off.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -1272,7 +1272,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -1319,7 +1319,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_color.request.call_args == call( @@ -1330,7 +1330,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) assert dev1_cluster_level.request.call_args_list[1] == call( @@ -1341,7 +1341,7 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: transition_time=0, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -1373,9 +1373,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): assert hass.states.get(entity_id).state == STATE_ON -async def async_test_on_off_from_hass( - hass, cluster, entity_id, expected_tries: int = 3 -): +async def async_test_on_off_from_hass(hass, cluster, entity_id): """Test on off functionality from hass.""" # turn on via UI cluster.request.reset_mock() @@ -1390,16 +1388,14 @@ async def async_test_on_off_from_hass( cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) - await async_test_off_from_hass( - hass, cluster, entity_id, expected_tries=expected_tries - ) + await async_test_off_from_hass(hass, cluster, entity_id) -async def async_test_off_from_hass(hass, cluster, entity_id, expected_tries: int = 3): +async def async_test_off_from_hass(hass, cluster, entity_id): """Test turning off the light from Home Assistant.""" # turn off via UI @@ -1415,18 +1411,13 @@ async def async_test_off_from_hass(hass, cluster, entity_id, expected_tries: int cluster.commands_by_name["off"].schema, expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) async def async_test_level_on_off_from_hass( - hass, - on_off_cluster, - level_cluster, - entity_id, - expected_default_transition: int = 0, - expected_tries: int = 3, + hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 ): """Test on off functionality from hass.""" @@ -1448,7 +1439,7 @@ async def async_test_level_on_off_from_hass( on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) on_off_cluster.request.reset_mock() @@ -1472,7 +1463,7 @@ async def async_test_level_on_off_from_hass( on_off_cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) assert level_cluster.request.call_args == call( @@ -1483,7 +1474,7 @@ async def async_test_level_on_off_from_hass( transition_time=100, expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) on_off_cluster.request.reset_mock() @@ -1508,15 +1499,13 @@ async def async_test_level_on_off_from_hass( transition_time=int(expected_default_transition), expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() - await async_test_off_from_hass( - hass, on_off_cluster, entity_id, expected_tries=expected_tries - ) + await async_test_off_from_hass(hass, on_off_cluster, entity_id) async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): @@ -1533,9 +1522,7 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected assert hass.states.get(entity_id).attributes.get("brightness") == level -async def async_test_flash_from_hass( - hass, cluster, entity_id, flash, expected_tries: int = 3 -): +async def async_test_flash_from_hass(hass, cluster, entity_id, flash): """Test flash functionality from hass.""" # turn on via UI cluster.request.reset_mock() @@ -1555,7 +1542,7 @@ async def async_test_flash_from_hass( effect_variant=general.Identify.EffectVariant.Default, expect_reply=True, manufacturer=None, - tries=expected_tries, + tries=1, tsn=None, ) @@ -1655,15 +1642,13 @@ async def test_zha_group_light_entity( assert "color_mode" not in group_state.attributes # test turning the lights on and off from the HA - await async_test_on_off_from_hass( - hass, group_cluster_on_off, group_entity_id, expected_tries=1 - ) + await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) await async_shift_time(hass) # test short flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, group_entity_id, FLASH_SHORT, expected_tries=1 + hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) await async_shift_time(hass) @@ -1678,7 +1663,6 @@ async def test_zha_group_light_entity( group_cluster_level, group_entity_id, expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition - expected_tries=1, ) await async_shift_time(hass) @@ -1699,7 +1683,7 @@ async def test_zha_group_light_entity( # test long flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, group_entity_id, FLASH_LONG, expected_tries=1 + hass, group_cluster_identify, group_entity_id, FLASH_LONG ) await async_shift_time(hass) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 8fb7825a953..9f98acb9359 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -176,7 +176,7 @@ async def test_switch( cluster.commands_by_name["on"].schema, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) @@ -196,7 +196,7 @@ async def test_switch( cluster.commands_by_name["off"].schema, expect_reply=True, manufacturer=None, - tries=3, + tries=1, tsn=None, ) From 93d52d88351bea85b046fecec22e5469111c2da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Tue, 6 Jun 2023 13:42:49 -0400 Subject: [PATCH 55/66] Bump aiobafi6 to 0.8.2 (#94125) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index b5b5b76967e..37fd5cee7c6 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", "iot_class": "local_push", - "requirements": ["aiobafi6==0.8.0"], + "requirements": ["aiobafi6==0.8.2"], "zeroconf": [ { "type": "_api._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 10fa122d30c..866a5bb8c99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -134,7 +134,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.8.0 +aiobafi6==0.8.2 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0eac0f7fbce..660e296d30d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -124,7 +124,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.8.0 +aiobafi6==0.8.2 # homeassistant.components.aws aiobotocore==2.1.0 From e6638ca3563c80409a6d6ef356854b68467c20bb Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 7 Jun 2023 03:04:53 +0300 Subject: [PATCH 56/66] Remove goalfeed integration (#94129) --- .coveragerc | 1 - homeassistant/components/goalfeed/__init__.py | 65 ------------------- .../components/goalfeed/manifest.json | 9 --- homeassistant/generated/integrations.json | 6 -- requirements_all.txt | 3 - 5 files changed, 84 deletions(-) delete mode 100644 homeassistant/components/goalfeed/__init__.py delete mode 100644 homeassistant/components/goalfeed/manifest.json diff --git a/.coveragerc b/.coveragerc index c244617007f..8494ef357bf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -420,7 +420,6 @@ omit = homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py homeassistant/components/glances/sensor.py - homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py homeassistant/components/goodwe/button.py homeassistant/components/goodwe/coordinator.py diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py deleted file mode 100644 index f452b858e79..00000000000 --- a/homeassistant/components/goalfeed/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Component for the Goalfeed service.""" -import json - -import pysher -import requests -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType - -# Version downgraded due to regression in library -# For details: https://github.com/nlsdfnbch/Pysher/issues/38 -DOMAIN = "goalfeed" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -GOALFEED_HOST = "feed.goalfeed.ca" -GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth" -GOALFEED_APP_ID = "bfd4ed98c1ff22c04074" - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Goalfeed component.""" - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - - def goal_handler(data): - """Handle goal events.""" - goal = json.loads(json.loads(data)) - - hass.bus.fire("goal", event_data=goal) - - def connect_handler(data): - """Handle connection.""" - post_data = { - "username": username, - "password": password, - "connection_info": data, - } - resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json() - - channel = pusher.subscribe("private-goals", resp["auth"]) - channel.bind("goal", goal_handler) - - pusher = pysher.Pusher( - GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST - ) - - pusher.connection.bind("pusher:connection_established", connect_handler) - pusher.connect() - - return True diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json deleted file mode 100644 index 077596b0185..00000000000 --- a/homeassistant/components/goalfeed/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "goalfeed", - "name": "Goalfeed", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/goalfeed", - "iot_class": "cloud_push", - "loggers": ["pysher"], - "requirements": ["pysher==1.0.7"] -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6418e93aa03..6f13633e306 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1995,12 +1995,6 @@ } } }, - "goalfeed": { - "name": "Goalfeed", - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push" - }, "goalzero": { "name": "Goal Zero Yeti", "integration_type": "device", diff --git a/requirements_all.txt b/requirements_all.txt index 866a5bb8c99..b90de9ec7d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1966,9 +1966,6 @@ pyserial==3.5 # homeassistant.components.sesame pysesame2==1.0.1 -# homeassistant.components.goalfeed -pysher==1.0.7 - # homeassistant.components.sia pysiaalarm==3.1.1 From 23f28988364167abfdfecf82f8b2e719b2ba2a71 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 6 Jun 2023 23:03:59 +0200 Subject: [PATCH 57/66] Correct zha device classes for voc and pm25 (#94130) Correct zha device classes --- homeassistant/components/zha/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index da1f1f6c04c..918458a32ad 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -722,7 +722,9 @@ class PPBVOCLevel(Sensor): """VOC Level sensor.""" SENSOR_ATTR = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS + _attr_device_class: SensorDeviceClass = ( + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS + ) _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_name: str = "VOC level" _decimals = 0 @@ -736,6 +738,7 @@ class PM25(Sensor): """Particulate Matter 2.5 microns or less sensor.""" SENSOR_ATTR = "measured_value" + _attr_device_class: SensorDeviceClass = SensorDeviceClass.PM25 _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_name: str = "Particulate matter" _decimals = 0 From b077bf9b86c941ddadacdec4ab8a2aebde7181b8 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 6 Jun 2023 20:07:21 -0400 Subject: [PATCH 58/66] Fix multiple smart detects firing at once for UniFi Protect (#94133) * Fix multiple smart detects firing at once * Tweak * Clean up logging. Linting * Linting --- .../components/unifiprotect/binary_sensor.py | 20 ++++------- homeassistant/components/unifiprotect/data.py | 24 +++++++++++++ .../components/unifiprotect/manifest.json | 2 +- .../components/unifiprotect/models.py | 34 +++++++++---------- .../components/unifiprotect/sensor.py | 7 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_sensor.py | 4 ++- 8 files changed, 55 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 7aa7c6d5cf1..fe4399c4c6d 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -14,8 +14,6 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, ProtectModelWithId, Sensor, - SmartDetectAudioType, - SmartDetectObjectType, ) from pyunifiprotect.data.nvr import UOSDisk @@ -364,8 +362,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_value="is_smart_detected", ufp_required_field="can_detect_person", ufp_enabled="is_person_detection_on", - ufp_event_obj="last_smart_detect_event", - ufp_smart_type=SmartDetectObjectType.PERSON, + ufp_event_obj="last_person_detect_event", ), ProtectBinaryEventEntityDescription( key="smart_obj_vehicle", @@ -374,8 +371,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_value="is_smart_detected", ufp_required_field="can_detect_vehicle", ufp_enabled="is_vehicle_detection_on", - ufp_event_obj="last_smart_detect_event", - ufp_smart_type=SmartDetectObjectType.VEHICLE, + ufp_event_obj="last_vehicle_detect_event", ), ProtectBinaryEventEntityDescription( key="smart_obj_face", @@ -384,8 +380,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_value="is_smart_detected", ufp_required_field="can_detect_face", ufp_enabled="is_face_detection_on", - ufp_event_obj="last_smart_detect_event", - ufp_smart_type=SmartDetectObjectType.FACE, + ufp_event_obj="last_face_detect_event", ), ProtectBinaryEventEntityDescription( key="smart_obj_package", @@ -394,8 +389,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_value="is_smart_detected", ufp_required_field="can_detect_package", ufp_enabled="is_package_detection_on", - ufp_event_obj="last_smart_detect_event", - ufp_smart_type=SmartDetectObjectType.PACKAGE, + ufp_event_obj="last_package_detect_event", ), ProtectBinaryEventEntityDescription( key="smart_audio_any", @@ -412,8 +406,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_value="is_smart_detected", ufp_required_field="can_detect_smoke", ufp_enabled="is_smoke_detection_on", - ufp_event_obj="last_smart_audio_detect_event", - ufp_smart_type=SmartDetectAudioType.SMOKE, + ufp_event_obj="last_smoke_detect_event", ), ProtectBinaryEventEntityDescription( key="smart_audio_cmonx", @@ -422,8 +415,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_value="is_smart_detected", ufp_required_field="can_detect_smoke", ufp_enabled="is_smoke_detection_on", - ufp_event_obj="last_smart_audio_detect_event", - ufp_smart_type=SmartDetectAudioType.CMONX, + ufp_event_obj="last_cmonx_detect_event", ), ) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 68d48003ba6..88c500f18fd 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -40,6 +40,11 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = ProtectAdoptableDeviceModel | NVR +SMART_EVENTS = { + EventType.SMART_DETECT, + EventType.SMART_AUDIO_DETECT, + EventType.SMART_DETECT_LINE, +} @callback @@ -223,6 +228,25 @@ class ProtectData: # trigger updates for camera that the event references elif isinstance(obj, Event): + if obj.type in SMART_EVENTS: + if obj.camera is not None: + if obj.end is None: + _LOGGER.debug( + "%s (%s): New smart detection started for %s (%s)", + obj.camera.name, + obj.camera.mac, + obj.smart_detect_types, + obj.id, + ) + else: + _LOGGER.debug( + "%s (%s): Smart detection ended for %s (%s)", + obj.camera.name, + obj.camera.mac, + obj.smart_detect_types, + obj.id, + ) + if obj.type == EventType.DEVICE_ADOPTED: if obj.metadata is not None and obj.metadata.device_id is not None: device = self.api.bootstrap.get_device_from_id( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a2bb76c92b7..e16180b03bc 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.9.1", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.10.0", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 40280c02867..8c688231628 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass +from datetime import timedelta from enum import Enum import logging from typing import Any, Generic, TypeVar, cast @@ -10,6 +11,7 @@ from typing import Any, Generic, TypeVar, cast from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel from homeassistant.helpers.entity import EntityDescription +from homeassistant.util import dt as dt_util from .utils import get_nested_attr @@ -67,7 +69,6 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): """Mixin for events.""" ufp_event_obj: str | None = None - ufp_smart_type: str | None = None def get_event_obj(self, obj: T) -> Event | None: """Return value from UniFi Protect device.""" @@ -79,23 +80,22 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): def get_is_on(self, obj: T) -> bool: """Return value if event is active.""" - value = bool(self.get_ufp_value(obj)) - if value: - event = self.get_event_obj(obj) - value = event is not None - if not value: - _LOGGER.debug("%s (%s): missing event", self.name, obj.mac) + event = self.get_event_obj(obj) + if event is None: + return False - if event is not None and self.ufp_smart_type is not None: - value = self.ufp_smart_type in event.smart_detect_types - if not value: - _LOGGER.debug( - "%s (%s): %s not in %s", - self.name, - obj.mac, - self.ufp_smart_type, - event.smart_detect_types, - ) + now = dt_util.utcnow() + value = now > event.start + if value and event.end is not None and now > event.end: + value = False + # only log if the recent ended recently + if event.end + timedelta(seconds=10) < now: + _LOGGER.debug( + "%s (%s): end ended at %s", + self.name, + obj.mac, + event.end.isoformat(), + ) if value: _LOGGER.debug("%s (%s): value is on", self.name, obj.mac) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 783955b3401..dec6f10a57f 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -15,7 +15,6 @@ from pyunifiprotect.data import ( ProtectDeviceModel, ProtectModelWithId, Sensor, - SmartDetectObjectType, ) from homeassistant.components.sensor import ( @@ -528,10 +527,9 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( name="License Plate Detected", icon="mdi:car", translation_key="license_plate", - ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE, ufp_value="is_smart_detected", ufp_required_field="can_detect_license_plate", - ufp_event_obj="last_smart_detect_event", + ufp_event_obj="last_license_plate_detect_event", ), ) @@ -767,8 +765,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): EventEntityMixin._async_update_device_from_protect(self, device) is_on = self.entity_description.get_is_on(device) is_license_plate = ( - self.entity_description.ufp_smart_type - == SmartDetectObjectType.LICENSE_PLATE + self.entity_description.ufp_event_obj == "last_license_plate_detect_event" ) if ( not is_on diff --git a/requirements_all.txt b/requirements_all.txt index b90de9ec7d8..854a82af964 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.9.1 +pyunifiprotect==4.10.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 660e296d30d..4f69bf74baf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.9.1 +pyunifiprotect==4.10.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index db7cdc801bf..89a153caed2 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -537,7 +537,9 @@ async def test_camera_update_licenseplate( new_camera = camera.copy() new_camera.is_smart_detected = True - new_camera.last_smart_detect_event_id = event.id + new_camera.last_smart_detect_event_ids[ + SmartDetectObjectType.LICENSE_PLATE + ] = event.id mock_msg = Mock() mock_msg.changed_data = {} From 7740539df00f14e672d75f5b6f6f4e8cea32712f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Jun 2023 20:55:25 -0400 Subject: [PATCH 59/66] Bump waqiasync to 1.1.0 (#94136) --- homeassistant/components/waqi/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index d1c75217830..e5630d5fd29 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["waqiasync"], - "requirements": ["waqiasync==1.0.0"] + "requirements": ["waqiasync==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 854a82af964..7a3e7ac699a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2632,7 +2632,7 @@ wakeonlan==2.1.0 wallbox==0.4.12 # homeassistant.components.waqi -waqiasync==1.0.0 +waqiasync==1.1.0 # homeassistant.components.folder_watcher watchdog==2.3.1 From f4e3ef6b515787c78ffdd2117233f8f149cfd883 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Jun 2023 22:00:28 -0400 Subject: [PATCH 60/66] Bumped version to 2023.6.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b872c061c9..dadc9bfc9b1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 29411420d42..94ce62f7bdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b5" +version = "2023.6.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 02d55a8e4969f6399259f6a9e16712c2e4db5ca8 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 7 Jun 2023 05:15:03 -0400 Subject: [PATCH 61/66] Bump unifiprotect to 4.10.1 (#94141) --- 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 e16180b03bc..78e2ee3012c 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.10.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.10.1", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 7a3e7ac699a..b2116bacaca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.0 +pyunifiprotect==4.10.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f69bf74baf..fb54f734c6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.0 +pyunifiprotect==4.10.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 5cc61acfb2045515face106337893fb95195a2c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Jun 2023 11:14:51 +0200 Subject: [PATCH 62/66] Fix migration of Google Assistant cloud settings (#94148) --- homeassistant/components/cloud/google_config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 8592a4ffbe3..1b7375946f7 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -108,7 +108,12 @@ def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool: if domain in SUPPORTED_DOMAINS: return True - device_class = get_device_class(hass, entity_id) + try: + device_class = get_device_class(hass, entity_id) + except HomeAssistantError: + # The entity no longer exists + return False + if ( domain == "binary_sensor" and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES From 74ccdcda682f586bc9e75f66873b4f92daf561ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Jun 2023 12:57:37 +0200 Subject: [PATCH 63/66] Update frontend to 20230607.0 (#94150) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 47bdf60b8e7..af8898f28e2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230606.0"] + "requirements": ["home-assistant-frontend==20230607.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f02f852aa32..22d47a0e430 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.1.0 hass-nabucasa==0.67.1 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230606.0 +home-assistant-frontend==20230607.0 home-assistant-intents==2023.6.5 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b2116bacaca..8bb4580aa61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230606.0 +home-assistant-frontend==20230607.0 # homeassistant.components.conversation home-assistant-intents==2023.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb54f734c6a..bc1cae28d09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -716,7 +716,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230606.0 +home-assistant-frontend==20230607.0 # homeassistant.components.conversation home-assistant-intents==2023.6.5 From 468be632fd00af1328d62ceeb14957fa2516bb47 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Jun 2023 13:16:23 +0200 Subject: [PATCH 64/66] Add debug logs to cloud migration (#94151) --- homeassistant/components/cloud/alexa_config.py | 10 ++++++++++ homeassistant/components/cloud/google_config.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 052fddabb54..8c1300f6228 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -221,6 +221,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): async def on_hass_started(hass: HomeAssistant) -> None: if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION: + _LOGGER.info( + "Start migration of Alexa settings from v%s to v%s", + self._prefs.alexa_settings_version, + ALEXA_SETTINGS_VERSION, + ) if self._prefs.alexa_settings_version < 2 or ( # Recover from a bug we had in 2023.5.0 where entities didn't get exposed self._prefs.alexa_settings_version < 3 @@ -233,6 +238,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): ): self._migrate_alexa_entity_settings_v1() + _LOGGER.info( + "Finished migration of Alexa settings from v%s to v%s", + self._prefs.alexa_settings_version, + ALEXA_SETTINGS_VERSION, + ) await self._prefs.async_update( alexa_settings_version=ALEXA_SETTINGS_VERSION ) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 1b7375946f7..0a49c0b6ed6 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -213,6 +213,11 @@ class CloudGoogleConfig(AbstractConfig): async def on_hass_started(hass: HomeAssistant) -> None: if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION: + _LOGGER.info( + "Start migration of Google Assistant settings from v%s to v%s", + self._prefs.google_settings_version, + GOOGLE_SETTINGS_VERSION, + ) if self._prefs.google_settings_version < 2 or ( # Recover from a bug we had in 2023.5.0 where entities didn't get exposed self._prefs.google_settings_version < 3 @@ -225,6 +230,11 @@ class CloudGoogleConfig(AbstractConfig): ): self._migrate_google_entity_settings_v1() + _LOGGER.info( + "Finished migration of Google Assistant settings from v%s to v%s", + self._prefs.google_settings_version, + GOOGLE_SETTINGS_VERSION, + ) await self._prefs.async_update( google_settings_version=GOOGLE_SETTINGS_VERSION ) From 3d3fecbd2340d4c0dddeb5c1cb2f77ae251379fc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Jun 2023 13:46:17 +0200 Subject: [PATCH 65/66] Disable google assistant local control of climate entities (#94153) --- homeassistant/components/google_assistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index bf511f8eaeb..918cec046fb 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -186,7 +186,7 @@ STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id" SOURCE_CLOUD = "cloud" SOURCE_LOCAL = "local" -NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK} +NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK, TYPE_THERMOSTAT} FAN_SPEEDS = { "5/5": ["High", "Max", "Fast", "5"], From 421fa5b035eee2798d2cbd1155cd7228e131db09 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Jun 2023 13:49:03 +0200 Subject: [PATCH 66/66] Bumped version to 2023.6.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dadc9bfc9b1..9308d364ecb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 94ce62f7bdd..2ece07d96e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.0b6" +version = "2023.6.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"