1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00

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 homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
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.update_coordinator import CoordinatorEntity
from .const import (
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
CONF_CONNECTIONS,
CONF_COORDINATOR,
DOMAIN,
LOGGER,
PLATFORMS,
)
from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS
from .coordinator import FritzboxDataUpdateCoordinator
from .model import FritzExtraAttributes
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
)
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
await async_migrate_entries(hass, entry.entry_id, _update_unique_id)
@ -138,11 +137,3 @@ class FritzBoxEntity(CoordinatorEntity):
sw_version=self.device.fw_version,
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,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ENTITY_CATEGORY_CONFIG
from homeassistant.core import HomeAssistant
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]
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:
"""Initialize the FritzBox entity."""
super().__init__(coordinator, ain, entity_description)
self._attr_name = self.device.name
self._attr_unique_id = ain
self._attr_name = f"{self.device.name} {entity_description.name}"
self._attr_unique_id = f"{ain}_{entity_description.key}"
@property
def is_on(self) -> bool | None:

View File

@ -26,9 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from .const import (
ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN,
CONF_COORDINATOR,
@ -176,8 +174,6 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
"""Return the device specific state attributes."""
attrs: ClimateExtraAttributes = {
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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONF_DEVICES,
PERCENTAGE,
STATE_OFF,
STATE_ON,
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
)
state = hass.states.get(ENTITY_ID)
state = hass.states.get(f"{ENTITY_ID}_alarm")
assert state
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 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")
assert state
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
)
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.state == STATE_UNAVAILABLE

View File

@ -23,9 +23,7 @@ from homeassistant.components.climate.const import (
)
from homeassistant.components.fritzbox.const import (
ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN,
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_MODES] == [PRESET_ECO, PRESET_COMFORT]
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_LOCKED] == "fake_locked"
assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer"
assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window"
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 pyfritzhome import LoginError
import pytest
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.sensor import DOMAIN as SENSOR_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."""
entry = MockConfigEntry(
domain=FB_DOMAIN,
@ -52,23 +84,55 @@ async def test_update_unique_id(hass: HomeAssistant, fritz: Mock):
entry.add_to_hass(hass)
entity_registry = er.async_get(hass)
entity = entity_registry.async_get_or_create(
SENSOR_DOMAIN,
FB_DOMAIN,
CONF_FAKE_AIN,
unit_of_measurement=TEMP_CELSIUS,
entity: er.RegistryEntry = entity_registry.async_get_or_create(
**entitydata,
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)
await hass.async_block_till_done()
entity_migrated = entity_registry.async_get(entity.entity_id)
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."""
entry = MockConfigEntry(
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 = entity_registry.async_get_or_create(
SENSOR_DOMAIN,
FB_DOMAIN,
f"{CONF_FAKE_AIN}_temperature",
unit_of_measurement=TEMP_CELSIUS,
**entitydata,
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)
await hass.async_block_till_done()
entity_migrated = entity_registry.async_get(entity.entity_id)
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):

View File

@ -4,11 +4,7 @@ from unittest.mock import Mock
from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import (
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
DOMAIN as FB_DOMAIN,
)
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN, SensorStateClass
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
@ -40,8 +36,6 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
assert state
assert state.state == "1.23"
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_STATE_CLASS] == SensorStateClass.MEASUREMENT

View File

@ -4,11 +4,7 @@ from unittest.mock import Mock
from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import (
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
DOMAIN as FB_DOMAIN,
)
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
@ -50,16 +46,12 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
assert state
assert state.state == STATE_ON
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
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature")
assert state
assert state.state == "1.23"
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_STATE_CLASS] == SensorStateClass.MEASUREMENT