From d935f9400dfa1f5f7dee481805a627c2c10b0ddf Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 17 Jan 2023 15:39:42 +0100 Subject: [PATCH 1/8] Fix Matter unique_id generation (#86046) * bae entity unique id on Operational Instance Name standard * Update homeassistant/components/matter/entity.py Co-authored-by: Stefan Agner * also adjust unique id for devices * final adjustment * remove assert on server_info * move device info to init * fabric_id_hex * use DeviceInfo instead of dict * fix test Co-authored-by: Stefan Agner --- homeassistant/components/matter/adapter.py | 65 +++++++++++++--------- homeassistant/components/matter/entity.py | 22 ++++---- homeassistant/components/matter/helpers.py | 28 ++++++++++ tests/components/matter/test_adapter.py | 5 +- 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index b573ed0a3fc..b07b489e029 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -1,11 +1,15 @@ """Matter to Home Assistant adapter.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from chip.clusters import Objects as all_clusters from matter_server.common.models.events import EventType -from matter_server.common.models.node_device import AbstractMatterNodeDevice +from matter_server.common.models.node_device import ( + AbstractMatterNodeDevice, + MatterBridgedNodeDevice, +) +from matter_server.common.models.server_information import ServerInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, LOGGER from .device_platform import DEVICE_PLATFORM +from .helpers import get_device_id if TYPE_CHECKING: from matter_server.client import MatterClient @@ -66,31 +71,49 @@ class MatterAdapter: bridge_unique_id: str | None = None if node.aggregator_device_type_instance is not None and ( - node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic) + node.root_device_type_instance.get_cluster(all_clusters.Basic) ): - self._create_device_registry( - node_info, node_info.nodeLabel or "Hub device", None + # create virtual (parent) device for bridge node device + bridge_device = MatterBridgedNodeDevice( + node.aggregator_device_type_instance ) - bridge_unique_id = node_info.uniqueID + self._create_device_registry(bridge_device) + server_info = cast(ServerInfo, self.matter_client.server_info) + bridge_unique_id = get_device_id(server_info, bridge_device) for node_device in node.node_devices: self._setup_node_device(node_device, bridge_unique_id) def _create_device_registry( self, - info: all_clusters.Basic | all_clusters.BridgedDeviceBasic, - name: str, - bridge_unique_id: str | None, + node_device: AbstractMatterNodeDevice, + bridge_unique_id: str | None = None, ) -> None: """Create a device registry entry.""" + server_info = cast(ServerInfo, self.matter_client.server_info) + node_unique_id = get_device_id( + server_info, + node_device, + ) + basic_info = node_device.device_info() + device_type_instances = node_device.device_type_instances() + + name = basic_info.nodeLabel + if not name and isinstance(node_device, MatterBridgedNodeDevice): + # fallback name for Bridge + name = "Hub device" + elif not name and device_type_instances: + # fallback name based on device type + name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}" + dr.async_get(self.hass).async_get_or_create( name=name, config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, info.uniqueID)}, - hw_version=info.hardwareVersionString, - sw_version=info.softwareVersionString, - manufacturer=info.vendorName, - model=info.productName, + identifiers={(DOMAIN, node_unique_id)}, + hw_version=basic_info.hardwareVersionString, + sw_version=basic_info.softwareVersionString, + manufacturer=basic_info.vendorName, + model=basic_info.productName, via_device=(DOMAIN, bridge_unique_id) if bridge_unique_id else None, ) @@ -98,17 +121,9 @@ class MatterAdapter: self, node_device: AbstractMatterNodeDevice, bridge_unique_id: str | None ) -> None: """Set up a node device.""" - node = node_device.node() - basic_info = node_device.device_info() - device_type_instances = node_device.device_type_instances() - - name = basic_info.nodeLabel - if not name and device_type_instances: - name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node.node_id}" - - self._create_device_registry(basic_info, name, bridge_unique_id) - - for instance in device_type_instances: + self._create_device_registry(node_device, bridge_unique_id) + # run platform discovery from device type instances + for instance in node_device.device_type_instances(): created = False for platform, devices in DEVICE_PLATFORM.items(): diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 4f28c1d2369..fd839dcca5e 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -5,16 +5,18 @@ from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance from matter_server.common.models.events import EventType from matter_server.common.models.node_device import AbstractMatterNodeDevice +from matter_server.common.models.server_information import ServerInfo from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import DOMAIN +from .helpers import get_device_id, get_operational_instance_id if TYPE_CHECKING: from matter_server.client import MatterClient @@ -55,24 +57,20 @@ class MatterEntity(Entity): self._node_device = node_device self._device_type_instance = device_type_instance self.entity_description = entity_description - node = device_type_instance.node self._unsubscribes: list[Callable] = [] # for fast lookups we create a mapping to the attribute paths - self._attributes_map: dict[type, str] = {} - server_info = matter_client.server_info # The server info is set when the client connects to the server. - assert server_info is not None + self._attributes_map: dict[type, str] = {} + server_info = cast(ServerInfo, self.matter_client.server_info) + # create unique_id based on "Operational Instance Name" and endpoint/device type self._attr_unique_id = ( - f"{server_info.compressed_fabric_id}-" - f"{node.unique_id}-" + f"{get_operational_instance_id(server_info, self._node_device.node())}-" f"{device_type_instance.endpoint}-" f"{device_type_instance.device_type.device_type}" ) - - @property - def device_info(self) -> DeviceInfo | None: - """Return device info for device registry.""" - return {"identifiers": {(DOMAIN, self._node_device.device_info().uniqueID)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, get_device_id(server_info, node_device))} + ) async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 479b1d824ad..8dd20538a39 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -10,6 +10,10 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN if TYPE_CHECKING: + from matter_server.common.models.node import MatterNode + from matter_server.common.models.node_device import AbstractMatterNodeDevice + from matter_server.common.models.server_information import ServerInfo + from .adapter import MatterAdapter @@ -29,3 +33,27 @@ def get_matter(hass: HomeAssistant) -> MatterAdapter: # In case of the config entry we need to fix this. matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values())) return matter_entry_data.adapter + + +def get_operational_instance_id( + server_info: ServerInfo, + node: MatterNode, +) -> str: + """Return `Operational Instance Name` for given MatterNode.""" + fabric_id_hex = f"{server_info.compressed_fabric_id:016X}" + node_id_hex = f"{node.node_id:016X}" + # operational instance id matches the mdns advertisement for the node + # this is the recommended ID to recognize a unique matter node (within a fabric) + return f"{fabric_id_hex}-{node_id_hex}" + + +def get_device_id( + server_info: ServerInfo, + node_device: AbstractMatterNodeDevice, +) -> str: + """Return HA device_id for the given MatterNodeDevice.""" + operational_instance_id = get_operational_instance_id( + server_info, node_device.node() + ) + # append nodedevice(type) to differentiate between a root node and bridge within HA devices. + return f"{operational_instance_id}-{node_device.__class__.__name__}" diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 6bd341b0f2f..c89b45e4c0b 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -27,8 +27,9 @@ async def test_device_registry_single_node_device( ) dev_reg = dr.async_get(hass) - - entry = dev_reg.async_get_device({(DOMAIN, "mock-onoff-light")}) + entry = dev_reg.async_get_device( + {(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")} + ) assert entry is not None assert entry.name == "Mock OnOff Light" From 15a35004ddd0b624cec8f26af95f926665f07725 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Jan 2023 19:09:12 +0100 Subject: [PATCH 2/8] Code styling tweaks to the Matter integration (#86096) --- homeassistant/components/matter/adapter.py | 5 ++++- homeassistant/components/matter/entity.py | 2 +- homeassistant/components/matter/helpers.py | 11 ++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index b07b489e029..16e7d456212 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -104,7 +104,10 @@ class MatterAdapter: name = "Hub device" elif not name and device_type_instances: # fallback name based on device type - name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}" + name = ( + f"{device_type_instances[0].device_type.__doc__[:-1]}" + f" {node_device.node().node_id}" + ) dr.async_get(self.hass).async_get_or_create( name=name, diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index fd839dcca5e..f239cec0342 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -113,7 +113,7 @@ class MatterEntity(Entity): @callback def get_matter_attribute(self, attribute: type) -> MatterAttribute | None: - """Lookup MatterAttribute instance on device instance by providing the attribute class.""" + """Lookup MatterAttribute on device by providing the attribute class.""" return next( ( x diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 8dd20538a39..5abf81ee608 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -29,8 +29,8 @@ class MatterEntryData: def get_matter(hass: HomeAssistant) -> MatterAdapter: """Return MatterAdapter instance.""" # NOTE: This assumes only one Matter connection/fabric can exist. - # Shall we support connecting to multiple servers in the client or by config entries? - # In case of the config entry we need to fix this. + # Shall we support connecting to multiple servers in the client or by + # config entries? In case of the config entry we need to fix this. matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values())) return matter_entry_data.adapter @@ -42,8 +42,8 @@ def get_operational_instance_id( """Return `Operational Instance Name` for given MatterNode.""" fabric_id_hex = f"{server_info.compressed_fabric_id:016X}" node_id_hex = f"{node.node_id:016X}" - # operational instance id matches the mdns advertisement for the node - # this is the recommended ID to recognize a unique matter node (within a fabric) + # Operational instance id matches the mDNS advertisement for the node + # this is the recommended ID to recognize a unique matter node (within a fabric). return f"{fabric_id_hex}-{node_id_hex}" @@ -55,5 +55,6 @@ def get_device_id( operational_instance_id = get_operational_instance_id( server_info, node_device.node() ) - # append nodedevice(type) to differentiate between a root node and bridge within HA devices. + # Append nodedevice(type) to differentiate between a root node + # and bridge within Home Assistant devices. return f"{operational_instance_id}-{node_device.__class__.__name__}" From 67f7a9ea78181a32db87c8dafb59691084d1d0fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 09:48:21 -1000 Subject: [PATCH 3/8] Bump govee-ble to 0.21.1 (#86103) fixes https://github.com/home-assistant/core/issues/85580 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index ad54aa4bc43..aaa659d1667 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -73,7 +73,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.21.0"], + "requirements": ["govee-ble==0.21.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@PierreAronnax"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 5e812d80058..3f6fecf57da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -803,7 +803,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.21.0 +govee-ble==0.21.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214c868043c..fa5c72da0af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ google-nest-sdm==2.2.2 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.21.0 +govee-ble==0.21.1 # homeassistant.components.gree greeclimate==1.3.0 From 2c127c00d4e4c84b88449e9b96cd8486da89a44d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 18 Jan 2023 17:17:33 +0100 Subject: [PATCH 4/8] Adjust device registry for Matter devices (#86108) * adjust device registry * ignore test unique id * update test * ditch uniqueid + prefix serial * adjust test * add tests * fix switch test * prefix all identifiers * Update homeassistant/components/matter/adapter.py Co-authored-by: Martin Hjelmare * no underscore in id * fix test Co-authored-by: Martin Hjelmare --- homeassistant/components/matter/adapter.py | 26 ++++++++++------- homeassistant/components/matter/const.py | 4 +++ homeassistant/components/matter/entity.py | 5 ++-- .../fixtures/nodes/on-off-plugin-unit.json | 2 +- .../matter/fixtures/nodes/onoff-light.json | 2 +- tests/components/matter/test_adapter.py | 29 ++++++++++++++++++- tests/components/matter/test_switch.py | 10 +++---- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 16e7d456212..08763e38327 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from .const import DOMAIN, ID_TYPE_DEVICE_ID, ID_TYPE_SERIAL, LOGGER from .device_platform import DEVICE_PLATFORM from .helpers import get_device_id @@ -91,10 +91,7 @@ class MatterAdapter: ) -> None: """Create a device registry entry.""" server_info = cast(ServerInfo, self.matter_client.server_info) - node_unique_id = get_device_id( - server_info, - node_device, - ) + basic_info = node_device.device_info() device_type_instances = node_device.device_type_instances() @@ -103,16 +100,23 @@ class MatterAdapter: # fallback name for Bridge name = "Hub device" elif not name and device_type_instances: - # fallback name based on device type - name = ( - f"{device_type_instances[0].device_type.__doc__[:-1]}" - f" {node_device.node().node_id}" - ) + # use the productName if no node label is present + name = basic_info.productName + + node_device_id = get_device_id( + server_info, + node_device, + ) + identifiers = {(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} + # if available, we also add the serialnumber as identifier + if basic_info.serialNumber and "test" not in basic_info.serialNumber.lower(): + # prefix identifier with 'serial_' to be able to filter it + identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}")) dr.async_get(self.hass).async_get_or_create( name=name, config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, node_unique_id)}, + identifiers=identifiers, hw_version=basic_info.hardwareVersionString, sw_version=basic_info.softwareVersionString, manufacturer=basic_info.vendorName, diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py index c5ec1173ac0..e7f96bd2448 100644 --- a/homeassistant/components/matter/const.py +++ b/homeassistant/components/matter/const.py @@ -8,3 +8,7 @@ CONF_USE_ADDON = "use_addon" DOMAIN = "matter" LOGGER = logging.getLogger(__package__) + +# prefixes to identify device identifier id types +ID_TYPE_DEVICE_ID = "deviceid" +ID_TYPE_SERIAL = "serial" diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index f239cec0342..820d0f72846 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -15,7 +15,7 @@ from matter_server.common.models.server_information import ServerInfo from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .const import DOMAIN +from .const import DOMAIN, ID_TYPE_DEVICE_ID from .helpers import get_device_id, get_operational_instance_id if TYPE_CHECKING: @@ -68,8 +68,9 @@ class MatterEntity(Entity): f"{device_type_instance.endpoint}-" f"{device_type_instance.device_type.device_type}" ) + node_device_id = get_device_id(server_info, node_device) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, get_device_id(server_info, node_device))} + identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} ) async def async_added_to_hass(self) -> None: diff --git a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json index cbbe39b1f09..e26450a9a28 100644 --- a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json +++ b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json @@ -175,7 +175,7 @@ "attribute_id": 5, "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", "attribute_name": "NodeLabel", - "value": "Mock OnOff Plugin Unit" + "value": "" }, "0/40/6": { "node_id": 1, diff --git a/tests/components/matter/fixtures/nodes/onoff-light.json b/tests/components/matter/fixtures/nodes/onoff-light.json index cc6521aa2e3..340d7cb71c9 100644 --- a/tests/components/matter/fixtures/nodes/onoff-light.json +++ b/tests/components/matter/fixtures/nodes/onoff-light.json @@ -469,7 +469,7 @@ "attribute_id": 15, "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", "attribute_name": "SerialNumber", - "value": "TEST_SN" + "value": "12345678" }, "0/40/16": { "node_id": 1, diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index c89b45e4c0b..f83f21dc5e0 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -28,10 +28,13 @@ async def test_device_registry_single_node_device( dev_reg = dr.async_get(hass) entry = dev_reg.async_get_device( - {(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")} + {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} ) assert entry is not None + # test serial id present as additional identifier + assert (DOMAIN, "serial_12345678") in entry.identifiers + assert entry.name == "Mock OnOff Light" assert entry.manufacturer == "Nabu Casa" assert entry.model == "Mock Light" @@ -39,6 +42,30 @@ async def test_device_registry_single_node_device( assert entry.sw_version == "v1.0" +async def test_device_registry_single_node_device_alt( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test additional device with different attribute values.""" + await setup_integration_with_node_fixture( + hass, + "on-off-plugin-unit", + matter_client, + ) + + dev_reg = dr.async_get(hass) + entry = dev_reg.async_get_device( + {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} + ) + assert entry is not None + + # test name is derived from productName (because nodeLabel is absent) + assert entry.name == "Mock OnOffPluginUnit (powerplug/switch)" + + # test serial id NOT present as additional identifier + assert (DOMAIN, "serial_TEST_SN") not in entry.identifiers + + @pytest.mark.skip("Waiting for a new test fixture") async def test_device_registry_bridge( hass: HomeAssistant, diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index a79edd6010b..9fe225b1b13 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -30,7 +30,7 @@ async def test_turn_on( switch_node: MatterNode, ) -> None: """Test turning on a switch.""" - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "off" @@ -38,7 +38,7 @@ async def test_turn_on( "switch", "turn_on", { - "entity_id": "switch.mock_onoff_plugin_unit", + "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", }, blocking=True, ) @@ -53,7 +53,7 @@ async def test_turn_on( set_node_attribute(switch_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "on" @@ -64,7 +64,7 @@ async def test_turn_off( switch_node: MatterNode, ) -> None: """Test turning off a switch.""" - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "off" @@ -72,7 +72,7 @@ async def test_turn_off( "switch", "turn_off", { - "entity_id": "switch.mock_onoff_plugin_unit", + "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", }, blocking=True, ) From 58beab1b596376a3bc22027829386a9d46f931a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 16:06:37 -1000 Subject: [PATCH 5/8] Fix live logbook stalling when there are no historical events with a high commit interval (#86110) * Force live logbook to send an empty message to indicate no results Since the sync task can take a while if the recorder is busy, the logbook will appear to hang if we do not send the first partial message even if its empty. This work is in preparation for a higher database commit interval where this issue is most obvious. The historical only path did not have this issue because it never had to wait for the db sync. * update tests --- .../components/logbook/websocket_api.py | 10 ++++++++-- tests/components/logbook/test_websocket_api.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 8d0dd49bff8..04b288d523b 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -83,6 +83,7 @@ async def _async_send_historical_events( formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, partial: bool, + force_send: bool = False, ) -> dt | None: """Select historical data from the database and deliver it to the websocket. @@ -116,7 +117,7 @@ async def _async_send_historical_events( # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - if last_event_time or not partial: + if last_event_time or not partial or force_send: connection.send_message(message) return last_event_time @@ -150,7 +151,7 @@ async def _async_send_historical_events( # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - if older_query_last_event_time or not partial: + if older_query_last_event_time or not partial or force_send: connection.send_message(older_message) # Returns the time of the newest event @@ -384,6 +385,11 @@ async def ws_event_stream( messages.event_message, event_processor, partial=True, + # Force a send since the wait for the sync task + # can take a a while if the recorder is busy and + # we want to make sure the client is not still spinning + # because it is waiting for the first message + force_send=True, ) live_stream.task = asyncio.create_task( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 5b16c98998c..91d1a95f75b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1817,6 +1817,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] + await async_wait_recording_done(hass) # There are no answers to our initial query # so we get an empty reply. This is to ensure @@ -1828,6 +1829,15 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"]["events"] == [] + assert "partial" in msg["event"] + await async_wait_recording_done(hass) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] + await async_wait_recording_done(hass) hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) @@ -1942,6 +1952,14 @@ async def test_logbook_stream_match_multiple_entities( assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"]["events"] == [] + assert "partial" in msg["event"] + await async_wait_recording_done(hass) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] await async_wait_recording_done(hass) hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) From 8dd0752bd0d148add734edb746738a6a7ea6e4d5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 19 Jan 2023 01:11:40 +0200 Subject: [PATCH 6/8] Fix Shelly sleeping Gen2 device updates (#86198) --- homeassistant/components/shelly/coordinator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 12a46ee3ef9..96852512334 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -475,6 +475,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): async def _async_disconnected(self) -> None: """Handle device disconnected.""" + # Sleeping devices send data and disconnects + # There are no disconnect events for sleeping devices + if self.entry.data.get(CONF_SLEEP_PERIOD): + return + async with self._connection_lock: if not self.connected: # Already disconnected return From 82a13740b33904e106fabcf1407c4587ffb9bb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Thu, 19 Jan 2023 22:07:08 +0100 Subject: [PATCH 7/8] Update allowlisted OAuth redirect URIs for Wear OS in China (#86247) --- homeassistant/components/auth/indieauth.py | 1 + tests/components/auth/test_indieauth.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 478f7ab2831..ec8431366ab 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -47,6 +47,7 @@ async def verify_redirect_uri( if client_id == "https://home-assistant.io/android" and redirect_uri in ( "homeassistant://auth-callback", "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", ): return True diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 17d1fa927a0..43bd6b71fe5 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -190,9 +190,19 @@ async def test_verify_redirect_uri_android_ios(client_id): client_id, "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ) + assert await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", + ) else: assert not await indieauth.verify_redirect_uri( None, client_id, "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ) + assert not await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", + ) From aa7e05153807de1b2fc6d1767fcdec654d36ffd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Jan 2023 16:09:03 -0500 Subject: [PATCH 8/8] Bumped version to 2023.1.6 --- 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 d9eb4bb8534..d14ce310238 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 = 1 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index c1d8bb8caa2..6fd631b63c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.5" +version = "2023.1.6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"