Migrate climate attributes to own entities in AVM Fritz!SmartHome (#143394)

* migrate climate attributes to own entities

* add a comment to make it searchable

* Apply suggestions from code review

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Apply suggestions from code review

* update snapshots

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Michael 2025-04-30 18:37:58 +02:00 committed by GitHub
parent 6a514ac2de
commit e53f380710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 247 additions and 3 deletions

View File

@ -55,6 +55,32 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
suitable=lambda device: device.device_lock is not None, suitable=lambda device: device.device_lock is not None,
is_on=lambda device: not device.device_lock, is_on=lambda device: not device.device_lock,
), ),
FritzBinarySensorEntityDescription(
key="battery_low",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
suitable=lambda device: device.battery_low is not None,
is_on=lambda device: device.battery_low,
entity_registry_enabled_default=False,
),
FritzBinarySensorEntityDescription(
key="holiday_active",
translation_key="holiday_active",
suitable=lambda device: device.holiday_active is not None,
is_on=lambda device: device.holiday_active,
),
FritzBinarySensorEntityDescription(
key="summer_active",
translation_key="summer_active",
suitable=lambda device: device.summer_active is not None,
is_on=lambda device: device.summer_active,
),
FritzBinarySensorEntityDescription(
key="window_open",
translation_key="window_open",
suitable=lambda device: device.window_open is not None,
is_on=lambda device: device.window_open,
),
) )

View File

@ -214,6 +214,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
@property @property
def extra_state_attributes(self) -> ClimateExtraAttributes: def extra_state_attributes(self) -> ClimateExtraAttributes:
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
# deprecated with #143394, can be removed in 2025.11
attrs: ClimateExtraAttributes = { attrs: ClimateExtraAttributes = {
ATTR_STATE_BATTERY_LOW: self.data.battery_low, ATTR_STATE_BATTERY_LOW: self.data.battery_low,
} }

View File

@ -1,5 +1,28 @@
{ {
"entity": { "entity": {
"binary_sensor": {
"holiday_active": {
"default": "mdi:bag-suitcase-outline",
"state": {
"on": "mdi:bag-suitcase-outline",
"off": "mdi:bag-suitcase-off-outline"
}
},
"summer_active": {
"default": "mdi:radiator-off",
"state": {
"on": "mdi:radiator-off",
"off": "mdi:radiator"
}
},
"window_open": {
"default": "mdi:window-open",
"state": {
"on": "mdi:window-open",
"off": "mdi:window-closed"
}
}
},
"climate": { "climate": {
"thermostat": { "thermostat": {
"state_attributes": { "state_attributes": {

View File

@ -55,7 +55,10 @@
"binary_sensor": { "binary_sensor": {
"alarm": { "name": "Alarm" }, "alarm": { "name": "Alarm" },
"device_lock": { "name": "Button lock via UI" }, "device_lock": { "name": "Button lock via UI" },
"lock": { "name": "Button lock on device" } "holiday_active": { "name": "Holiday mode" },
"lock": { "name": "Button lock on device" },
"summer_active": { "name": "Summer mode" },
"window_open": { "name": "Open window detected" }
}, },
"climate": { "climate": {
"thermostat": { "thermostat": {

View File

@ -47,6 +47,54 @@
'state': 'on', 'state': 'on',
}) })
# --- # ---
# name: test_setup[binary_sensor.fake_name_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.fake_name_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'fritzbox',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '12345 1234567_battery_low',
'unit_of_measurement': None,
})
# ---
# name: test_setup[binary_sensor.fake_name_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'fake_name Battery',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.fake_name_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_setup[binary_sensor.fake_name_button_lock_on_device-entry] # name: test_setup[binary_sensor.fake_name_button_lock_on_device-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -143,3 +191,144 @@
'state': 'off', 'state': 'off',
}) })
# --- # ---
# name: test_setup[binary_sensor.fake_name_holiday_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.fake_name_holiday_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Holiday mode',
'platform': 'fritzbox',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'holiday_active',
'unique_id': '12345 1234567_holiday_active',
'unit_of_measurement': None,
})
# ---
# name: test_setup[binary_sensor.fake_name_holiday_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'fake_name Holiday mode',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.fake_name_holiday_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_setup[binary_sensor.fake_name_open_window_detected-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.fake_name_open_window_detected',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Open window detected',
'platform': 'fritzbox',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'window_open',
'unique_id': '12345 1234567_window_open',
'unit_of_measurement': None,
})
# ---
# name: test_setup[binary_sensor.fake_name_open_window_detected-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'fake_name Open window detected',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.fake_name_open_window_detected',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_setup[binary_sensor.fake_name_summer_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.fake_name_summer_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Summer mode',
'platform': 'fritzbox',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'summer_active',
'unique_id': '12345 1234567_summer_active',
'unit_of_measurement': None,
})
# ---
# name: test_setup[binary_sensor.fake_name_summer_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'fake_name Summer mode',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.fake_name_summer_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -4,6 +4,7 @@ from datetime import timedelta
from unittest import mock from unittest import mock
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -23,6 +24,7 @@ from tests.common import async_fire_time_changed, snapshot_platform
ENTITY_ID = f"{BINARY_SENSOR_DOMAIN}.{CONF_FAKE_NAME}" ENTITY_ID = f"{BINARY_SENSOR_DOMAIN}.{CONF_FAKE_NAME}"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_setup( async def test_setup(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,

View File

@ -105,7 +105,7 @@ async def test_coordinator_automatic_registry_cleanup(
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 11 assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 19
assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2
fritz().get_devices.return_value = [ fritz().get_devices.return_value = [
@ -119,5 +119,5 @@ async def test_coordinator_automatic_registry_cleanup(
async_fire_time_changed(hass, utcnow() + timedelta(seconds=35)) async_fire_time_changed(hass, utcnow() + timedelta(seconds=35))
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 8 assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 12
assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1 assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1