Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e7f3e56
Bump aioshelly to 13.26.1 (#172160)
bdraco May 25, 2026
6de03f4
Add state attribute in_zones to BaseScannerEntity (#171832)
emontnemery May 25, 2026
f9bef80
Add infrared receiver support to ESPHome (#171789)
abmantis May 25, 2026
d5bae0a
Add pylint rule for checking async_setup calls in tests (#171890)
joostlek May 25, 2026
060f447
Fix swallowed exceptions in `homeassistant` action handlers (#170922)
mib1185 May 25, 2026
f4b7840
Bump aiodhcpwatcher to 1.2.7 (#172161)
bdraco May 25, 2026
946625e
Restore mopeka sensor entity data across reloads (#172178)
bdraco May 25, 2026
c8b70b1
Restore kegtron sensor entity data across reloads (#172176)
bdraco May 25, 2026
3516883
Restore ruuvitag_ble sensor entity data across reloads (#172180)
bdraco May 25, 2026
f1e2f94
Restore moat sensor entity data across reloads (#172177)
bdraco May 25, 2026
e77c16e
Restore bluemaestro sensor entity data across reloads (#172174)
bdraco May 25, 2026
cf02cfa
Restore rapt_ble sensor entity data across reloads (#172179)
bdraco May 25, 2026
3899f53
Restore sensirion_ble sensor entity data across reloads (#172181)
bdraco May 25, 2026
1329f12
Restore aranet sensor entity data across reloads (#172173)
bdraco May 25, 2026
cc411d0
Restore victron_ble sensor entity data across reloads (#172185)
bdraco May 25, 2026
cd4d669
Restore thermobeacon sensor entity data across reloads (#172183)
bdraco May 25, 2026
a5ceafa
Restore tilt_ble sensor entity data across reloads (#172184)
bdraco May 25, 2026
0cbf27f
Restore sensorpro sensor entity data across reloads (#172182)
bdraco May 25, 2026
afb27bc
bump soco to 0.31.1 for Sonos (#172168)
PeteRager May 25, 2026
74ca79a
Extend INKBIRD active scan duration to cover slower broadcasters (#17…
bdraco May 25, 2026
2b58ef9
Refactor set HVAC mode for Plugwise (#172121)
bouwew May 25, 2026
64ed269
Bump to aiounifi v91 (#172175)
Kane610 May 25, 2026
640f826
Bump habluetooth to 6.7.4 (#172162)
bdraco May 25, 2026
8ff6de7
Local helper for Axis serial number (#172172)
Kane610 May 25, 2026
baa6198
Bump dbus-fast to 5.0.11 (#172191)
bdraco May 25, 2026
b82c95e
Bump ulid-transform to 2.2.9 (#172190)
bdraco May 25, 2026
d2b37ee
Bump aiohttp-asyncmdnsresolver to 0.2.0 (#172188)
bdraco May 25, 2026
0ed21db
Bump icmplib to 3.0.4 (#172189)
bdraco May 25, 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
6 changes: 5 additions & 1 deletion homeassistant/components/aranet/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ async def async_setup_entry(
Aranet4BluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
entry.async_on_unload(
entry.runtime_data.async_register_processor(
processor, AranetSensorEntityDescription
)
)


class Aranet4BluetoothSensorEntity(
Expand Down
21 changes: 19 additions & 2 deletions homeassistant/components/axis/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import Mapping
from ipaddress import ip_address
from typing import Any
from typing import TYPE_CHECKING, Any
from urllib.parse import urlsplit

import voluptuous as vol
Expand Down Expand Up @@ -49,6 +49,9 @@
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api

if TYPE_CHECKING:
import axis

AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
DEFAULT_PORT = 443
DEFAULT_PROTOCOL = "https"
Expand Down Expand Up @@ -93,7 +96,8 @@ async def async_step_user(
errors["base"] = "cannot_connect"

else:
serial = api.vapix.serial_number
if (serial := self._get_serial_number(api)) is None:
return self.async_abort(reason="no_serial_number")
config = {
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
CONF_HOST: user_input[CONF_HOST],
Expand Down Expand Up @@ -258,6 +262,19 @@ async def _process_discovered_device(

return await self.async_step_user()

@staticmethod
def _get_serial_number(api: axis.AxisDevice) -> str | None:
"""Retrieve the device serial number from the Axis API.

Tries basic_device_info first, then property_handler. Returns None if not found.
"""
vapix = api.vapix
if vapix.basic_device_info.initialized:
return vapix.basic_device_info["0"].serial_number
if vapix.params.property_handler.initialized:
return vapix.params.property_handler["0"].system_serial_number
return None


class AxisOptionsFlowHandler(OptionsFlow):
"""Handle Axis device options."""
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/axis/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"no_serial_number": "Could not retrieve a serial number from the device. Please check device connectivity and try again.",
"not_axis_device": "Discovered device not an Axis device",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/bluemaestro/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ async def async_setup_entry(
BlueMaestroBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload(
coordinator.async_register_processor(processor, SensorEntityDescription)
)


class BlueMaestroBluetoothSensorEntity(
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/bluetooth/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"bluetooth-adapters==2.2.0",
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.9",
"habluetooth==6.7.3"
"dbus-fast==5.0.11",
"habluetooth==6.7.4"
]
}
24 changes: 22 additions & 2 deletions homeassistant/components/device_tracker/config_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,23 @@ def is_connected(self) -> bool | None:
"""Return true if the device is connected."""
raise NotImplementedError

@final
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr: dict[str, Any] = {ATTR_IN_ZONES: []}
attr.update(super().state_attributes)

if not self.is_connected:
return attr

attr[ATTR_IN_ZONES] = [
zone.ENTITY_ID_HOME,
*zone.async_get_enclosing_zones(self.hass, zone.ENTITY_ID_HOME),
]

return attr


class ScannerEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes tracker entities."""
Expand Down Expand Up @@ -484,9 +501,12 @@ async def async_internal_added_to_hass(self) -> None:
# Do this last or else the entity registry update listener has been installed
await super().async_internal_added_to_hass()

@final
# BaseScannerEntity.state_attributes is @final to keep external subclasses
# from tampering with it; ScannerEntity is an in-tree subclass that
# intentionally extends it with ip/mac/hostname.
@final # type: ignore[misc]
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr = super().state_attributes

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/dhcp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
],
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.6",
"aiodhcpwatcher==1.2.7",
"aiodiscover==3.2.3",
"cached-ipaddress==1.1.1"
]
Expand Down
9 changes: 7 additions & 2 deletions homeassistant/components/esphome/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def async_static_info_updated(
platform: entity_platform.EntityPlatform,
async_add_entities: AddEntitiesCallback,
info_type: type[_InfoT],
entity_type: type[_EntityT],
entity_type: Callable[[RuntimeEntryData, EntityInfo, type[_StateT]], _EntityT],
state_type: type[_StateT],
infos: list[EntityInfo],
) -> None:
Expand Down Expand Up @@ -188,14 +188,19 @@ async def platform_async_setup_entry(
async_add_entities: AddEntitiesCallback,
*,
info_type: type[_InfoT],
entity_type: type[_EntityT],
entity_type: Callable[[RuntimeEntryData, EntityInfo, type[_StateT]], _EntityT],
state_type: type[_StateT],
info_filter: Callable[[_InfoT], bool] | None = None,
) -> None:
"""Set up an esphome platform.

This method is in charge of receiving, distributing and storing
info and state updates.

`entity_type` is any callable that builds an entity from
`(entry_data, info, state_type)`. A regular entity class satisfies this,
and platforms with multiple entity classes can pass a factory function
that picks the class per static info.
"""
entry_data = entry.runtime_data
entry_data.info[info_type] = {}
Expand Down
99 changes: 88 additions & 11 deletions homeassistant/components/esphome/infrared.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
"""Infrared platform for ESPHome."""

from functools import partial
import functools
import logging
from typing import TYPE_CHECKING

from aioesphomeapi import EntityState, InfraredCapability, InfraredInfo
from aioesphomeapi import EntityInfo, EntityState, InfraredCapability, InfraredInfo
from aioesphomeapi.client import InfraredRFReceiveEventModel

from homeassistant.components.infrared import InfraredCommand, InfraredEmitterEntity
from homeassistant.core import callback
from homeassistant.components.infrared import (
InfraredCommand,
InfraredEmitterEntity,
InfraredReceivedSignal,
InfraredReceiverEntity,
)
from homeassistant.core import CALLBACK_TYPE, callback

from .entity import (
EsphomeEntity,
convert_api_error_ha_error,
platform_async_setup_entry,
)
from .entry_data import RuntimeEntryData

_LOGGER = logging.getLogger(__name__)

PARALLEL_UPDATES = 0


class EsphomeInfraredEntity(
EsphomeEntity[InfraredInfo, EntityState], InfraredEmitterEntity
):
"""ESPHome infrared emitter entity using native API."""
class _EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState]):
"""Common base for ESPHome infrared entities."""

@callback
def _on_device_update(self) -> None:
Expand All @@ -32,6 +38,10 @@ def _on_device_update(self) -> None:
# Infrared entities should go available as soon as the device comes online
self.async_write_ha_state()


class EsphomeInfraredEmitterEntity(_EsphomeInfraredEntity, InfraredEmitterEntity):
"""ESPHome infrared emitter entity using native API."""

@convert_api_error_ha_error
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command."""
Expand All @@ -46,10 +56,77 @@ async def async_send_command(self, command: InfraredCommand) -> None:
)


async_setup_entry = partial(
class EsphomeInfraredReceiverEntity(_EsphomeInfraredEntity, InfraredReceiverEntity):
"""ESPHome infrared receiver entity using native API."""

_unsub_receive: CALLBACK_TYPE | None = None

async def async_added_to_hass(self) -> None:
"""Register callbacks including IR receive subscription."""
await super().async_added_to_hass()
self._async_subscribe_receive()

async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from the device on entity removal."""
await super().async_will_remove_from_hass()
if self._unsub_receive is not None:
self._unsub_receive()
self._unsub_receive = None

@callback
def _async_subscribe_receive(self) -> None:
"""Subscribe to IR receive events if the device is connected."""
# Subscribing requires an active API connection; defer to
# _on_device_update when the device is not (yet) available.
if self._unsub_receive is not None or not self._entry_data.available:
return
self._unsub_receive = self._client.subscribe_infrared_rf_receive(
self._on_infrared_rf_receive
)

@callback
def _on_device_update(self) -> None:
"""Call when device updates or entry data changes."""
super()._on_device_update()
if self._entry_data.available:
self._async_subscribe_receive()
elif self._unsub_receive is not None:
self._unsub_receive = None

@callback
def _on_infrared_rf_receive(self, event: InfraredRFReceiveEventModel) -> None:
"""Handle a received IR signal from the device."""
if (
event.key != self._static_info.key
or event.device_id != self._static_info.device_id
):
return
self._handle_received_signal(InfraredReceivedSignal(timings=event.timings))


def _make_infrared_entity(
entry_data: RuntimeEntryData,
info: EntityInfo,
state_type: type[EntityState],
) -> _EsphomeInfraredEntity:
"""Build the right infrared entity based on the InfraredInfo capabilities."""
if TYPE_CHECKING:
assert isinstance(info, InfraredInfo)
cls = (
EsphomeInfraredReceiverEntity
if info.capabilities & InfraredCapability.RECEIVER
else EsphomeInfraredEmitterEntity
)
return cls(entry_data, info, state_type)


async_setup_entry = functools.partial(
platform_async_setup_entry,
info_type=InfraredInfo,
entity_type=EsphomeInfraredEntity,
entity_type=_make_infrared_entity,
state_type=EntityState,
info_filter=lambda info: bool(info.capabilities & InfraredCapability.TRANSMITTER),
info_filter=lambda info: bool(
info.capabilities
& (InfraredCapability.TRANSMITTER | InfraredCapability.RECEIVER)
),
)
10 changes: 6 additions & 4 deletions homeassistant/components/homeassistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,12 @@ async def async_handle_reload_config(call: ServiceCall) -> None:
"""Service handler for reloading core config."""
try:
conf = await conf_util.async_hass_config_yaml(hass)
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error(err)
return
except (HomeAssistantError, FileNotFoundError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="core_config_reload_failed",
translation_placeholders={"error": str(err)},
) from err

# auth only processed during startup
await core_config.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
Expand Down
10 changes: 6 additions & 4 deletions homeassistant/components/homeassistant/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ async def reload_config(call: ServiceCall) -> None:
"""Reload the scene config."""
try:
config = await conf_util.async_hass_config_yaml(hass)
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error(err)
return
except (HomeAssistantError, FileNotFoundError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="scene_config_reload_failed",
translation_placeholders={"error": str(err)},
) from err

integration = await async_get_integration(hass, SCENE_DOMAIN)

Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/homeassistant/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"config_validator_unknown_err": {
"message": "Unknown error calling {domain} config validator - {error}."
},
"core_config_reload_failed": {
"message": "Failed to reload the Home Assistant Core configuration - {error}"
},
"max_length_exceeded": {
"message": "Value {value} for property {property_name} has a maximum length of {max_length} characters."
},
Expand Down Expand Up @@ -48,6 +51,9 @@
"platform_schema_validator_err": {
"message": "Unknown error when validating config for {domain} from integration {p_name} - {error}."
},
"scene_config_reload_failed": {
"message": "Failed to reload the Home Assistant scene platform configuration - {error}"
},
"service_config_entry_not_found": {
"message": "Integration {domain} config entry with ID {entry_id} was not found."
},
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/inkbird/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@

FALLBACK_POLL_INTERVAL = timedelta(seconds=180)

# IBS-TH2 broadcasts every ~20-30s and only carries sensor data in the scan
# response, so the default 10s active window misses the device most cycles.
# 25s covers one full broadcast interval with margin to absorb jitter.
ACTIVE_SCAN_DURATION = 25.0


class INKBIRDActiveBluetoothProcessorCoordinator(
ActiveBluetoothProcessorCoordinator[SensorUpdate]
Expand Down Expand Up @@ -57,6 +62,7 @@ def __init__(
needs_poll_method=self._async_needs_poll,
poll_method=self._async_poll_data,
connectable=False, # Polling only happens if active scanning is disabled
scan_duration=ACTIVE_SCAN_DURATION,
)

async def async_init(self) -> None:
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/inkbird/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ async def async_setup_entry(
INKBIRDBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
entry.async_on_unload(
entry.runtime_data.async_register_processor(processor, SensorEntityDescription)
)


class INKBIRDBluetoothSensorEntity(
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/kegtron/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ async def async_setup_entry(
KegtronBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload(
coordinator.async_register_processor(processor, SensorEntityDescription)
)


class KegtronBluetoothSensorEntity(
Expand Down
Loading
Loading