Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e6c0c26
Add join/unjoin support to Forked DAAPD (#179317)
JanGiese Aug 18, 2026
9a51258
Split user flow init and data submission in geonetnz_volcano config f…
exxamalte Aug 18, 2026
f59ca4b
Use consistent node_id key in Z-Wave JS node statistics events (#179360)
MindFreeze Aug 18, 2026
e4a52c0
Recover Z-Wave JS config entry when a flow is abandoned (#179429)
balloobbot Aug 18, 2026
64419c6
Use dedicated translation on sensor entities if it's Ecovacs mower (#…
edenhaus Aug 18, 2026
0190c70
Bump xiaomi-ble to 1.16.0 (#178706)
trcyberoptic Aug 18, 2026
d279383
Revert host update on Overkiz local gateway rediscovery (#179422)
iMicknl Aug 18, 2026
7bd45f2
Reduce Teslemetry vehicle polling for disabled entities (#179454)
Bre77 Aug 18, 2026
465366a
Update pnpm to 11.22.0 (#179485)
renovate[bot] Aug 18, 2026
890d376
Update ruff (#179481)
renovate[bot] Aug 18, 2026
19b4086
Update SQLAlchemy to 2.0.52 (#179482)
renovate[bot] Aug 18, 2026
c3b71c4
Make URLs in GeoSphere Austria Warnings more specific (#179240)
Xiretza Aug 18, 2026
0140b90
Don't let zeroconf prompts block Z-Wave JS USB and add-on discovery (…
balloobbot Aug 18, 2026
aeb5fe4
Fix PS4 v2->v3 migration (#179489)
emontnemery Aug 18, 2026
baff8f6
Invalidate conversation slot-list cache on device registry changes (#…
emontnemery Aug 18, 2026
66a16ef
Replace deprecated os.path.commonprefix (#179011)
cdce8p Aug 18, 2026
5bc3a5b
Update orjson to 3.12.0 (#179484)
renovate[bot] Aug 18, 2026
20c0c6e
Drop redundant aliases in intent tests (#179488)
emontnemery Aug 18, 2026
a8423f4
Add diagnostics to Lyngdorf (#179478)
fishloa Aug 18, 2026
155d961
Fix KeyError in Shelly integration for LinkedGo ST802 in `floor heati…
bieniu Aug 18, 2026
dd58d21
Update uv to 0.12.5 (#179483)
renovate[bot] Aug 18, 2026
e299001
Add binary_sensor platform to IntelliClima (#178154)
dvdinth Aug 18, 2026
36435f5
Add available disk space sensor to Transmission integration (#179487)
Eniot666 Aug 18, 2026
35b21e6
Add humidifier platform to Midea (#178873)
chemelli74 Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.2
rev: v0.16.3
hooks:
- id: ruff-check
args:
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@

_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"]
_DEVICE_REGISTRY_UPDATE_FIELDS = ["name", "name_by_user"]

_DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}

Expand Down Expand Up @@ -288,6 +289,15 @@ def _filter_entity_registry_changes(
field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
)

@callback
def _filter_device_registry_changes(
self, event_data: dr.EventDeviceRegistryUpdatedData
) -> bool:
"""Filter device registry changed events."""
return event_data["action"] == "update" and any(
field in event_data["changes"] for field in _DEVICE_REGISTRY_UPDATE_FIELDS
)

@callback
def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
"""Filter state changed events."""
Expand All @@ -312,6 +322,11 @@ def _listen_clear_slot_list(self) -> None:
self._async_clear_slot_list,
event_filter=self._filter_entity_registry_changes,
),
self.hass.bus.async_listen(
dr.EVENT_DEVICE_REGISTRY_UPDATED,
self._async_clear_slot_list,
event_filter=self._filter_device_registry_changes,
),
self.hass.bus.async_listen(
EVENT_STATE_CHANGED,
self._async_clear_slot_list,
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/ecovacs/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,32 @@
"stats_area": {
"default": "mdi:floor-plan"
},
"stats_area_mower": {
"default": "mdi:floor-plan"
},
"stats_time": {
"default": "mdi:timer-outline"
},
"stats_time_mower": {
"default": "mdi:timer-outline"
},
"total_stats_area": {
"default": "mdi:floor-plan"
},
"total_stats_area_mower": {
"default": "mdi:floor-plan"
},
"total_stats_cleanings": {
"default": "mdi:counter"
},
"total_stats_cleanings_mower": {
"default": "mdi:counter"
},
"total_stats_time": {
"default": "mdi:timer-outline"
},
"total_stats_time_mower": {
"default": "mdi:timer-outline"
}
},
"switch": {
Expand Down
88 changes: 62 additions & 26 deletions homeassistant/components/ecovacs/sensor.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Ecovacs sensor module."""

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, override
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field, fields, replace
from typing import Any, Self, override

from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
Expand Down Expand Up @@ -33,10 +33,10 @@
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.typing import UNDEFINED, StateType, UndefinedType

from . import EcovacsConfigEntry
from .const import LEGACY_SUPPORTED_LIFESPANS, SUPPORTED_LIFESPANS
Expand All @@ -49,6 +49,14 @@
from .util import get_name_key, get_options, get_supported_entities


@dataclass(kw_only=True, frozen=True)
class EcovacsSensorDeviceTypeOverride:
"""Description values, which differ for a specific device type."""

native_unit_of_measurement: str | UndefinedType | None = UNDEFINED
translation_key: str | UndefinedType | None = UNDEFINED


@dataclass(kw_only=True, frozen=True)
class EcovacsSensorEntityDescription[EventT: Event](
EcovacsCapabilityEntityDescription,
Expand All @@ -57,15 +65,23 @@ class EcovacsSensorEntityDescription[EventT: Event](
"""Ecovacs sensor entity description."""

value_fn: Callable[[EventT], StateType]
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None

device_type_overrides: Mapping[DeviceType, EcovacsSensorDeviceTypeOverride] = field(
default_factory=dict
)

@callback
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
"""Get the area native unit of measurement based on device type."""
if device_type is DeviceType.MOWER:
return UnitOfArea.SQUARE_CENTIMETERS
return UnitOfArea.SQUARE_METERS
def get_for(self, device: DeviceType) -> Self:
"""Get entity description for specific device type."""
if (overrides := self.device_type_overrides.get(device)) is None:
return self

return replace(
self,
**{
f.name: value
for f in fields(overrides)
if (value := getattr(overrides, f.name)) is not UNDEFINED
},
)


ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
Expand All @@ -76,8 +92,14 @@ def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
value_fn=lambda e: e.area,
translation_key="stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
native_unit_of_measurement=UnitOfArea.SQUARE_CENTIMETERS,
translation_key="stats_area_mower",
)
},
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
Expand All @@ -87,6 +109,11 @@ def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="stats_time_mower",
)
},
),
# TotalStats
EcovacsSensorEntityDescription[TotalStatsEvent](
Expand All @@ -97,6 +124,11 @@ def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="total_stats_area_mower",
)
},
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
Expand All @@ -107,13 +139,23 @@ def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="total_stats_time_mower",
)
},
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
value_fn=lambda e: e.cleanings,
key="total_stats_cleanings",
translation_key="total_stats_cleanings",
state_class=SensorStateClass.TOTAL_INCREASING,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="total_stats_cleanings_mower",
)
},
),
EcovacsSensorEntityDescription[BatteryEvent](
capability_fn=lambda caps: caps.battery,
Expand Down Expand Up @@ -274,18 +316,12 @@ def __init__(
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description, **kwargs)
if (
entity_description.native_unit_of_measurement_fn
and (
native_unit_of_measurement
:= entity_description.native_unit_of_measurement_fn(
device.capabilities.device_type
)
)
is not None
):
self._attr_native_unit_of_measurement = native_unit_of_measurement
super().__init__(
device,
capability,
entity_description.get_for(device.capabilities.device_type),
**kwargs,
)

@override
async def async_added_to_hass(self) -> None:
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/ecovacs/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,32 @@
"stats_area": {
"name": "Area cleaned"
},
"stats_area_mower": {
"name": "Area mowed"
},
"stats_time": {
"name": "Cleaning duration"
},
"stats_time_mower": {
"name": "Mowing duration"
},
"total_stats_area": {
"name": "Total area cleaned"
},
"total_stats_area_mower": {
"name": "Total area mowed"
},
"total_stats_cleanings": {
"name": "Total cleanings"
},
"total_stats_cleanings_mower": {
"name": "Total mowings"
},
"total_stats_time": {
"name": "Total cleaning duration"
},
"total_stats_time_mower": {
"name": "Total mowing duration"
}
},
"switch": {
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/forked_daapd/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
| MediaPlayerEntityFeature.GROUPING
)
SUPPORTED_FEATURES_ZONE = (
MediaPlayerEntityFeature.VOLUME_SET
Expand Down
71 changes: 71 additions & 0 deletions homeassistant/components/forked_daapd/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
)
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
Expand All @@ -51,6 +53,7 @@
DEFAULT_TTS_PAUSE_TIME,
DEFAULT_TTS_VOLUME,
DEFAULT_UNMUTE_VOLUME,
DOMAIN,
FD_NAME,
KNOWN_PIPES,
PIPE_FUNCTION_MAP,
Expand Down Expand Up @@ -458,6 +461,74 @@ async def async_toggle(self) -> None:
else:
await self.async_turn_off()

@override
async def async_join_players(self, group_members: list[str]) -> None:
"""Join `group_members` (outputs) to the current playback."""
entity_registry = er.async_get(self.hass)
known_output_ids = {output["id"] for output in self._outputs}
output_ids: list[str] = []
for entity_id in group_members:
if entity_id == self.entity_id:
continue
if not (entity_entry := entity_registry.async_get(entity_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entity_not_found",
translation_placeholders={"entity_id": entity_id},
)
if (
entity_entry.platform != DOMAIN
or entity_entry.config_entry_id != self._entry_id
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="not_forked_daapd_output",
translation_placeholders={"entity_id": entity_id},
)
# Zone unique ids are f"{config_entry.entry_id}-{output_id}"
output_id = entity_entry.unique_id.split("-", 1)[1]
# Registry entries persist after an output disappears from the server
if output_id not in known_output_ids:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="output_not_found",
translation_placeholders={"entity_id": entity_id},
)
output_ids.append(output_id)

await asyncio.gather(
*(
self.api.change_output(output_id, selected=True)
for output_id in output_ids
)
)

@override
async def async_unjoin_player(self) -> None:
"""Remove all outputs from the current playback."""
if any(output["selected"] for output in self._outputs):
await self.api.set_enabled_outputs([])

@property
@override
def group_members(self) -> list[str]:
"""List of players which are currently grouped together."""
entity_registry = er.async_get(self.hass)
output_id_to_entity_id = {
entry.unique_id.split("-", 1)[1]: entry.entity_id
for entry in er.async_entries_for_config_entry(
entity_registry, self._entry_id
)
# Skip the master entity, whose unique id is the config entry id
if isinstance(entry.unique_id, str) and "-" in entry.unique_id
}
return [self.entity_id] + [
entity_id
for output in self._outputs
if output["selected"]
and (entity_id := output_id_to_entity_id.get(output["id"])) is not None
]

@property
@override
def name(self) -> str:
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/forked_daapd/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
}
}
},
"exceptions": {
"entity_not_found": {
"message": "Entity {entity_id} not found."
},
"not_forked_daapd_output": {
"message": "Entity {entity_id} is not an output of this OwnTone server."
},
"output_not_found": {
"message": "The output for entity {entity_id} no longer exists on the OwnTone server."
}
},
"options": {
"step": {
"init": {
Expand Down
Loading
Loading