Compare commits

..

2 Commits
rc ... 2026.3.1

Author SHA1 Message Date
Franck Nijhof
f552b8221f 2026.3.1 (#165001) 2026-03-06 22:10:34 +01:00
Franck Nijhof
2f9faa53a1 2026.3.0 (#164757) 2026-03-04 20:17:05 +01:00
58 changed files with 140 additions and 631 deletions

View File

@@ -1,5 +1,6 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -24,15 +25,20 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model = self.device.model
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=self.device.model,
model=model,
model_id=self.device.device_type,
manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=self.device.software_version,
serial_number=serial_num,
sw_version=(
self.device.software_version
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.0.1"]
"requirements": ["aioamazondevices==13.0.0"]
}

View File

@@ -101,10 +101,7 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None
await method(self.device, state)
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
self.async_write_ha_state()
await self.coordinator.async_request_refresh()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.1"]
"requirements": ["pyanglianwater==3.1.0"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientError
from aiohttp import ClientResponseError
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
@@ -13,12 +13,7 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -50,18 +45,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (
AugustApiAIOHTTPError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
}

View File

@@ -15,7 +15,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.10"],
"requirements": ["PyChromecast==14.0.9"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.1"]
"requirements": ["aiocomelit==2.0.0"]
}

View File

@@ -133,20 +133,26 @@ async def _async_wifi_entities_list(
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict[int, dict[str, Any]] = {}
networks: dict = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2):
networks[i] = network_info
networks[i] = {
"ssid": network_info["NewSSID"],
"bssid": network_info["NewBSSID"],
"standard": network_info["NewStandard"],
"enabled": network_info["NewEnable"],
"status": network_info["NewStatus"],
}
for i, network in networks.copy().items():
networks[i]["switch_name"] = network["NewSSID"]
networks[i]["switch_name"] = network["ssid"]
if (
len(
[
j
for j, n in networks.items()
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
if slugify(n["ssid"]) == slugify(network["ssid"])
]
)
> 1
@@ -428,11 +434,13 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
for key, attr in attributes_dict.items():
self._attributes[attr] = self.port_mapping[key]
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
await self._avm_wrapper.async_add_port_mapping(
resp = await self._avm_wrapper.async_add_port_mapping(
self.connection_type, self.port_mapping
)
return bool(resp is not None)
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
@@ -517,11 +525,12 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False)
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
"""Handle switch state change request."""
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
self._avm_wrapper.devices[self._mac].wan_access = turn_on
self.async_write_ha_state()
return True
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
@@ -532,11 +541,10 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
avm_wrapper: AvmWrapper,
device_friendly_name: str,
network_num: int,
network_data: dict[str, Any],
network_data: dict,
) -> None:
"""Init Fritz Wifi switch."""
self._avm_wrapper = avm_wrapper
self._wifi_info = network_data
self._attributes = {}
self._attr_entity_category = EntityCategory.CONFIG
@@ -552,7 +560,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
type=SWITCH_TYPE_WIFINETWORK,
callback_update=self._async_fetch_update,
callback_switch=self._async_switch_on_off_executor,
init_state=network_data["NewEnable"],
init_state=network_data["enabled"],
)
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
@@ -579,9 +587,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
self._attributes["mac_address_control"] = wifi_info[
"NewMACAddressControlEnabled"
]
self._wifi_info = wifi_info
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
"""Handle wifi switch."""
self._wifi_info["NewEnable"] = turn_on
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260312.0"]
"requirements": ["home-assistant-frontend==20260304.0"]
}

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
}

View File

@@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
"requirements": ["govee-local-api==2.4.0"]
"requirements": ["govee-local-api==2.3.0"]
}

View File

@@ -97,6 +97,7 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
IntellifireSensorEntityDescription(
key="timer_end_timestamp",
translation_key="timer_end_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=_time_remaining_to_timestamp,
),

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==2.0.3"]
"requirements": ["pyjvcprojector==2.0.1"]
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==13.2.2"]
"requirements": ["ical==13.2.0"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==13.2.2"]
"requirements": ["ical==13.2.0"]
}

