Move lock and devicelock attributes into sensors for all AVM Fritz!Smarthome entities (#60426)

This commit is contained in:
Michael 2022-01-07 14:46:17 +01:00 committed by GitHub
parent b3f3e7259e
commit 9deebaa65f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 73 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -17,17 +18,8 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
CONF_CONNECTIONS,
CONF_COORDINATOR,
DOMAIN,
LOGGER,
PLATFORMS,
)
from .coordinator import FritzboxDataUpdateCoordinator from .coordinator import FritzboxDataUpdateCoordinator
from .model import FritzExtraAttributes
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -65,6 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"Migrating unique_id [%s] to [%s]", entry.unique_id, new_unique_id "Migrating unique_id [%s] to [%s]", entry.unique_id, new_unique_id
) )
return {"new_unique_id": new_unique_id} return {"new_unique_id": new_unique_id}
if entry.domain == BINARY_SENSOR_DOMAIN and "_" not in entry.unique_id:
new_unique_id = f"{entry.unique_id}_alarm"
LOGGER.info(
"Migrating unique_id [%s] to [%s]", entry.unique_id, new_unique_id
)
return {"new_unique_id": new_unique_id}
return None return None
await async_migrate_entries(hass, entry.entry_id, _update_unique_id) await async_migrate_entries(hass, entry.entry_id, _update_unique_id)
@ -138,11 +137,3 @@ class FritzBoxEntity(CoordinatorEntity):
sw_version=self.device.fw_version, sw_version=self.device.fw_version,
configuration_url=self.coordinator.configuration_url, configuration_url=self.coordinator.configuration_url,
) )
@property
def extra_state_attributes(self) -> FritzExtraAttributes:
"""Return the state attributes of the device."""
return {
ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
ATTR_STATE_LOCKED: self.device.lock,
}

View File

@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ENTITY_CATEGORY_CONFIG
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -44,6 +45,22 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
suitable=lambda device: device.has_alarm, # type: ignore[no-any-return] suitable=lambda device: device.has_alarm, # type: ignore[no-any-return]
is_on=lambda device: device.alert_state, # type: ignore[no-any-return] is_on=lambda device: device.alert_state, # type: ignore[no-any-return]
), ),
FritzBinarySensorEntityDescription(
key="lock",
name="Button Lock on Device",
device_class=BinarySensorDeviceClass.LOCK,
entity_category=ENTITY_CATEGORY_CONFIG,
suitable=lambda device: device.lock is not None,
is_on=lambda device: not device.lock,
),
FritzBinarySensorEntityDescription(
key="device_lock",
name="Button Lock via UI",
device_class=BinarySensorDeviceClass.LOCK,
entity_category=ENTITY_CATEGORY_CONFIG,
suitable=lambda device: device.device_lock is not None,
is_on=lambda device: not device.device_lock,
),
) )
@ -76,8 +93,8 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity):
) -> None: ) -> None:
"""Initialize the FritzBox entity.""" """Initialize the FritzBox entity."""
super().__init__(coordinator, ain, entity_description) super().__init__(coordinator, ain, entity_description)
self._attr_name = self.device.name self._attr_name = f"{self.device.name} {entity_description.name}"
self._attr_unique_id = ain self._attr_unique_id = f"{ain}_{entity_description.key}"
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:

View File

