diff --git a/custom_components/eldom/climate.py b/custom_components/eldom/climate.py new file mode 100644 index 0000000..3dc50cd --- /dev/null +++ b/custom_components/eldom/climate.py @@ -0,0 +1,173 @@ +"""Platform for Eldom water heater integration.""" + +import logging +from typing import Any + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + DEVICE_TYPE_CONVECTOR_HEATER, + DEVICE_TYPE_MAPPING, + DOMAIN, + MANUFACTURER_NAME, +) +from .coordinator import EldomCoordinator +from .eldom_convector import EldomConvectorHeater +from .models import EldomData + +SUPPORT_FLAGS_CLIMATE = ( + ClimateEntityFeature.TARGET_TEMPERATURE, + ClimateEntityFeature.TURN_ON, + ClimateEntityFeature.TURN_OFF, +) + +TEMP_UNIT = UnitOfTemperature.CELSIUS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Eldom convector heater setup.""" + + eldom_data: EldomData = hass.data[DOMAIN][entry.entry_id] + + await eldom_data.coordinator.async_config_entry_first_refresh() + + async_add_entities( + EldomConvectorHeaterEntity(convector_heater, eldom_data.coordinator) + for convector_heater in eldom_data.coordinator.data.get( + DEVICE_TYPE_CONVECTOR_HEATER + ).values() + ) + + +class EldomConvectorHeaterEntity(ClimateEntity, CoordinatorEntity): + """Representation of an Eldom convector heater. + + The CoordinatorEntity class provides: + should_poll + async_update + async_added_to_hass + available + """ + + def __init__( + self, convector_heater: EldomConvectorHeater, coordinator: EldomCoordinator + ) -> None: + """Initialize an Eldom water heater.""" + super().__init__(coordinator) + + self._convector_heater = convector_heater + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this convector heater.""" + return DeviceInfo( + name=self._convector_heater.name, + identifiers={(DOMAIN, self._convector_heater.device_id)}, + manufacturer=MANUFACTURER_NAME, + model=DEVICE_TYPE_MAPPING.get(self._convector_heater.type), + sw_version=self._convector_heater.software_version, + hw_version=self._convector_heater.hardware_version, + ) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._convector_heater.device_id + + @property + def name(self) -> str: + """Return the name of the convector heater.""" + return self._convector_heater.name + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + return SUPPORT_FLAGS_CLIMATE + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_UNIT + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self._convector_heater.max_temperature + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self._convector_heater.min_temperature + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._convector_heater.current_temperature + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self._convector_heater.target_temperature + + @property + def hvac_mode(self) -> HVACMode | None: + """Return current operation ie. Off or Heating.""" + return self._convector_heater.current_operation + + @property + def hvac_modes(self) -> list[HVACMode]: + """Return the list of available operation modes.""" + return self._convector_heater.operation_modes + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the convector heater on.""" + await self._convector_heater.turn_on() + self.schedule_update_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the convector heater off.""" + await self._convector_heater.turn_off() + self.schedule_update_ha_state() + await self.coordinator.async_request_refresh() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target operation mode.""" + try: + await self._convector_heater.set_operation_mode(hvac_mode) + self.schedule_update_ha_state() + await self.coordinator.async_request_refresh() + except Exception as e: + _LOGGER.error("Error while setting operation mode: %s", e) + raise HomeAssistantError("Error while setting operation mode") from e + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temperature = kwargs.get("temperature") + await self._convector_heater.set_temperature(temperature) + self.schedule_update_ha_state() + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._convector_heater = self.coordinator.data.get( + self._convector_heater.type + ).get(self._convector_heater.id) + + self.async_write_ha_state() diff --git a/custom_components/eldom/const.py b/custom_components/eldom/const.py index 4d6db80..20da92f 100644 --- a/custom_components/eldom/const.py +++ b/custom_components/eldom/const.py @@ -7,8 +7,10 @@ DEVICE_TYPE_FLAT_BOILER = 7 DEVICE_TYPE_SMART_BOILER = 5 +DEVICE_TYPE_CONVECTOR_HEATER = 4 DEVICE_TYPE_MAPPING = { DEVICE_TYPE_FLAT_BOILER: "Flat Boiler", DEVICE_TYPE_SMART_BOILER: "Smart Boiler", + DEVICE_TYPE_CONVECTOR_HEATER: "Convector Heater", } diff --git a/custom_components/eldom/coordinator.py b/custom_components/eldom/coordinator.py index 7dd73fe..480c9f0 100644 --- a/custom_components/eldom/coordinator.py +++ b/custom_components/eldom/coordinator.py @@ -8,8 +8,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DEVICE_TYPE_FLAT_BOILER, DEVICE_TYPE_SMART_BOILER, DOMAIN +from .const import ( + DEVICE_TYPE_CONVECTOR_HEATER, + DEVICE_TYPE_FLAT_BOILER, + DEVICE_TYPE_SMART_BOILER, + DOMAIN, +) from .eldom_boiler import FlatEldomBoiler, SmartEldomBoiler +from .eldom_convector import EldomConvectorHeater _LOGGER = logging.getLogger(__name__) @@ -47,7 +53,18 @@ async def _async_update_data(self) -> dict: if device.deviceType == DEVICE_TYPE_SMART_BOILER } + convector_heaters: dict[str, EldomConvectorHeater] = { + device.id: EldomConvectorHeater( + device.id, + await self.api.get_convector_heater_status(device.id), + self.api, + ) + for device in devices + if device.deviceType == DEVICE_TYPE_CONVECTOR_HEATER + } + return { DEVICE_TYPE_FLAT_BOILER: flat_boilers, DEVICE_TYPE_SMART_BOILER: smart_boilers, + DEVICE_TYPE_CONVECTOR_HEATER: convector_heaters, } diff --git a/custom_components/eldom/eldom_boiler.py b/custom_components/eldom/eldom_boiler.py index 9368b4a..d04a975 100644 --- a/custom_components/eldom/eldom_boiler.py +++ b/custom_components/eldom/eldom_boiler.py @@ -1,4 +1,4 @@ -"""Eldom objects.""" +"""Eldom boiler objects.""" from abc import ABC, abstractmethod import logging diff --git a/custom_components/eldom/eldom_convector.py b/custom_components/eldom/eldom_convector.py new file mode 100644 index 0000000..94161fe --- /dev/null +++ b/custom_components/eldom/eldom_convector.py @@ -0,0 +1,139 @@ +"""Eldom convector heater objects.""" + +import logging + +from eldom.client import Client as EldomClient +from eldom.models import ConvectorHeaterDetails + +from homeassistant.components.climate import HVACMode + +OPERATION_MODES = {0: HVACMode.OFF, 1: HVACMode.HEAT} + +MAX_TEMP = 35 +MIN_TEMP = 5 + +_LOGGER = logging.getLogger(__name__) + + +class EldomConvectorHeater: + """An Eldom convector heater representation object.""" + + def __init__( + self, + id: int, + convector_heater_details: ConvectorHeaterDetails, + eldom_client: EldomClient, + ) -> None: + """Initialize the heater.""" + self._id = id + self._convector_heater_details = convector_heater_details + self._eldom_client = eldom_client + + @property + def id(self) -> int: + """Retrieve the heater's ID.""" + return self._id + + @property + def device_id(self) -> str: + """Retrieve the heater's device ID.""" + return self._convector_heater_details.DeviceID + + @property + def name(self) -> str: + """Retrieve the heater's name.""" + return f"Convector Heater ({self._convector_heater_details.DeviceID[-4:]})" + + @property + def type(self) -> int: + """Retrieve the heater's type.""" + return self._convector_heater_details.Type + + @property + def software_version(self) -> str: + """Retrieve the heater's software version.""" + return self._convector_heater_details.SoftwareVersion + + @property + def hardware_version(self) -> str: + """Retrieve the heater's hardware version.""" + return self._convector_heater_details.HardwareVersion + + @property + def operation_modes(self) -> list[HVACMode]: + """Retrieve the heater's operation modes. Modes are: Off, Heat.""" + return list(OPERATION_MODES.values()) + + @property + def max_temperature(self) -> float: + """Retrieve the heater's maximum temperature.""" + return MAX_TEMP + + @property + def min_temperature(self) -> float: + """Retrieve the heater's minimum temperature.""" + return MIN_TEMP + + @property + def current_temperature(self) -> float: + """Retrieve the heater's current temperature.""" + return self._convector_heater_details.AmbientTemp + + @property + def target_temperature(self) -> float: + """Retrieve the heater's target temperature.""" + return self._convector_heater_details.SetTemp + + @property + def powerful_enabled(self) -> bool: + """Retrieve whether the heater's powerful mode is enabled.""" + return self._convector_heater_details.BoostHeating + + @property + def day_energy_consumption(self) -> float: + """Retrieve the heater's day energy consumption.""" + return self._convector_heater_details.EnergyD + + @property + def night_energy_consumption(self) -> float: + """Retrieve the heater's night energy consumption.""" + return self._convector_heater_details.EnergyN + + @property + def current_operation(self) -> HVACMode: + """Return current operation ie. Off or Heat.""" + return OPERATION_MODES.get(self._convector_heater_details.State, "Unknown") + + @property + def power_level(self) -> int: + """Retrieve the heating level of the heater.""" + return self._convector_heater_details.Power + + async def turn_on(self) -> None: + """Turn the heater on.""" + await self.set_operation_mode(HVACMode.HEAT) + + async def turn_off(self) -> None: + """Turn the heater off.""" + await self.set_operation_mode(HVACMode.OFF) + + async def set_operation_mode(self, operation_mode: HVACMode) -> None: + """Set new target operation mode.""" + if operation_mode not in OPERATION_MODES.values(): + raise ValueError("Operation mode not supported") + + operation_mode_id = {v: k for k, v in OPERATION_MODES.items()}[operation_mode] + + self._convector_heater_details.State = operation_mode_id + + await self._eldom_client.set_convector_heater_state( + self.device_id, operation_mode_id + ) + + async def set_temperature(self, temperature: float) -> None: + """Set the temperature of the heater.""" + self._convector_heater_details.SetTemp = temperature + + await self._eldom_client.set_convector_heater_temperature( + self.device_id, temperature + ) diff --git a/custom_components/eldom/manifest.json b/custom_components/eldom/manifest.json index 05877ec..994adf5 100644 --- a/custom_components/eldom/manifest.json +++ b/custom_components/eldom/manifest.json @@ -8,8 +8,8 @@ "issue_tracker": "https://github.com/qbaware/homeassistant-eldom/issues", "homekit": {}, "iot_class": "cloud_polling", - "requirements": ["pyeldom==0.3.0"], + "requirements": ["pyeldom==0.4.1"], "ssdp": [], "zeroconf": [], - "version": "2.2.10" + "version": "3.0.0" }