From 7b14b6af0ed5b6eb920373ec923836e8328f7154 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Thu, 27 Feb 2025 20:03:44 +0900 Subject: [PATCH] Add water heater entity to LG ThinQ (#138257) Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/__init__.py | 1 + .../components/lg_thinq/water_heater.py | 201 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 homeassistant/components/lg_thinq/water_heater.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index 72d81af4ff0..f83cbadf925 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -47,6 +47,7 @@ PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, Platform.VACUUM, + Platform.WATER_HEATER, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lg_thinq/water_heater.py b/homeassistant/components/lg_thinq/water_heater.py new file mode 100644 index 00000000000..5a5c8d024b6 --- /dev/null +++ b/homeassistant/components/lg_thinq/water_heater.py @@ -0,0 +1,201 @@ +"""Support for waterheater entities.""" + +from __future__ import annotations + +import logging +from typing import Any + +from thinqconnect import DeviceType +from thinqconnect.integration import ExtendedProperty + +from homeassistant.components.water_heater import ( + ATTR_OPERATION_MODE, + STATE_ECO, + STATE_HEAT_PUMP, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityDescription, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + +DEVICE_TYPE_WH_MAP: dict[DeviceType, WaterHeaterEntityDescription] = { + DeviceType.WATER_HEATER: WaterHeaterEntityDescription( + key=ExtendedProperty.WATER_HEATER, + name=None, + ), + DeviceType.SYSTEM_BOILER: WaterHeaterEntityDescription( + key=ExtendedProperty.WATER_BOILER, + name=None, + ), +} + +# Mapping between device and HA operation modes +DEVICE_OP_MODE_TO_HA = { + "auto": STATE_ECO, + "heat_pump": STATE_HEAT_PUMP, + "turbo": STATE_PERFORMANCE, + "vacation": STATE_OFF, +} +HA_STATE_TO_DEVICE_OP_MODE = {v: k for k, v in DEVICE_OP_MODE_TO_HA.items()} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up an entry for water_heater platform.""" + entities: list[ThinQWaterHeaterEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + description := DEVICE_TYPE_WH_MAP.get(coordinator.api.device.device_type) + ) is not None: + if coordinator.api.device.device_type == DeviceType.WATER_HEATER: + entities.append( + ThinQWaterHeaterEntity( + coordinator, description, ExtendedProperty.WATER_HEATER + ) + ) + elif coordinator.api.device.device_type == DeviceType.SYSTEM_BOILER: + entities.append( + ThinQWaterBoilerEntity( + coordinator, description, ExtendedProperty.WATER_BOILER + ) + ) + if entities: + async_add_entities(entities) + + +class ThinQWaterHeaterEntity(ThinQEntity, WaterHeaterEntity): + """Represent a ThinQ water heater entity.""" + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: WaterHeaterEntityDescription, + property_id: str, + ) -> None: + """Initialize a water_heater entity.""" + super().__init__(coordinator, entity_description, property_id) + self._attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + ) + self._attr_temperature_unit = ( + self._get_unit_of_measurement(self.data.unit) or UnitOfTemperature.CELSIUS + ) + if modes := self.data.job_modes: + self._attr_operation_list = [ + DEVICE_OP_MODE_TO_HA.get(mode, mode) for mode in modes + ] + else: + self._attr_operation_list = [STATE_HEAT_PUMP] + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + self._attr_current_temperature = self.data.current_temp + self._attr_target_temperature = self.data.target_temp + + if self.data.max is not None: + self._attr_max_temp = self.data.max + if self.data.min is not None: + self._attr_min_temp = self.data.min + if self.data.step is not None: + self._attr_target_temperature_step = self.data.step + + self._attr_temperature_unit = ( + self._get_unit_of_measurement(self.data.unit) or UnitOfTemperature.CELSIUS + ) + if self.data.is_on: + self._attr_current_operation = ( + DEVICE_OP_MODE_TO_HA.get(job_mode, job_mode) + if (job_mode := self.data.job_mode) is not None + else STATE_HEAT_PUMP + ) + else: + self._attr_current_operation = STATE_OFF + + _LOGGER.debug( + "[%s:%s] update status: c:%s, t:%s, op_mode:%s, op_list:%s, is_on:%s", + self.coordinator.device_name, + self.property_id, + self.current_temperature, + self.target_temperature, + self.current_operation, + self.operation_list, + self.data.is_on, + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperatures.""" + _LOGGER.debug( + "[%s:%s] async_set_temperature: %s", + self.coordinator.device_name, + self.property_id, + kwargs, + ) + if (operation_mode := kwargs.get(ATTR_OPERATION_MODE)) is not None: + await self.async_set_operation_mode(str(operation_mode)) + if operation_mode == STATE_OFF: + return + + if ( + temperature := kwargs.get(ATTR_TEMPERATURE) + ) is not None and temperature != self.target_temperature: + await self.async_call_api( + self.coordinator.api.async_set_target_temperature( + self.property_id, temperature + ) + ) + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + mode = HA_STATE_TO_DEVICE_OP_MODE.get(operation_mode, operation_mode) + _LOGGER.debug( + "[%s:%s] async_set_operation_mode: %s", + self.coordinator.device_name, + self.property_id, + mode, + ) + await self.async_call_api( + self.coordinator.api.async_set_job_mode(self.property_id, mode) + ) + + +class ThinQWaterBoilerEntity(ThinQWaterHeaterEntity): + """Represent a ThinQ water boiler entity.""" + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: WaterHeaterEntityDescription, + property_id: str, + ) -> None: + """Initialize a water_heater entity.""" + super().__init__(coordinator, entity_description, property_id) + self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + _LOGGER.debug( + "[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id + ) + await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id)) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + _LOGGER.debug( + "[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id + ) + await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id))