View File

@@ -188,7 +188,6 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
finished = 522, 11012
extra_dry = 523
hand_iron = 524
hygiene_drying = 525
moisten = 526
thermo_spin = 527
timed_drying = 528

View File

@@ -1006,7 +1006,6 @@
"heating_up_phase": "Heating up phase",
"hot_milk": "Hot milk",
"hygiene": "Hygiene",
"hygiene_drying": "Hygiene drying",
"interim_rinse": "Interim rinse",
"keep_warm": "Keep warm",
"keeping_warm": "Keeping warm",

View File

@@ -163,6 +163,8 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
latitude: float | None
longitude: float | None
gps_accuracy: float
# Reset manually set location to allow automatic zone detection
self._attr_location_name = None
if isinstance(
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
) and isinstance(

View File

@@ -19,5 +19,6 @@ async def async_get_config_entry_diagnostics(
return {
"device_info": client.device_info,
"vehicles": client.vehicles,
"ct_connected": client.ct_connected,
"cap_available": client.cap_available,
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.7.0"]
"requirements": ["ohme==1.6.0"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.7"]
"requirements": ["onedrive-personal-sdk==0.1.5"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.7"]
"requirements": ["onedrive-personal-sdk==0.1.5"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.9.0"]
"requirements": ["python-otbr-api==2.8.0"]
}

View File

@@ -159,12 +159,8 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
# Logic that can be reverted back once the new unique ID is in
existing_entry = await self.async_set_unique_id(
user_input[CONF_API_TOKEN]
)
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
self._abort_if_unique_id_configured()
return self.async_update_reload_and_abort(
reconf_entry,
data_updates={

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.33"]
"requirements": ["pyportainer==1.0.31"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==13.2.2"]
"requirements": ["ical==13.2.0"]
}

View File

@@ -34,5 +34,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.7.0"]
"requirements": ["pysmartthings==3.6.0"]
}

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -81,7 +81,6 @@ clean_area:
selector:
area:
multiple: true
reorder: true
send_command:
target:

View File

@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
Keys.WARNING: VictronBLESensorEntityDescription(
key=Keys.WARNING,
device_class=SensorDeviceClass.ENUM,
translation_key="warning",
translation_key="alarm",
options=ALARM_OPTIONS,
),
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(

View File

@@ -248,24 +248,7 @@
"name": "[%key:component::victron_ble::common::starter_voltage%]"
},
"warning": {
"name": "Warning",
"state": {
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
}
"name": "Warning"
},
"yield_today": {
"name": "Yield today"

View File

@@ -78,7 +78,6 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
data,
session,
)
self._session = session
# Last resort as no MAC or S/N can be retrieved via API
self._id = config_entry.unique_id
@@ -136,15 +135,11 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
try:
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
_LOGGER.debug(
"Session cookies missing for host %s, re-login",
self.api.base_url.host,
)
await self.api.login()
await self.api.login()
raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data()
data_wifi = await self.api.get_wifi_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -104,7 +104,6 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
await self.coordinator.api.set_wifi_status(
status, self.entity_description.typology, self.entity_description.band
)
await self.coordinator.async_request_refresh()
except CannotAuthenticate as err:
self.coordinator.config_entry.async_start_reauth(self.hass)
raise HomeAssistantError(

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientError
from aiohttp import ClientResponseError
from yalexs.const import Brand
from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND
@@ -15,12 +15,7 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -47,18 +42,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_yale(hass, entry, yale_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
except (
YaleApiError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
except (YaleApiError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
}

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["yalexs-ble==3.2.8"]
"requirements": ["yalexs-ble==3.2.7"]
}

View File

@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.0.2", "serialx==0.6.2"],
"requirements": ["zha==1.0.1", "serialx==0.6.2"],
"usb": [
{
"description": "*2652*",

View File

@@ -87,9 +87,6 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
_current_position_value: ZwaveValue | None = None
_target_position_value: ZwaveValue | None = None
_stop_position_value: ZwaveValue | None = None
# Keep track of the target position for legacy devices
# that don't include the targetValue in their reports.
_commanded_target_position: int | None = None
def _set_position_values(
self,
@@ -156,19 +153,12 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
if not self._attr_is_opening and not self._attr_is_closing:
return
if (current := self._current_position_value) is None or current.value is None:
return
# Prefer the Z-Wave targetValue property when the device reports it.
# Legacy multilevel switches only report currentValue, so fall back to
# the target position we commanded when targetValue is not available.
target_val = (
t.value
if (t := self._target_position_value) is not None and t.value is not None
else self._commanded_target_position
)
if target_val is not None and current.value == target_val:
if (
(current := self._current_position_value) is not None
and (target := self._target_position_value) is not None
and current.value is not None
and current.value == target.value
):
self._attr_is_opening = False
self._attr_is_closing = False
@@ -213,8 +203,6 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
else:
return
self._commanded_target_position = target_position
self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs: Any) -> None:

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)

View File

@@ -181,24 +181,15 @@ class RestoreStateData:
}
# Start with the currently registered states
stored_states: list[StoredState] = []
for entity_id, entity in self.entities.items():
if entity_id not in current_states_by_entity_id:
continue
try:
extra_data = entity.extra_restore_state_data
except Exception:
_LOGGER.exception(
"Error getting extra restore state data for %s", entity_id
)
continue
stored_states.append(
StoredState(
current_states_by_entity_id[entity_id],
extra_data,
now,
)
stored_states = [
StoredState(
current_states_by_entity_id[entity_id],
entity.extra_restore_state_data,
now,
)
for entity_id, entity in self.entities.items()
if entity_id in current_states_by_entity_id
]
expiration_time = now - STATE_EXPIRATION
for entity_id, stored_state in self.last_states.items():
@@ -228,8 +219,6 @@ class RestoreStateData:
)
except HomeAssistantError as exc:
_LOGGER.error("Error saving current states", exc_info=exc)
except Exception:
_LOGGER.exception("Unexpected error saving current states")
@callback
def async_setup_dump(self, *args: Any) -> None:
@@ -269,15 +258,13 @@ class RestoreStateData:
@callback
def async_restore_entity_removed(
self,
entity_id: str,
state: State | None,
extra_data: ExtraStoredData | None,
self, entity_id: str, extra_data: ExtraStoredData | None
) -> None:
"""Unregister this entity from saving state."""
# When an entity is being removed from hass, store its last state. This
# allows us to support state restoration if the entity is removed, then
# re-added while hass is still running.
state = self.hass.states.get(entity_id)
# To fully mimic all the attribute data types when loaded from storage,
# we're going to serialize it to JSON and then re-load it.
if state is not None:
@@ -300,18 +287,8 @@ class RestoreEntity(Entity):
async def async_internal_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
try:
extra_data = self.extra_restore_state_data
except Exception:
_LOGGER.exception(
"Error getting extra restore state data for %s", self.entity_id
)
state = None
extra_data = None
else:
state = self.hass.states.get(self.entity_id)
async_get(self.hass).async_restore_entity_removed(
self.entity_id, state, extra_data
self.entity_id, self.extra_restore_state_data
)
await super().async_internal_will_remove_from_hass()

View File

@@ -301,7 +301,6 @@ class AreaSelectorConfig(BaseSelectorConfig, total=False):
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
multiple: bool
reorder: bool
@SELECTORS.register("area")
@@ -321,7 +320,6 @@ class AreaSelector(Selector[AreaSelectorConfig]):
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("reorder", default=False): cv.boolean,
}
)

View File

@@ -40,7 +40,7 @@ habluetooth==5.8.0
hass-nabucasa==1.15.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260312.0
home-assistant-frontend==20260304.0
home-assistant-intents==2026.3.3
httpx==0.28.1
ifaddr==0.2.0
@@ -48,7 +48,7 @@ Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
openai==2.21.0
orjson==3.11.7
orjson==3.11.5
packaging>=23.1
paho-mqtt==2.1.0
Pillow==12.1.1

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.3.2"
version = "2026.3.1"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -65,7 +65,7 @@ dependencies = [
"Pillow==12.1.1",
"propcache==0.4.1",
"pyOpenSSL==25.3.0",
"orjson==3.11.7",
"orjson==3.11.5",
"packaging>=23.1",
"psutil-home-assistant==0.0.1",
"python-slugify==8.0.4",

2
requirements.txt generated
View File

@@ -34,7 +34,7 @@ ifaddr==0.2.0
Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
orjson==3.11.7
orjson==3.11.5
packaging>=23.1
Pillow==12.1.1
propcache==0.4.1

30
requirements_all.txt generated
View File

@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.10
PyChromecast==14.0.9
# homeassistant.components.flume
PyFlume==0.6.5
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.0.1
aioamazondevices==13.0.0
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -224,7 +224,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==2.0.1
aiocomelit==2.0.0
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
@@ -1122,7 +1122,7 @@ gotailwind==0.3.0
govee-ble==0.44.0
# homeassistant.components.govee_light_local
govee-local-api==2.4.0
govee-local-api==2.3.0
# homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2
@@ -1226,7 +1226,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260312.0
home-assistant-frontend==20260304.0
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1271,7 +1271,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.2
ical==13.2.0
# homeassistant.components.caldav
icalendar==6.3.1
@@ -1663,7 +1663,7 @@ odp-amsterdam==6.1.2
oemthermostat==1.1.1
# homeassistant.components.ohme
ohme==1.7.0
ohme==1.6.0
# homeassistant.components.ollama
ollama==0.5.1
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.7
onedrive-personal-sdk==0.1.5
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1938,7 +1938,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.1
pyanglianwater==3.1.0
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -2179,7 +2179,7 @@ pyitachip2ir==0.0.7
pyituran==0.1.5
# homeassistant.components.jvc_projector
pyjvcprojector==2.0.3
pyjvcprojector==2.0.1
# homeassistant.components.kaleidescape
pykaleidescape==1.1.3
@@ -2370,7 +2370,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.33
pyportainer==1.0.31
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2473,7 +2473,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1
# homeassistant.components.smartthings
pysmartthings==3.7.0
pysmartthings==3.6.0
# homeassistant.components.smarty
pysmarty2==0.10.3
@@ -2612,7 +2612,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.9.0
python-otbr-api==2.8.0
# homeassistant.components.overseerr
python-overseerr==0.9.0
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.2.8
yalexs-ble==3.2.7
# homeassistant.components.august
# homeassistant.components.yale
@@ -3347,7 +3347,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.0.2
zha==1.0.1
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.10
PyChromecast==14.0.9
# homeassistant.components.flume
PyFlume==0.6.5
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.0.1
aioamazondevices==13.0.0
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==2.0.1
aiocomelit==2.0.0
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
@@ -998,7 +998,7 @@ gotailwind==0.3.0
govee-ble==0.44.0
# homeassistant.components.govee_light_local
govee-local-api==2.4.0
govee-local-api==2.3.0
# homeassistant.components.gpsd
gps3==0.33.3
@@ -1087,7 +1087,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260312.0
home-assistant-frontend==20260304.0
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1126,7 +1126,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.2
ical==13.2.0
# homeassistant.components.caldav
icalendar==6.3.1
@@ -1449,7 +1449,7 @@ objgraph==3.5.0
odp-amsterdam==6.1.2
# homeassistant.components.ohme
ohme==1.7.0
ohme==1.6.0
# homeassistant.components.ollama
ollama==0.5.1
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.7
onedrive-personal-sdk==0.1.5
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1669,7 +1669,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.1
pyanglianwater==3.1.0
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -1856,7 +1856,7 @@ pyisy==3.4.1
pyituran==0.1.5
# homeassistant.components.jvc_projector
pyjvcprojector==2.0.3
pyjvcprojector==2.0.1
# homeassistant.components.kaleidescape
pykaleidescape==1.1.3
@@ -2020,7 +2020,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.33
pyportainer==1.0.31
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2102,7 +2102,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1
# homeassistant.components.smartthings
pysmartthings==3.7.0
pysmartthings==3.6.0
# homeassistant.components.smarty
pysmarty2==0.10.3
@@ -2208,7 +2208,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.9.0
python-otbr-api==2.8.0
# homeassistant.components.overseerr
python-overseerr==0.9.0
@@ -2783,7 +2783,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.2.8
yalexs-ble==3.2.7
# homeassistant.components.august
# homeassistant.components.yale
@@ -2817,7 +2817,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.0.2
zha==1.0.1
# homeassistant.components.zinvolt
zinvolt==0.3.0

View File

@@ -2,7 +2,10 @@
from unittest.mock import AsyncMock
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.const.devices import (
SPEAKER_GROUP_DEVICE_TYPE,
SPEAKER_GROUP_FAMILY,
)
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
import pytest
@@ -114,7 +117,7 @@ async def test_alexa_dnd_group_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model="Speaker Group",
model=SPEAKER_GROUP_DEVICE_TYPE,
entry_type=dr.DeviceEntryType.SERVICE,
)
@@ -153,7 +156,7 @@ async def test_alexa_unsupported_notification_sensor_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model="Speaker Group",
model=SPEAKER_GROUP_DEVICE_TYPE,
entry_type=dr.DeviceEntryType.SERVICE,
)

View File

@@ -2,7 +2,7 @@
from unittest.mock import Mock, patch
from aiohttp import ClientError, ClientResponseError
from aiohttp import ClientResponseError
import pytest
from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
@@ -18,11 +18,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
@@ -308,59 +304,3 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
"""Test OAuth token request reauth error starts a reauth flow."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestReauthError(
request_info=Mock(real_url="https://auth.august.com/access_token"),
status=401,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "pick_implementation"
assert flows[0]["context"]["source"] == "reauth"
async def test_oauth_token_request_transient_error_is_retryable(
hass: HomeAssistant,
) -> None:
"""Test OAuth token transient request error marks entry for setup retry."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestTransientError(
request_info=Mock(real_url="https://auth.august.com/access_token"),
status=500,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
"""Test OAuth transport client errors mark entry for setup retry."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=ClientError("connection error"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -396,11 +396,6 @@ async def test_switch_device_no_ip_address(
"async_set_deflection_enable",
STATE_ON,
),
(
"switch.mock_title_wi_fi_mywifi",
"async_set_wlan_configuration",
STATE_ON,
),
],
)
async def test_switch_turn_on_off(

View File

@@ -427,7 +427,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -464,6 +466,7 @@
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'timestamp',
'friendly_name': 'IntelliFire Timer end',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_timer_end',

View File

@@ -644,31 +644,6 @@ async def test_setting_device_tracker_location_via_abbr_reset_message(
assert state.attributes["source_type"] == "gps"
assert state.state == STATE_HOME
# Override the GPS state via a direct state update
async_fire_mqtt_message(hass, "test-topic", "office")
state = hass.states.get("device_tracker.test")
assert state.state == "office"
# Test a GPS attributes update without a reset
async_fire_mqtt_message(
hass,
"attributes-topic",
'{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}',
)
state = hass.states.get("device_tracker.test")
assert state.state == "office"
# Reset the manual set location
# This should calculate the location from GPS attributes
async_fire_mqtt_message(hass, "test-topic", "reset")
state = hass.states.get("device_tracker.test")
assert state.attributes["latitude"] == 32.87336
assert state.attributes["longitude"] == -117.22743
assert state.attributes["gps_accuracy"] == 1.5
assert state.attributes["source_type"] == "gps"
assert state.state == STATE_HOME
async def test_setting_blocked_attribute_via_mqtt_json_message(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator

View File

@@ -2,6 +2,7 @@
# name: test_diagnostics
dict({
'cap_available': True,
'ct_connected': True,
'device_info': dict({
'model': 'Home Pro',
'name': 'Ohme Home Pro',

View File

@@ -263,28 +263,20 @@ async def test_full_flow_reconfigure_unique_id(
) -> None:
"""Test the full flow of the config flow, this time with a known unique ID."""
mock_config_entry.add_to_hass(hass)
other_entry = MockConfigEntry(
domain=DOMAIN,
title="Portainer other",
data=USER_INPUT_RECONFIGURE,
unique_id=USER_INPUT_RECONFIGURE[CONF_API_TOKEN],
)
other_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT_RECONFIGURE,
user_input=MOCK_USER_SETUP,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert mock_config_entry.data[CONF_API_TOKEN] == "test_api_token"
assert mock_config_entry.data[CONF_URL] == "https://127.0.0.1:9000/"
assert mock_config_entry.data[CONF_VERIFY_SSL] is True
assert len(mock_setup_entry.mock_calls) == 0

View File

@@ -2,7 +2,7 @@
from unittest.mock import Mock, patch
from aiohttp import ClientError, ClientResponseError
from aiohttp import ClientResponseError
import pytest
from yalexs.exceptions import InvalidAuth, YaleApiError
@@ -17,11 +17,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -258,58 +254,3 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
"""Test OAuth token request reauth error starts a reauth flow."""
entry = await mock_yale_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestReauthError(
request_info=Mock(real_url="https://auth.yale.com/access_token"),
status=401,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["context"]["source"] == "reauth"
async def test_oauth_token_request_transient_error_is_retryable(
hass: HomeAssistant,
) -> None:
"""Test OAuth token transient request error marks entry for setup retry."""
entry = await mock_yale_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestTransientError(
request_info=Mock(real_url="https://auth.yale.com/access_token"),
status=500,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
"""Test OAuth transport client errors mark entry for setup retry."""
entry = await mock_yale_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=ClientError("connection error"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -1676,173 +1676,3 @@ async def test_multilevel_switch_cover_moving_state_none_result(
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSED
async def test_multilevel_switch_cover_unsupervised_no_target_value_update(
hass: HomeAssistant,
client: MagicMock,
chain_actuator_zws12: Node,
integration: MockConfigEntry,
) -> None:
"""Test cover transitions to CLOSED/OPEN without targetValue updates.
Regression test for issue #164915: covers with no supervision where the
device only sends currentValue updates (no targetValue updates) should
still properly transition from CLOSING/OPENING to CLOSED/OPEN once the
current position reaches the fully closed or fully open position.
"""
node = chain_actuator_zws12
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state
assert state.state == CoverState.CLOSED
# Set cover to fully open position via an unsolicited device report.
# This mirrors the log sequence: currentValue 0 => 99
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 99,
"prevValue": 0,
"propertyName": "currentValue",
},
},
)
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPEN
# Simulate SUCCESS_UNSUPERVISED (no Supervision CC on device).
client.async_send_command.return_value = {
"result": {"status": SetValueStatus.SUCCESS_UNSUPERVISED}
}
# Close cover should optimistically enter CLOSING.
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: WINDOW_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSING
# Simulate intermediate report from device (currentValue only, no targetValue).
# Log sequence: currentValue 99 => 78
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 78,
"prevValue": 99,
"propertyName": "currentValue",
},
},
)
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSING
# Simulate device reaching the fully closed position.
# Log sequence: currentValue 78 => 0
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 0,
"prevValue": 78,
"propertyName": "currentValue",
},
},
)
)
# Cover must leave CLOSING and report CLOSED.
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSED
# Now test the opening direction with the same conditions.
# Log sequence: currentValue 0 => 18 => 99
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: WINDOW_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPENING
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 18,
"prevValue": 0,
"propertyName": "currentValue",
},
},
)
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPENING
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 99,
"prevValue": 18,
"propertyName": "currentValue",
},
},
)
)
# Cover must leave OPENING and report OPEN.
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPEN

View File

@@ -6,8 +6,6 @@ import logging
from typing import Any
from unittest.mock import Mock, patch
import pytest
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
@@ -18,7 +16,6 @@ from homeassistant.helpers.reload import async_get_platform_without_config_entry
from homeassistant.helpers.restore_state import (
DATA_RESTORE_STATE,
STORAGE_KEY,
ExtraStoredData,
RestoreEntity,
RestoreStateData,
StoredState,
@@ -345,12 +342,8 @@ async def test_dump_data(hass: HomeAssistant) -> None:
assert state1["state"]["state"] == "off"
@pytest.mark.parametrize(
"exception",
[HomeAssistantError, RuntimeError],
)
async def test_dump_error(hass: HomeAssistant, exception: type[Exception]) -> None:
"""Test that errors during save are caught."""
async def test_dump_error(hass: HomeAssistant) -> None:
"""Test that we cache data."""
states = [
State("input_boolean.b0", "on"),
State("input_boolean.b1", "on"),
@@ -375,7 +368,7 @@ async def test_dump_error(hass: HomeAssistant, exception: type[Exception]) -> No
with patch(
"homeassistant.helpers.restore_state.Store.async_save",
side_effect=exception,
side_effect=HomeAssistantError,
) as mock_write_data:
await data.async_dump_states()
@@ -541,89 +534,3 @@ async def test_restore_entity_end_to_end(
assert len(storage_data) == 1
assert storage_data[0]["state"]["entity_id"] == entity_id
assert storage_data[0]["state"]["state"] == "stored"
async def test_dump_states_with_failing_extra_data(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a failing extra_restore_state_data skips only that entity."""
class BadRestoreEntity(RestoreEntity):
"""Entity that raises on extra_restore_state_data."""
@property
def extra_restore_state_data(self) -> ExtraStoredData | None:
raise RuntimeError("Unexpected error")
states = [
State("input_boolean.good", "on"),
State("input_boolean.bad", "on"),
]
platform = MockEntityPlatform(hass, domain="input_boolean")
good_entity = RestoreEntity()
good_entity.hass = hass
good_entity.entity_id = "input_boolean.good"
await platform.async_add_entities([good_entity])
bad_entity = BadRestoreEntity()
bad_entity.hass = hass
bad_entity.entity_id = "input_boolean.bad"
await platform.async_add_entities([bad_entity])
for state in states:
hass.states.async_set(state.entity_id, state.state, state.attributes)
data = async_get(hass)
with patch(
"homeassistant.helpers.restore_state.Store.async_save"
) as mock_write_data:
await data.async_dump_states()
assert mock_write_data.called
written_states = mock_write_data.mock_calls[0][1][0]
# Only the good entity should be saved
assert len(written_states) == 1
state0 = json_round_trip(written_states[0])
assert state0["state"]["entity_id"] == "input_boolean.good"
assert state0["state"]["state"] == "on"
assert "Error getting extra restore state data for input_boolean.bad" in caplog.text
async def test_entity_removal_with_failing_extra_data(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that entity removal succeeds even if extra_restore_state_data raises."""
class BadRestoreEntity(RestoreEntity):
"""Entity that raises on extra_restore_state_data."""
@property
def extra_restore_state_data(self) -> ExtraStoredData | None:
raise RuntimeError("Unexpected error")
platform = MockEntityPlatform(hass, domain="input_boolean")
entity = BadRestoreEntity()
entity.hass = hass
entity.entity_id = "input_boolean.bad"
await platform.async_add_entities([entity])
hass.states.async_set("input_boolean.bad", "on")
data = async_get(hass)
assert "input_boolean.bad" in data.entities
await entity.async_remove()
# Entity should be unregistered
assert "input_boolean.bad" not in data.entities
# No last state should be saved since extra data failed
assert "input_boolean.bad" not in data.last_states
assert "Error getting extra restore state data for input_boolean.bad" in caplog.text

View File

@@ -383,7 +383,7 @@ def test_entity_selector_schema_error(schema) -> None:
(None,),
),
(
{"multiple": True, "reorder": True},
{"multiple": True},
((["abc123", "def456"],)),
(None, "abc123", ["abc123", None]),
),