diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index e72e1d86fc1..e5ef4536bc4 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -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, - } diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index e83a2a67472..b58f3311cb5 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -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: diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 0f481773778..4d9d356b532 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -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 diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index d4827fbb289..7b67bcd6cf8 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -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" diff --git a/homeassistant/components/fritzbox/model.py b/homeassistant/components/fritzbox/model.py index 133638c1fe8..ea63ab983c1 100644 --- a/homeassistant/components/fritzbox/model.py +++ b/homeassistant/components/fritzbox/model.py @@ -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 diff --git a/tests/components/fritzbox/const.py b/tests/components/fritzbox/const.py index 1b8bc927800..58ad5ae177c 100644 --- a/tests/components/fritzbox/const.py +++ b/tests/components/fritzbox/const.py @@ -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" diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index a1ccc2bd0f2..2f1aaf65e07 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -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 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index fc3b15c4199..b91edc0148e 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -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 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 60828e83801..e0dfcdd2663 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -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): diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 07b54765c9a..21262636635 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -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 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index f6f81d5f24c..de49a5cd89e 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -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