Skip to content

Commit

Permalink
2023.1.6 (#86251)
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob committed Jan 20, 2023
2 parents c5fb3e7 + aa7e051 commit 9a4329a
Show file tree
Hide file tree
Showing 18 changed files with 180 additions and 58 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/auth/indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/govee_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 8 additions & 2 deletions homeassistant/components/logbook/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -384,6 +385,11 @@ def _queue_or_cancel(event: Event) -> None:
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(
Expand Down
74 changes: 48 additions & 26 deletions homeassistant/components/matter/adapter.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
"""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
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
Expand Down Expand Up @@ -66,49 +71,66 @@ def _setup_node(self, node: MatterNode) -> None:
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,
)

def _setup_node_device(
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():
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/matter/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@

DOMAIN = "matter"
LOGGER = logging.getLogger(__package__)

# prefixes to identify device identifier id types
ID_TYPE_DEVICE_ID = "deviceid"
ID_TYPE_SERIAL = "serial"
27 changes: 13 additions & 14 deletions homeassistant/components/matter/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
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
Expand Down Expand Up @@ -55,24 +57,21 @@ def __init__(
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."""
Expand Down Expand Up @@ -115,7 +114,7 @@ def _update_from_device(self) -> None:

@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
Expand Down
33 changes: 31 additions & 2 deletions homeassistant/components/matter/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
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


Expand All @@ -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__}"
5 changes: 5 additions & 0 deletions homeassistant/components/shelly/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,11 @@ def sw_version(self) -> str:

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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/components/auth/test_indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
18 changes: 18 additions & 0 deletions tests/components/logbook/test_websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 9a4329a

Please sign in to comment.