diff --git a/README.md b/README.md index d5fcaca..09ed5e5 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,13 @@ These are some next steps you may want to look into: - Create your first release. - Share your integration on the [Home Assistant Forum](https://community.home-assistant.io/). - Submit your integration to [HACS](https://hacs.xyz/docs/publish/start). + + +## TODO +- [ ] Integration configuration parameter sections +- [ ] Integration data sensors +- [ ] Integrate write operations for switches and number inputs +- [ ] Add translations for en and de +- [ ] Create service to start auto-feed linked to solar entity and a reserve +- [ ] Provide meatures +- [ ] Cleanup and document \ No newline at end of file diff --git a/custom_components/askoheat/__init__.py b/custom_components/askoheat/__init__.py index 70929c0..bf7f379 100644 --- a/custom_components/askoheat/__init__.py +++ b/custom_components/askoheat/__init__.py @@ -13,7 +13,10 @@ from homeassistant.loader import async_get_loaded_integration from .api import AskoHeatModbusApiClient -from .coordinator import AskoheatEMADataUpdateCoordinator +from .coordinator import ( + AskoheatConfigDataUpdateCoordinator, + AskoheatEMADataUpdateCoordinator, +) from .data import AskoheatData if TYPE_CHECKING: @@ -26,6 +29,7 @@ Platform.SENSOR, Platform.BINARY_SENSOR, Platform.SWITCH, + Platform.NUMBER, ] @@ -38,6 +42,7 @@ async def async_setup_entry( ema_coordinator = AskoheatEMADataUpdateCoordinator( hass=hass, ) + config_coordinator = AskoheatConfigDataUpdateCoordinator(hass=hass) entry.runtime_data = AskoheatData( client=AskoHeatModbusApiClient( host=entry.data[CONF_HOST], @@ -45,11 +50,13 @@ async def async_setup_entry( ), integration=async_get_loaded_integration(hass, entry.domain), ema_coordinator=ema_coordinator, + config_coordinator=config_coordinator, ) await entry.runtime_data.client.connect() # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities await ema_coordinator.async_config_entry_first_refresh() + await config_coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/custom_components/askoheat/api.py b/custom_components/askoheat/api.py index a9c44b8..dc9e62a 100644 --- a/custom_components/askoheat/api.py +++ b/custom_components/askoheat/api.py @@ -1,11 +1,9 @@ -"""Sample API Client.""" +"""Modbus API Client.""" from __future__ import annotations -from ast import Num -from datetime import datetime, time -from numbers import Number -from typing import TYPE_CHECKING, Any +from datetime import time +from typing import TYPE_CHECKING, Any, TypeVar import numpy as np from pymodbus.client import AsyncModbusTcpClient as ModbusClient @@ -25,6 +23,8 @@ from custom_components.askoheat.data import AskoheatDataBlock if TYPE_CHECKING: + from collections.abc import Callable + from pymodbus.pdu import ModbusPDU @@ -65,9 +65,9 @@ async def async_read_ema_data(self) -> AskoheatDataBlock: async def async_read_config_data(self) -> AskoheatDataBlock: """Read EMA states.""" # http://www.download.askoma.com/askofamily_plus/modbus/askoheat-modbus.html#Configuration_Block - data = await self.async_read_input_registers_data(500, 100) + data = await self.async_read_holding_registers_data(500, 100) LOGGER.debug("async_read_config_data %s", data) - return self._map_ema_data(data) + return self._map_config_data(data) async def async_read_input_registers_data( self, address: int, count: int @@ -156,7 +156,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD: _read_float32( data.registers[58:60] ), - NumberAttrKey.CON_ANALOG_INPUT_0_STEP: _read_byte(data.registers[60]), + NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD_STEP: _read_byte( + data.registers[60] + ), NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD_TEMPERATURE: _read_byte( data.registers[61] ), @@ -164,7 +166,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_1_THRESHOLD: _read_float32( data.registers[62:64] ), - NumberAttrKey.CON_ANALOG_INPUT_1_STEP: _read_byte(data.registers[64]), + NumberAttrKey.CON_ANALOG_INPUT_1_THRESHOLD_STEP: _read_byte( + data.registers[64] + ), NumberAttrKey.CON_ANALOG_INPUT_1_THRESHOLD_TEMPERATURE: _read_byte( data.registers[65] ), @@ -172,7 +176,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_2_THRESHOLD: _read_float32( data.registers[66:68] ), - NumberAttrKey.CON_ANALOG_INPUT_2_STEP: _read_byte(data.registers[68]), + NumberAttrKey.CON_ANALOG_INPUT_2_THRESHOLD_STEP: _read_byte( + data.registers[68] + ), NumberAttrKey.CON_ANALOG_INPUT_2_THRESHOLD_TEMPERATURE: _read_byte( data.registers[69] ), @@ -180,7 +186,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_3_THRESHOLD: _read_float32( data.registers[70:72] ), - NumberAttrKey.CON_ANALOG_INPUT_3_STEP: _read_byte(data.registers[72]), + NumberAttrKey.CON_ANALOG_INPUT_3_THRESHOLD_STEP: _read_byte( + data.registers[72] + ), NumberAttrKey.CON_ANALOG_INPUT_3_THRESHOLD_TEMPERATURE: _read_byte( data.registers[73] ), @@ -188,7 +196,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_4_THRESHOLD: _read_float32( data.registers[74:76] ), - NumberAttrKey.CON_ANALOG_INPUT_4_STEP: _read_byte(data.registers[76]), + NumberAttrKey.CON_ANALOG_INPUT_4_THRESHOLD_STEP: _read_byte( + data.registers[76] + ), NumberAttrKey.CON_ANALOG_INPUT_4_THRESHOLD_TEMPERATURE: _read_byte( data.registers[77] ), @@ -196,7 +206,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_5_THRESHOLD: _read_float32( data.registers[78:80] ), - NumberAttrKey.CON_ANALOG_INPUT_5_STEP: _read_byte(data.registers[80]), + NumberAttrKey.CON_ANALOG_INPUT_5_THRESHOLD_STEP: _read_byte( + data.registers[80] + ), NumberAttrKey.CON_ANALOG_INPUT_5_THRESHOLD_TEMPERATURE: _read_byte( data.registers[81] ), @@ -204,7 +216,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_6_THRESHOLD: _read_float32( data.registers[82:84] ), - NumberAttrKey.CON_ANALOG_INPUT_6_STEP: _read_byte(data.registers[84]), + NumberAttrKey.CON_ANALOG_INPUT_6_THRESHOLD_STEP: _read_byte( + data.registers[84] + ), NumberAttrKey.CON_ANALOG_INPUT_6_THRESHOLD_TEMPERATURE: _read_byte( data.registers[65] ), @@ -212,7 +226,9 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD: _read_float32( data.registers[86:88] ), - NumberAttrKey.CON_ANALOG_INPUT_7_STEP: _read_byte(data.registers[88]), + NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD_STEP: _read_byte( + data.registers[88] + ), NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD_TEMPERATURE: _read_byte( data.registers[89] ), @@ -426,7 +442,8 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: }, time_inputs={ TimeAttrKey.CON_LEGIO_PROTECTION_PREFERRED_START_TIME: _read_time( - data.registers[12:16] + register_value_hours=data.registers[12], + register_value_minutes=data.registers[13], ), TimeAttrKey.CON_LOW_TARIFF_START_TIME: time( hour=_read_byte(data.registers[52]), @@ -438,12 +455,11 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: ), }, text_intputs={ - TextAttrKey.CON_WATER_HARDNESS: _read_str(data.registers[16:20]), TextAttrKey.CON_INFO_STRING: _read_str(data.registers[22:38]), }, select_inputs={ - SelectAttrKey.CON_RTU_BAUDRATE: Baurate( - _read_str(data.registers[46:49]) + SelectAttrKey.CON_RTU_BAUDRATE: _read_enum( + data.registers[46:49], Baurate ), SelectAttrKey.CON_ENERGY_METER_TYPE: EnergyMeterType( _read_byte(data.registers[51]) @@ -491,22 +507,31 @@ def _map_register_to_heater_step( } -def _read_time(register_values: list[int]) -> time | None: +def _read_time(register_value_hours: int, register_value_minutes: int) -> time | None: """Read register values as string and parse as time.""" - time_string = _read_str(register_values) - try: - return datetime.strptime(time_string, "%H:%M %p").time # type: ignore # noqa: DTZ007, PGH003 - except Exception as err: # noqa: BLE001 - LOGGER.warning("Could not read time from string %s, %s", time_string, err) + hours = _read_uint16(register_value_hours) + minutes = _read_uint16(register_value_minutes) + return time(hour=hours, minute=minutes) + + +T = TypeVar("T") + + +def _read_enum(register_values: list[int], factory: Callable[[str], T]) -> T: + """Read register values as enum.""" + str_value = _read_str(register_values) + return factory(str_value) def _read_str(register_values: list[int]) -> str: """Read register values as str.""" - return str( - ModbusClient.convert_from_registers( - register_values, ModbusClient.DATATYPE.STRING - ) - ) + # custom implementation as strings a represented with little endian + byte_list = bytearray() + for x in register_values: + byte_list.extend(int.to_bytes(x, 2, "little")) + if byte_list[-1:] == b"\00": + byte_list = byte_list[:-1] + return byte_list.decode("utf-8") def _read_byte(register_value: int) -> np.byte: diff --git a/custom_components/askoheat/binary_sensor.py b/custom_components/askoheat/binary_sensor.py index 76f4d2b..ea88fe5 100644 --- a/custom_components/askoheat/binary_sensor.py +++ b/custom_components/askoheat/binary_sensor.py @@ -52,8 +52,7 @@ def __init__( entity_description: AskoheatBinarySensorEntityDescription, ) -> None: """Initialize the binary_sensor class.""" - super().__init__(coordinator) - self.entity_description = entity_description + super().__init__(coordinator, entity_description) self.entity_id = ENTITY_ID_FORMAT.format(entity_description.key) self._attr_unique_id = self.entity_id @@ -77,6 +76,5 @@ def _handle_coordinator_update(self) -> None: self.entity_description.on_states is not None and self._attr_state in self.entity_description.on_states ) - self.async_write_ha_state() super()._handle_coordinator_update() diff --git a/custom_components/askoheat/binary_sensor_entities_ema.py b/custom_components/askoheat/binary_sensor_entities_ema.py index c09d56d..fc49acd 100644 --- a/custom_components/askoheat/binary_sensor_entities_ema.py +++ b/custom_components/askoheat/binary_sensor_entities_ema.py @@ -2,99 +2,83 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass - from custom_components.askoheat.const import BinarySensorAttrKey from custom_components.askoheat.model import AskoheatBinarySensorEntityDescription EMA_BINARY_SENSOR_ENTITY_DESCRIPTIONS = ( AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEATER1_ACTIVE, - translation_key=BinarySensorAttrKey.HEATER1_ACTIVE, icon="mdi:power-plug", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEATER2_ACTIVE, - translation_key=BinarySensorAttrKey.HEATER2_ACTIVE, icon="mdi:power-plug", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEATER3_ACTIVE, - translation_key=BinarySensorAttrKey.HEATER3_ACTIVE, icon="mdi:power-plug", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.PUMP_ACTIVE, - translation_key=BinarySensorAttrKey.PUMP_ACTIVE, icon="mdi:pump", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED, - translation_key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED, icon="mdi:connection", device_class=BinarySensorDeviceClass.PROBLEM, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE, - translation_key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE, - icon="mdi:heat_pump", + icon="mdi:heat-pump", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE, - translation_key=BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE, icon="mdi:car-emergency", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE, - translation_key=BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE, icon="mdi:shield-sun", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.ANALOG_INPUT_ACTIVE, - translation_key=BinarySensorAttrKey.ANALOG_INPUT_ACTIVE, icon="mdi:sine-wave", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.SETPOINT_ACTIVE, - translation_key=BinarySensorAttrKey.SETPOINT_ACTIVE, icon="mdi:finance", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE, - translation_key=BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE, icon="mdi:solar-power", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.AUTOHEATER_ACTIVE, - translation_key=BinarySensorAttrKey.AUTOHEATER_ACTIVE, icon="mdi:water-boiler-auto", inverted=True, device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE, - translation_key=BinarySensorAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE, icon="mdi:water-boiler-auto", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.TEMP_LIMIT_REACHED, - translation_key=BinarySensorAttrKey.TEMP_LIMIT_REACHED, icon="mdi:water-boiler-auto", device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.ERROR_OCCURED, - translation_key=BinarySensorAttrKey.ERROR_OCCURED, icon="mdi:water-thermometer", device_class=BinarySensorDeviceClass.PROBLEM, ), diff --git a/custom_components/askoheat/const.py b/custom_components/askoheat/const.py index ccd6a42..1628ee0 100644 --- a/custom_components/askoheat/const.py +++ b/custom_components/askoheat/const.py @@ -56,50 +56,42 @@ class NumberAttrKey(StrEnum): # Analog input 0 CON_ANALOG_INPUT_0_THRESHOLD = "analog_input_0_threshold" - # 0-7 - CON_ANALOG_INPUT_0_STEP = "analog_input_0_step" + CON_ANALOG_INPUT_0_THRESHOLD_STEP = "analog_input_0_threshold_step" CON_ANALOG_INPUT_0_THRESHOLD_TEMPERATURE = "analog_input_0_threshold_temperature" # Analog input 1 CON_ANALOG_INPUT_1_THRESHOLD = "analog_input_1_threshold" - # 0-7 - CON_ANALOG_INPUT_1_STEP = "analog_input_1_step" + CON_ANALOG_INPUT_1_THRESHOLD_STEP = "analog_input_1_threshold_step" CON_ANALOG_INPUT_1_THRESHOLD_TEMPERATURE = "analog_input_1_threshold_temperature" # Analog input 2 CON_ANALOG_INPUT_2_THRESHOLD = "analog_input_2_threshold" - # 0-7 - CON_ANALOG_INPUT_2_STEP = "analog_input_2_step" + CON_ANALOG_INPUT_2_THRESHOLD_STEP = "analog_input_2_threshold_step" CON_ANALOG_INPUT_2_THRESHOLD_TEMPERATURE = "analog_input_2_threshold_temperature" # Analog input 3 CON_ANALOG_INPUT_3_THRESHOLD = "analog_input_3_threshold" - # 0-7 - CON_ANALOG_INPUT_3_STEP = "analog_input_3_step" + CON_ANALOG_INPUT_3_THRESHOLD_STEP = "analog_input_3_threshold_step" CON_ANALOG_INPUT_3_THRESHOLD_TEMPERATURE = "analog_input_3_threshold_temperature" # Analog input 4 CON_ANALOG_INPUT_4_THRESHOLD = "analog_input_4_threshold" - # 0-7 - CON_ANALOG_INPUT_4_STEP = "analog_input_4_step" + CON_ANALOG_INPUT_4_THRESHOLD_STEP = "analog_input_4_threshold_step" CON_ANALOG_INPUT_4_THRESHOLD_TEMPERATURE = "analog_input_4_threshold_temperature" # Analog input 5 CON_ANALOG_INPUT_5_THRESHOLD = "analog_input_5_threshold" - # 0-7 - CON_ANALOG_INPUT_5_STEP = "analog_input_5_step" + CON_ANALOG_INPUT_5_THRESHOLD_STEP = "analog_input_5_threshold_step" CON_ANALOG_INPUT_5_THRESHOLD_TEMPERATURE = "analog_input_5_threshold_temperature" # Analog input 6 CON_ANALOG_INPUT_6_THRESHOLD = "analog_input_6_threshold" - # 0-7 - CON_ANALOG_INPUT_6_STEP = "analog_input_6_step" + CON_ANALOG_INPUT_6_THRESHOLD_STEP = "analog_input_6_threshold_step" CON_ANALOG_INPUT_6_THRESHOLD_TEMPERATURE = "analog_input_6_threshold_temperature" # Analog input 7 CON_ANALOG_INPUT_7_THRESHOLD = "analog_input_7_threshold" - # 0-7 - CON_ANALOG_INPUT_7_STEP = "analog_input_7_step" + CON_ANALOG_INPUT_7_THRESHOLD_STEP = "analog_input_7_threshold_step" CON_ANALOG_INPUT_7_THRESHOLD_TEMPERATURE = "analog_input_7_threshold_temperature" # 0-7 @@ -135,8 +127,6 @@ class TimeAttrKey(StrEnum): class TextAttrKey(StrEnum): """Askoheat text entities attribute keys.""" - # i.e. „32 °fH", „hart", °14 dH" - CON_WATER_HARDNESS = "legio_water_hardness" CON_INFO_STRING = "info_string" diff --git a/custom_components/askoheat/data.py b/custom_components/askoheat/data.py index 15febf5..04ddb03 100644 --- a/custom_components/askoheat/data.py +++ b/custom_components/askoheat/data.py @@ -22,7 +22,10 @@ TextAttrKey, TimeAttrKey, ) - from custom_components.askoheat.coordinator import AskoheatEMADataUpdateCoordinator + from custom_components.askoheat.coordinator import ( + AskoheatConfigDataUpdateCoordinator, + AskoheatEMADataUpdateCoordinator, + ) from .api import AskoHeatModbusApiClient @@ -36,6 +39,7 @@ class AskoheatData: client: AskoHeatModbusApiClient ema_coordinator: AskoheatEMADataUpdateCoordinator + config_coordinator: AskoheatConfigDataUpdateCoordinator integration: Integration diff --git a/custom_components/askoheat/entity.py b/custom_components/askoheat/entity.py index b859b76..599523e 100644 --- a/custom_components/askoheat/entity.py +++ b/custom_components/askoheat/entity.py @@ -2,24 +2,30 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from custom_components.askoheat.model import AdkoheatEntityDescription - -from .const import ATTRIBUTION, LOGGER +from .const import ATTRIBUTION from .coordinator import AskoheatDataUpdateCoordinator -from homeassistant.core import callback +if TYPE_CHECKING: + from custom_components.askoheat.model import AskoheatEntityDescription class AskoheatEntity(CoordinatorEntity[AskoheatDataUpdateCoordinator]): """AskoheatEntity class.""" _attr_attribution = ATTRIBUTION - entity_description: AdkoheatEntityDescription + entity_description: AskoheatEntityDescription - def __init__(self, coordinator: AskoheatDataUpdateCoordinator) -> None: + def __init__( + self, + coordinator: AskoheatDataUpdateCoordinator, + entity_description: AskoheatEntityDescription, + ) -> None: """Initialize.""" super().__init__(coordinator) self._attr_unique_id = coordinator.config_entry.entry_id @@ -31,6 +37,19 @@ def __init__(self, coordinator: AskoheatDataUpdateCoordinator) -> None: ), }, ) + self.entity_description = entity_description + self.translation_key = ( + entity_description.translation_key or entity_description.key.value + ) + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + # initially initialize values + self._handle_coordinator_update() + + async def _data_update(self) -> None: + self._handle_coordinator_update() @callback def _handle_coordinator_update(self) -> None: @@ -47,3 +66,4 @@ def _handle_coordinator_update(self) -> None: self._attr_icon = descr.icon super()._handle_coordinator_update() + self.async_write_ha_state() diff --git a/custom_components/askoheat/model.py b/custom_components/askoheat/model.py index 3b4e344..d451e84 100644 --- a/custom_components/askoheat/model.py +++ b/custom_components/askoheat/model.py @@ -3,37 +3,44 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import date, datetime -from decimal import Decimal +from enum import StrEnum from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar from homeassistant.components.binary_sensor import BinarySensorEntityDescription +from homeassistant.components.number import NumberEntityDescription, NumberMode from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.switch import SwitchEntityDescription from homeassistant.const import Platform from homeassistant.helpers.entity import EntityDescription -from custom_components.askoheat.const import DOMAIN +from custom_components.askoheat.const import ( + DOMAIN, + BinarySensorAttrKey, + NumberAttrKey, + SensorAttrKey, + SwitchAttrKey, +) if TYPE_CHECKING: - from custom_components.askoheat.const import ( - BinarySensorAttrKey, - SensorAttrKey, - SwitchAttrKey, - ) + from datetime import date, datetime + from decimal import Decimal + + +K = TypeVar("K", bound=StrEnum) @dataclass(frozen=True) -class AdkoheatEntityDescription(EntityDescription): +class AskoheatEntityDescription[K](EntityDescription): """Class describing base askoheat entity.""" + key: K icon_by_state: dict[date | datetime | Decimal, str] | None = None @dataclass(frozen=True) class AskoheatBinarySensorEntityDescription( - AdkoheatEntityDescription, BinarySensorEntityDescription + AskoheatEntityDescription[BinarySensorAttrKey], BinarySensorEntityDescription ): """Class describing Askoheat binary sensor entities.""" @@ -53,7 +60,7 @@ def data_key(self) -> str: @dataclass(frozen=True) class AskoheatSwitchEntityDescription( - AdkoheatEntityDescription, + AskoheatEntityDescription[SwitchAttrKey], SwitchEntityDescription, ): """Class describing Askoheat switch entities.""" @@ -74,7 +81,7 @@ def data_key(self) -> str: @dataclass(frozen=True) class AskoheatSensorEntityDescription( - AdkoheatEntityDescription, + AskoheatEntityDescription[SensorAttrKey], SensorEntityDescription, ): """Class describing Askoheat sensor entities.""" @@ -89,3 +96,24 @@ class AskoheatSensorEntityDescription( def data_key(self) -> str: """Get data key.""" return f"sensor.{self.key}" + + +@dataclass(frozen=True) +class AskoheatNumberEntityDescription( + AskoheatEntityDescription[NumberAttrKey], + NumberEntityDescription, +): + """Class describing Askoheat number entities.""" + + key: NumberAttrKey + platform = Platform.NUMBER + factor: float | None = None + native_precision: int | None = None + domain = DOMAIN + mode: NumberMode = NumberMode.AUTO + native_default_value: float | None = None + + @cached_property + def data_key(self) -> str: + """Get data key.""" + return f"number.{self.key}" diff --git a/custom_components/askoheat/number.py b/custom_components/askoheat/number.py new file mode 100644 index 0000000..0a0d6e6 --- /dev/null +++ b/custom_components/askoheat/number.py @@ -0,0 +1,74 @@ +"""Number platform for askoheat.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.components.number import ENTITY_ID_FORMAT, NumberEntity +from homeassistant.core import callback + +from custom_components.askoheat.number_entities_config import ( + CONF_NUMBER_ENTITY_DESCRIPTIONS, +) + +from .entity import AskoheatEntity + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from custom_components.askoheat.model import ( + AskoheatNumberEntityDescription, + ) + + from .coordinator import AskoheatDataUpdateCoordinator + from .data import AskoheatConfigEntry + + +async def async_setup_entry( + hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` + entry: AskoheatConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the number platform.""" + async_add_entities( + AskoHeatNumber( + coordinator=entry.runtime_data.config_coordinator, + entity_description=entity_description, + ) + for entity_description in CONF_NUMBER_ENTITY_DESCRIPTIONS + ) + + +class AskoHeatNumber(AskoheatEntity, NumberEntity): + """askoheat number class.""" + + entity_description: AskoheatNumberEntityDescription + + def __init__( + self, + coordinator: AskoheatDataUpdateCoordinator, + entity_description: AskoheatNumberEntityDescription, + ) -> None: + """Initialize the number class.""" + super().__init__(coordinator, entity_description) + self.entity_id = ENTITY_ID_FORMAT.format(entity_description.key) + self._attr_unique_id = self.entity_id + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + data = self.coordinator.data + if data is None: + return + self._attr_native_value = data[self.entity_description.data_key] + + if self._attr_native_value is not None: + if self.entity_description.factor is not None: + self._attr_native_value *= self.entity_description.factor + if self.entity_description.native_precision is not None: + self._attr_native_value = round( + self._attr_native_value, self.entity_description.native_precision + ) + self.async_write_ha_state() + super()._handle_coordinator_update() diff --git a/custom_components/askoheat/number_entities_config.py b/custom_components/askoheat/number_entities_config.py new file mode 100644 index 0000000..49c9137 --- /dev/null +++ b/custom_components/askoheat/number_entities_config.py @@ -0,0 +1,430 @@ +"""Predefined configuration askoheat number entities.""" + +from homeassistant.components.number import NumberMode +from homeassistant.const import ( + EntityCategory, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, +) + +from custom_components.askoheat.const import NumberAttrKey +from custom_components.askoheat.model import ( + AskoheatNumberEntityDescription, +) + +CONF_NUMBER_ENTITY_DESCRIPTIONS = ( + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_RELAY_SEC_COUNT_SECONDS, + native_min_value=0, + native_max_value=16, + native_default_value=5, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:timelapse", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_PUMP_SEC_COUNT_SECONDS, + native_min_value=0, + native_max_value=240, + native_default_value=30, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:timelapse", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_AUTO_HEATER_OFF_MINUTES, + native_min_value=2, + native_max_value=10080, + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:timer-outline", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_CASCADE_PRIORIZATION, + native_min_value=0, + native_max_value=255, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:priority-high", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_HEATBUFFER_VOLUME, + native_min_value=0, + native_max_value=1000, + native_unit_of_measurement=UnitOfVolume.LITERS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:waves", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_LEGIO_PROTECTION_TEMPERATURE, + native_min_value=50, + native_max_value=65, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:water-thermometer", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_LEGIO_PROTECTION_HEATUP_MINUTES, + native_min_value=0, + native_max_value=1440, + native_default_value=240, + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:timelapse", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_NUMBER_OF_HOUSEHOLD_MEMBERS, + native_min_value=1, + native_max_value=255, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:human-male-female-child", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_LOAD_FEEDIN_BASIC_ENERGY_LEVEL, + native_min_value=0, + native_max_value=10000, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:lightning-bolt", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_TIMEZONE_OFFSET, + native_min_value=-12, + native_max_value=12, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:map-clock", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_LOAD_FEEDIN_DELAY_SECONDS, + native_min_value=0, + native_max_value=120, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:timer-sand", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_RTU_SLAVE_ID, + native_min_value=0, + native_max_value=240, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:identifier", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_HYSTERESIS, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-downhill", + ), + # Analog input 0 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:gauge", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 1 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_1_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_1_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_1_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 2 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_2_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_2_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_2_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 3 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_3_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_3_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_3_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 4 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_4_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_4_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_4_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 5 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_5_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_5_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_5_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 6 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_6_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_6_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_6_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + # Analog input 7 + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:slope-uphill", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + native_default_value=7, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_HEAT_PUMP_REQUEST_OFF_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_HEAT_PUMP_REQUEST_ON_STEP, + native_min_value=0, + native_max_value=7, + native_step=1, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_EMERGENCY_MODE_ON_STOP, + native_min_value=0, + native_max_value=7, + native_step=1, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.SLIDER, + icon="mdi:stairs", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_TEMPERATURE_HYSTERESIS, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_MINIMAL_TEMPERATURE, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer-minus", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_SET_HEATER_STEP_TEMPERATURE_LIMIT, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer-chevron-up", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_LOAD_FEEDIN_OR_SETPOINT_TEMPERATURE_LIMIT, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer-chevron-up", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_LOW_TARIFF_TEMPERATURE_LIMIT, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer-chevron-up", + ), + AskoheatNumberEntityDescription( + key=NumberAttrKey.CON_HEATPUMP_REQUEST_TEMPERATURE_LIMIT, + native_min_value=0, + native_max_value=95, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.CONFIG, + mode=NumberMode.BOX, + icon="mdi:thermometer-chevron-up", + ), +) diff --git a/custom_components/askoheat/sensor.py b/custom_components/askoheat/sensor.py index 8ad286c..b3e5ce3 100644 --- a/custom_components/askoheat/sensor.py +++ b/custom_components/askoheat/sensor.py @@ -50,8 +50,7 @@ def __init__( entity_description: AskoheatSensorEntityDescription, ) -> None: """Initialize the sensor class.""" - super().__init__(coordinator) - self.entity_description = entity_description + super().__init__(coordinator, entity_description) self.entity_id = ENTITY_ID_FORMAT.format(entity_description.key) self._attr_unique_id = self.entity_id @@ -82,5 +81,4 @@ def _handle_coordinator_update(self) -> None: ) self._attr_native_value = float_value - self.async_write_ha_state() super()._handle_coordinator_update() diff --git a/custom_components/askoheat/sensor_entities_ema.py b/custom_components/askoheat/sensor_entities_ema.py index 038d4d8..4f067aa 100644 --- a/custom_components/askoheat/sensor_entities_ema.py +++ b/custom_components/askoheat/sensor_entities_ema.py @@ -5,7 +5,6 @@ SensorStateClass, ) from homeassistant.const import ( - EntityCategory, UnitOfElectricPotential, UnitOfPower, UnitOfTemperature, @@ -17,7 +16,6 @@ EMA_SENSOR_ENTITY_DESCRIPTIONS = ( AskoheatSensorEntityDescription( key=SensorAttrKey.ANALOG_INPUT_VALUE, - translation_key=SensorAttrKey.ANALOG_INPUT_VALUE, icon="mdi:gauge", native_precision=0, state_class=SensorStateClass.MEASUREMENT, @@ -27,8 +25,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.HEATER_LOAD, - translation_key=SensorAttrKey.HEATER_LOAD, - icon="mdi:lightning-bold", + icon="mdi:lightning-bolt", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, @@ -36,7 +33,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.LOAD_FEEDIN_VALUE, - translation_key=SensorAttrKey.LOAD_FEEDIN_VALUE, icon="mdi:solar-power", native_precision=0, state_class=SensorStateClass.MEASUREMENT, @@ -46,7 +42,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.LOAD_SETPOINT_VALUE, - translation_key=SensorAttrKey.LOAD_SETPOINT_VALUE, icon="mdi:lightning-bolt", native_precision=0, state_class=SensorStateClass.MEASUREMENT, @@ -56,7 +51,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE, - translation_key=SensorAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, @@ -66,7 +60,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE, - translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, @@ -76,7 +69,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE, - translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, @@ -86,7 +78,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE, - translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, @@ -96,7 +87,6 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE, - translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, diff --git a/custom_components/askoheat/switch.py b/custom_components/askoheat/switch.py index 5be8a85..9d63f6c 100644 --- a/custom_components/askoheat/switch.py +++ b/custom_components/askoheat/switch.py @@ -4,9 +4,12 @@ from typing import TYPE_CHECKING, Any -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity +from homeassistant.core import callback -from custom_components.askoheat.model import AskoheatSwitchEntityDescription +from custom_components.askoheat.switch_entities_ema import ( + EMA_SWITCH_ENTITY_DESCRIPTIONS, +) from .entity import AskoheatEntity @@ -14,17 +17,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback + from custom_components.askoheat.model import AskoheatSwitchEntityDescription + from .coordinator import AskoheatDataUpdateCoordinator from .data import AskoheatConfigEntry -ENTITY_DESCRIPTIONS = ( - SwitchEntityDescription( - key="askoheat", - name="Integration Switch", - icon="mdi:format-quote-close", - ), -) - async def async_setup_entry( hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` @@ -32,13 +29,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the switch platform.""" - # async_add_entities( - # IntegrationBlueprintSwitch( - # coordinator=entry.runtime_data.coordinator, - # entity_description=entity_description, - # ) - # for entity_description in ENTITY_DESCRIPTIONS - # ) + async_add_entities( + AskoHeatSwitch( + coordinator=entry.runtime_data.ema_coordinator, + entity_description=entity_description, + ) + for entity_description in EMA_SWITCH_ENTITY_DESCRIPTIONS + ) class AskoHeatSwitch(AskoheatEntity, SwitchEntity): @@ -52,20 +49,38 @@ def __init__( entity_description: AskoheatSwitchEntityDescription, ) -> None: """Initialize the switch class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return true if the switch is on.""" - return self.coordinator.data.get("title", "") == "foo" + super().__init__(coordinator, entity_description) + self.entity_id = ENTITY_ID_FORMAT.format(entity_description.key) + self._attr_unique_id = self.entity_id + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + data = self.coordinator.data + if data is None: + return + self._attr_state = data[self.entity_description.data_key] + if ( + self.entity_description.on_state is True + or self.entity_description.on_state is False + ) and self._attr_state is not None: + self._attr_state = bool(self._attr_state) # type: ignore # noqa: PGH003 + if self.entity_description.inverted: + self._attr_is_on = self._attr_state != self.entity_description.on_state + else: + self._attr_is_on = self._attr_state == self.entity_description.on_state or ( + self.entity_description.on_states is not None + and self._attr_state in self.entity_description.on_states + ) + + super()._handle_coordinator_update() async def async_turn_on(self, **_: Any) -> None: """Turn on the switch.""" # await self.coordinator.config_entry.runtime_data.client.async_set_title("bar") - await self.coordinator.async_request_refresh() + # await self.coordinator.async_request_refresh() async def async_turn_off(self, **_: Any) -> None: """Turn off the switch.""" # await self.coordinator.config_entry.runtime_data.client.async_set_title("foo") - await self.coordinator.async_request_refresh() + # await self.coordinator.async_request_refresh() diff --git a/custom_components/askoheat/switch_entities_ema.py b/custom_components/askoheat/switch_entities_ema.py new file mode 100644 index 0000000..d15abb5 --- /dev/null +++ b/custom_components/askoheat/switch_entities_ema.py @@ -0,0 +1,22 @@ +"""Predefined energy manager switches.""" + +from custom_components.askoheat.const import SwitchAttrKey +from custom_components.askoheat.model import AskoheatSwitchEntityDescription + +EMA_SWITCH_ENTITY_DESCRIPTIONS = ( + AskoheatSwitchEntityDescription( + key=SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER1, + icon="mdi:heat-wave", + entity_category=None, + ), + AskoheatSwitchEntityDescription( + key=SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER2, + icon="mdi:heat-wave", + entity_category=None, + ), + AskoheatSwitchEntityDescription( + key=SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER3, + icon="mdi:heat-wave", + entity_category=None, + ), +)