diff --git a/.coveragerc b/.coveragerc index ce80bef0a93..48fb4044572 100644 --- a/.coveragerc +++ b/.coveragerc @@ -643,6 +643,7 @@ omit = homeassistant/components/livisi/__init__.py homeassistant/components/livisi/climate.py homeassistant/components/livisi/coordinator.py + homeassistant/components/livisi/entity.py homeassistant/components/livisi/switch.py homeassistant/components/llamalab_automate/notify.py homeassistant/components/logi_circle/__init__.py diff --git a/homeassistant/components/livisi/climate.py b/homeassistant/components/livisi/climate.py index f99ad8dbe72..a6680a19af3 100644 --- a/homeassistant/components/livisi/climate.py +++ b/homeassistant/components/livisi/climate.py @@ -1,11 +1,8 @@ """Code to handle a Livisi Virtual Climate Control.""" from __future__ import annotations -from collections.abc import Mapping from typing import Any -from aiolivisi.const import CAPABILITY_MAP - from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -16,13 +13,10 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DOMAIN, - LIVISI_REACHABILITY_CHANGE, LIVISI_STATE_CHANGE, LOGGER, MAX_TEMPERATURE, @@ -30,6 +24,7 @@ from .const import ( VRCC_DEVICE_TYPE, ) from .coordinator import LivisiDataUpdateCoordinator +from .entity import LivisiEntity async def async_setup_entry( @@ -50,8 +45,8 @@ async def async_setup_entry( device["type"] == VRCC_DEVICE_TYPE and device["id"] not in coordinator.devices ): - livisi_climate: ClimateEntity = create_entity( - config_entry, device, coordinator + livisi_climate: ClimateEntity = LivisiClimate( + config_entry, coordinator, device ) LOGGER.debug("Include device type: %s", device.get("type")) coordinator.devices.add(device["id"]) @@ -63,32 +58,7 @@ async def async_setup_entry( ) -def create_entity( - config_entry: ConfigEntry, - device: dict[str, Any], - coordinator: LivisiDataUpdateCoordinator, -) -> ClimateEntity: - """Create Climate Entity.""" - capabilities: Mapping[str, Any] = device[CAPABILITY_MAP] - config_details: Mapping[str, Any] = device["config"] - room_id: str = device["location"] - room_name: str = coordinator.rooms[room_id] - livisi_climate = LivisiClimate( - config_entry, - coordinator, - unique_id=device["id"], - manufacturer=device["manufacturer"], - device_type=device["type"], - target_temperature_capability=capabilities["RoomSetpoint"], - temperature_capability=capabilities["RoomTemperature"], - humidity_capability=capabilities["RoomHumidity"], - room=room_name, - name=config_details["name"], - ) - return livisi_climate - - -class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntity): +class LivisiClimate(LivisiEntity, ClimateEntity): """Represents the Livisi Climate.""" _attr_hvac_modes = [HVACMode.HEAT] @@ -97,39 +67,21 @@ class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntit _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_target_temperature_high = MAX_TEMPERATURE _attr_target_temperature_low = MIN_TEMPERATURE - _attr_has_entity_name = True def __init__( self, config_entry: ConfigEntry, coordinator: LivisiDataUpdateCoordinator, - unique_id: str, - manufacturer: str, - device_type: str, - target_temperature_capability: str, - temperature_capability: str, - humidity_capability: str, - room: str, - name: str, + device: dict[str, Any], ) -> None: """Initialize the Livisi Climate.""" - self.config_entry = config_entry - self._attr_unique_id = unique_id - self._target_temperature_capability = target_temperature_capability - self._temperature_capability = temperature_capability - self._humidity_capability = humidity_capability - self.aio_livisi = coordinator.aiolivisi - self._attr_available = False - self._attr_name = name - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer=manufacturer, - model=device_type, - name=room, - suggested_area=room, - via_device=(DOMAIN, config_entry.entry_id), + super().__init__( + config_entry, coordinator, device, use_room_as_device_name=True ) - super().__init__(coordinator) + + self._target_temperature_capability = self.capabilities["RoomSetpoint"] + self._temperature_capability = self.capabilities["RoomTemperature"] + self._humidity_capability = self.capabilities["RoomHumidity"] async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -142,11 +94,11 @@ class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntit self._attr_available = False raise HomeAssistantError(f"Failed to turn off {self._attr_name}") - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Do nothing as LIVISI devices do not support changing the hvac mode.""" - async def async_added_to_hass(self) -> None: """Register callbacks.""" + + await super().async_added_to_hass() + target_temperature = await self.coordinator.async_get_vrcc_target_temperature( self._target_temperature_capability ) @@ -184,13 +136,9 @@ class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntit self.update_humidity, ) ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}", - self.update_reachability, - ) - ) + + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Do nothing as LIVISI devices do not support changing the hvac mode.""" @callback def update_target_temperature(self, target_temperature: float) -> None: @@ -206,12 +154,6 @@ class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntit @callback def update_humidity(self, humidity: int) -> None: - """Update the humidity temperature of the climate device.""" + """Update the humidity of the climate device.""" self._attr_current_humidity = humidity self.async_write_ha_state() - - @callback - def update_reachability(self, is_reachable: bool) -> None: - """Update the reachability of the climate device.""" - self._attr_available = is_reachable - self.async_write_ha_state() diff --git a/homeassistant/components/livisi/entity.py b/homeassistant/components/livisi/entity.py new file mode 100644 index 00000000000..613f55d1b7e --- /dev/null +++ b/homeassistant/components/livisi/entity.py @@ -0,0 +1,81 @@ +"""Code to handle a Livisi switches.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiolivisi.const import CAPABILITY_MAP + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, LIVISI_REACHABILITY_CHANGE +from .coordinator import LivisiDataUpdateCoordinator + + +class LivisiEntity(CoordinatorEntity[LivisiDataUpdateCoordinator], Entity): + """Represents a base livisi entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: LivisiDataUpdateCoordinator, + device: dict[str, Any], + *, + use_room_as_device_name: bool = False, + ) -> None: + """Initialize the common properties of a Livisi device.""" + self.aio_livisi = coordinator.aiolivisi + self.capabilities: Mapping[str, Any] = device[CAPABILITY_MAP] + + name = device["config"]["name"] + unique_id = device["id"] + + room_id: str | None = device.get("location") + room_name: str | None = None + if room_id is not None: + room_name = coordinator.rooms.get(room_id) + + self._attr_available = False + self._attr_unique_id = unique_id + + device_name = name + + # For livisi climate entities, the device should have the room name from + # the livisi setup, as each livisi room gets exactly one VRCC device. The entity + # name will always be some localized value of "Climate", so the full element name + # in homeassistent will be in the form of "Bedroom Climate" + if use_room_as_device_name and room_name is not None: + self._attr_name = name + device_name = room_name + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=device["manufacturer"], + model=device["type"], + name=device_name, + suggested_area=room_name, + via_device=(DOMAIN, config_entry.entry_id), + ) + super().__init__(coordinator) + + async def async_added_to_hass(self) -> None: + """Register callback for reachability.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}", + self.update_reachability, + ) + ) + + @callback + def update_reachability(self, is_reachable: bool) -> None: + """Update the reachability of the device.""" + self._attr_available = is_reachable + self.async_write_ha_state() diff --git a/homeassistant/components/livisi/switch.py b/homeassistant/components/livisi/switch.py index bcb9a204411..f5201ab8faa 100644 --- a/homeassistant/components/livisi/switch.py +++ b/homeassistant/components/livisi/switch.py @@ -8,18 +8,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - LIVISI_REACHABILITY_CHANGE, - LIVISI_STATE_CHANGE, - LOGGER, - PSS_DEVICE_TYPE, -) +from .const import DOMAIN, LIVISI_STATE_CHANGE, LOGGER, PSS_DEVICE_TYPE from .coordinator import LivisiDataUpdateCoordinator +from .entity import LivisiEntity async def async_setup_entry( @@ -40,8 +33,8 @@ async def async_setup_entry( device["type"] == PSS_DEVICE_TYPE and device["id"] not in coordinator.devices ): - livisi_switch: SwitchEntity = create_entity( - config_entry, device, coordinator + livisi_switch: SwitchEntity = LivisiSwitch( + config_entry, coordinator, device ) LOGGER.debug("Include device type: %s", device["type"]) coordinator.devices.add(device["id"]) @@ -53,59 +46,18 @@ async def async_setup_entry( ) -def create_entity( - config_entry: ConfigEntry, - device: dict[str, Any], - coordinator: LivisiDataUpdateCoordinator, -) -> SwitchEntity: - """Create Switch Entity.""" - config_details: dict[str, Any] = device["config"] - capabilities: list = device["capabilities"] - room_id: str = device["location"] - room_name: str = coordinator.rooms[room_id] - livisi_switch = LivisiSwitch( - config_entry, - coordinator, - unique_id=device["id"], - manufacturer=device["manufacturer"], - device_type=device["type"], - name=config_details["name"], - capability_id=capabilities[0], - room=room_name, - ) - return livisi_switch - - -class LivisiSwitch(CoordinatorEntity[LivisiDataUpdateCoordinator], SwitchEntity): +class LivisiSwitch(LivisiEntity, SwitchEntity): """Represents the Livisi Switch.""" def __init__( self, config_entry: ConfigEntry, coordinator: LivisiDataUpdateCoordinator, - unique_id: str, - manufacturer: str, - device_type: str, - name: str, - capability_id: str, - room: str, + device: dict[str, Any], ) -> None: - """Initialize the Livisi Switch.""" - self.config_entry = config_entry - self._attr_unique_id = unique_id - self._attr_name = name - self._capability_id = capability_id - self.aio_livisi = coordinator.aiolivisi - self._attr_available = False - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer=manufacturer, - model=device_type, - name=name, - suggested_area=room, - via_device=(DOMAIN, config_entry.entry_id), - ) - super().__init__(coordinator) + """Initialize the Livisi switch.""" + super().__init__(config_entry, coordinator, device) + self._capability_id = self.capabilities["SwitchActuator"] async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" @@ -127,6 +79,8 @@ class LivisiSwitch(CoordinatorEntity[LivisiDataUpdateCoordinator], SwitchEntity) async def async_added_to_hass(self) -> None: """Register callbacks.""" + await super().async_added_to_hass() + response = await self.coordinator.async_get_pss_state(self._capability_id) if response is None: self._attr_is_on = False @@ -140,22 +94,9 @@ class LivisiSwitch(CoordinatorEntity[LivisiDataUpdateCoordinator], SwitchEntity) self.update_states, ) ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}", - self.update_reachability, - ) - ) @callback def update_states(self, state: bool) -> None: - """Update the states of the switch device.""" + """Update the state of the switch device.""" self._attr_is_on = state self.async_write_ha_state() - - @callback - def update_reachability(self, is_reachable: bool) -> None: - """Update the reachability of the switch device.""" - self._attr_available = is_reachable - self.async_write_ha_state()