@ -26,9 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity from . import FritzBoxEntity
from .const import ( from .const import (
ATTR_STATE_BATTERY_LOW, ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE, ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN, ATTR_STATE_WINDOW_OPEN,
CONF_COORDINATOR, CONF_COORDINATOR,
@ -176,8 +174,6 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
attrs: ClimateExtraAttributes = { attrs: ClimateExtraAttributes = {
ATTR_STATE_BATTERY_LOW: self.device.battery_low, ATTR_STATE_BATTERY_LOW: self.device.battery_low,
ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
ATTR_STATE_LOCKED: self.device.lock,
} }
# the following attributes are available since fritzos 7 # the following attributes are available since fritzos 7

View File

@ -7,9 +7,7 @@ from typing import Final
from homeassistant.const import Platform from homeassistant.const import Platform
ATTR_STATE_BATTERY_LOW: Final = "battery_low" ATTR_STATE_BATTERY_LOW: Final = "battery_low"
ATTR_STATE_DEVICE_LOCKED: Final = "device_locked"
ATTR_STATE_HOLIDAY_MODE: Final = "holiday_mode" ATTR_STATE_HOLIDAY_MODE: Final = "holiday_mode"
ATTR_STATE_LOCKED: Final = "locked"
ATTR_STATE_SUMMER_MODE: Final = "summer_mode" ATTR_STATE_SUMMER_MODE: Final = "summer_mode"
ATTR_STATE_WINDOW_OPEN: Final = "window_open" ATTR_STATE_WINDOW_OPEN: Final = "window_open"

View File

@ -8,14 +8,8 @@ from typing import TypedDict
from pyfritzhome import FritzhomeDevice from pyfritzhome import FritzhomeDevice
class FritzExtraAttributes(TypedDict): @dataclass
"""TypedDict for sensors extra attributes.""" class ClimateExtraAttributes(TypedDict, total=False):
device_locked: bool
locked: bool
class ClimateExtraAttributes(FritzExtraAttributes, total=False):
"""TypedDict for climates extra attributes.""" """TypedDict for climates extra attributes."""
battery_level: int battery_level: int

View File

@ -15,6 +15,6 @@ MOCK_CONFIG = {
} }
CONF_FAKE_NAME = "fake_name" CONF_FAKE_NAME = "fake_name"
CONF_FAKE_AIN = "fake_ain" CONF_FAKE_AIN = "12345 1234567"
CONF_FAKE_MANUFACTURER = "fake_manufacturer" CONF_FAKE_MANUFACTURER = "fake_manufacturer"
CONF_FAKE_PRODUCTNAME = "fake_productname" CONF_FAKE_PRODUCTNAME = "fake_productname"

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONF_DEVICES, CONF_DEVICES,
PERCENTAGE, PERCENTAGE,
STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
@ -35,13 +36,32 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
) )
state = hass.states.get(ENTITY_ID) state = hass.states.get(f"{ENTITY_ID}_alarm")
assert state assert state
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Alarm"
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.WINDOW assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.WINDOW
assert ATTR_STATE_CLASS not in state.attributes assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{ENTITY_ID}_button_lock_on_device")
assert state
assert state.state == STATE_OFF
assert (
state.attributes[ATTR_FRIENDLY_NAME]
== f"{CONF_FAKE_NAME} Button Lock on Device"
)
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.LOCK
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{ENTITY_ID}_button_lock_via_ui")
assert state
assert state.state == STATE_OFF
assert (
state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Button Lock via UI"
)
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.LOCK
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_battery") state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_battery")
assert state assert state
assert state.state == "23" assert state.state == "23"
@ -58,7 +78,15 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock):
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
) )
state = hass.states.get(ENTITY_ID) state = hass.states.get(f"{ENTITY_ID}_alarm")
assert state
assert state.state == STATE_UNAVAILABLE
state = hass.states.get(f"{ENTITY_ID}_button_lock_on_device")
assert state
assert state.state == STATE_UNAVAILABLE
state = hass.states.get(f"{ENTITY_ID}_button_lock_via_ui")
assert state assert state
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE

View File

