mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
2023.1.6 (#86251)
This commit is contained in:
commit
9a4329aa1d
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
@ -13,8 +17,9 @@ 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matter_server.client import MatterClient
|
||||
@ -66,31 +71,56 @@ 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)
|
||||
|
||||
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:
|
||||
# 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, info.uniqueID)},
|
||||
hw_version=info.hardwareVersionString,
|
||||
sw_version=info.softwareVersionString,
|
||||
manufacturer=info.vendorName,
|
||||
model=info.productName,
|
||||
identifiers=identifiers,
|
||||
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 +128,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():
|
||||
|
@ -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"
|
||||
|
@ -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 .const import DOMAIN, ID_TYPE_DEVICE_ID
|
||||
from .helpers import get_device_id, get_operational_instance_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matter_server.client import MatterClient
|
||||
@ -55,24 +57,21 @@ 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)}}
|
||||
node_device_id = get_device_id(server_info, node_device)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")}
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle being added to Home Assistant."""
|
||||
@ -115,7 +114,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
|
||||
|
@ -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
|
||||
|
||||
|
||||
@ -25,7 +29,32 @@ 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
|
||||
|
||||
|
||||
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 Home Assistant devices.
|
||||
return f"{operational_instance_id}-{node_device.__class__.__name__}"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -27,10 +27,14 @@ 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, "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"
|
||||
@ -38,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,
|
||||
|
@ -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,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user