@ -23,9 +23,7 @@ from homeassistant.components.climate.const import (
) )
from homeassistant.components.fritzbox.const import ( from homeassistant.components.fritzbox.const import (
ATTR_STATE_BATTERY_LOW, ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE, ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN, ATTR_STATE_WINDOW_OPEN,
DOMAIN as FB_DOMAIN, DOMAIN as FB_DOMAIN,
@ -70,9 +68,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
assert state.attributes[ATTR_PRESET_MODE] is None assert state.attributes[ATTR_PRESET_MODE] is None
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT]
assert state.attributes[ATTR_STATE_BATTERY_LOW] is True assert state.attributes[ATTR_STATE_BATTERY_LOW] is True
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday" assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer" assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer"
assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window" assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window"
assert state.attributes[ATTR_TEMPERATURE] == 19.5 assert state.attributes[ATTR_TEMPERATURE] == 19.5

View File

@ -4,8 +4,10 @@ from __future__ import annotations
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
from pyfritzhome import LoginError from pyfritzhome import LoginError
import pytest
from requests.exceptions import ConnectionError, HTTPError from requests.exceptions import ConnectionError, HTTPError
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@ -42,7 +44,37 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
] ]
async def test_update_unique_id(hass: HomeAssistant, fritz: Mock): @pytest.mark.parametrize(
"entitydata,old_unique_id,new_unique_id",
[
(
{
"domain": SENSOR_DOMAIN,
"platform": FB_DOMAIN,
"unique_id": CONF_FAKE_AIN,
"unit_of_measurement": TEMP_CELSIUS,
},
CONF_FAKE_AIN,
f"{CONF_FAKE_AIN}_temperature",
),
(
{
"domain": BINARY_SENSOR_DOMAIN,
"platform": FB_DOMAIN,
"unique_id": CONF_FAKE_AIN,
},
CONF_FAKE_AIN,
f"{CONF_FAKE_AIN}_alarm",
),
],
)
async def test_update_unique_id(
hass: HomeAssistant,
fritz: Mock,
entitydata: dict,
old_unique_id: str,
new_unique_id: str,
):
"""Test unique_id update of integration.""" """Test unique_id update of integration."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=FB_DOMAIN, domain=FB_DOMAIN,
@ -52,23 +84,55 @@ async def test_update_unique_id(hass: HomeAssistant, fritz: Mock):
entry.add_to_hass(hass) entry.add_to_hass(hass)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
entity = entity_registry.async_get_or_create( entity: er.RegistryEntry = entity_registry.async_get_or_create(
SENSOR_DOMAIN, **entitydata,
FB_DOMAIN,
CONF_FAKE_AIN,
unit_of_measurement=TEMP_CELSIUS,
config_entry=entry, config_entry=entry,
) )
assert entity.unique_id == CONF_FAKE_AIN assert entity.unique_id == old_unique_id
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
entity_migrated = entity_registry.async_get(entity.entity_id) entity_migrated = entity_registry.async_get(entity.entity_id)
assert entity_migrated assert entity_migrated
assert entity_migrated.unique_id == f"{CONF_FAKE_AIN}_temperature" assert entity_migrated.unique_id == new_unique_id
async def test_update_unique_id_no_change(hass: HomeAssistant, fritz: Mock): @pytest.mark.parametrize(
"entitydata,unique_id",
[
(
{
"domain": SENSOR_DOMAIN,
"platform": FB_DOMAIN,
"unique_id": f"{CONF_FAKE_AIN}_temperature",
"unit_of_measurement": TEMP_CELSIUS,
},
f"{CONF_FAKE_AIN}_temperature",
),
(
{
"domain": BINARY_SENSOR_DOMAIN,
"platform": FB_DOMAIN,
"unique_id": f"{CONF_FAKE_AIN}_alarm",
},
f"{CONF_FAKE_AIN}_alarm",
),
(
{
"domain": BINARY_SENSOR_DOMAIN,
"platform": FB_DOMAIN,
"unique_id": f"{CONF_FAKE_AIN}_other",
},
f"{CONF_FAKE_AIN}_other",
),
],
)
async def test_update_unique_id_no_change(
hass: HomeAssistant,
fritz: Mock,
entitydata: dict,
unique_id: str,
):
"""Test unique_id is not updated of integration.""" """Test unique_id is not updated of integration."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=FB_DOMAIN, domain=FB_DOMAIN,
@ -79,19 +143,16 @@ async def test_update_unique_id_no_change(hass: HomeAssistant, fritz: Mock):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
entity = entity_registry.async_get_or_create( entity = entity_registry.async_get_or_create(
SENSOR_DOMAIN, **entitydata,
FB_DOMAIN,
f"{CONF_FAKE_AIN}_temperature",
unit_of_measurement=TEMP_CELSIUS,
config_entry=entry, config_entry=entry,
) )
assert entity.unique_id == f"{CONF_FAKE_AIN}_temperature" assert entity.unique_id == unique_id
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
entity_migrated = entity_registry.async_get(entity.entity_id) entity_migrated = entity_registry.async_get(entity.entity_id)
assert entity_migrated assert entity_migrated
assert entity_migrated.unique_id == f"{CONF_FAKE_AIN}_temperature" assert entity_migrated.unique_id == unique_id
async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock): async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock):

View File

@ -4,11 +4,7 @@ from unittest.mock import Mock
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import ( from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
DOMAIN as FB_DOMAIN,
)
from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN, SensorStateClass from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN, SensorStateClass
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
@ -40,8 +36,6 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
assert state assert state
assert state.state == "1.23" assert state.state == "1.23"
assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature"
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT

View File

@ -4,11 +4,7 @@ from unittest.mock import Mock
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import ( from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
DOMAIN as FB_DOMAIN,
)
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
@ -50,16 +46,12 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
assert state assert state
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert ATTR_STATE_CLASS not in state.attributes assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature") state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature")
assert state assert state
assert state.state == "1.23" assert state.state == "1.23"
assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature"
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT