From 644557aa07dd827d2851703e9facc7a4eaac2cb3 Mon Sep 17 00:00:00 2001 From: "mike.toggweiler" Date: Thu, 21 Nov 2024 23:19:24 +0100 Subject: [PATCH] added sensors of data/operation block including calculated duration in minutes --- custom_components/askoheat/__init__.py | 4 + custom_components/askoheat/api.py | 96 +++- custom_components/askoheat/api_conf_desc.py | 38 +- custom_components/askoheat/api_desc.py | 24 +- custom_components/askoheat/api_ema_desc.py | 24 +- custom_components/askoheat/api_op_desc.py | 514 ++++++++++++++++++++ custom_components/askoheat/api_par_desc.py | 20 +- custom_components/askoheat/binary_sensor.py | 5 + custom_components/askoheat/const.py | 78 ++- custom_components/askoheat/coordinator.py | 29 +- custom_components/askoheat/data.py | 2 + custom_components/askoheat/model.py | 23 +- custom_components/askoheat/sensor.py | 111 ++++- 13 files changed, 878 insertions(+), 90 deletions(-) create mode 100644 custom_components/askoheat/api_op_desc.py diff --git a/custom_components/askoheat/__init__.py b/custom_components/askoheat/__init__.py index 096072b..ca6692e 100644 --- a/custom_components/askoheat/__init__.py +++ b/custom_components/askoheat/__init__.py @@ -15,6 +15,7 @@ from .coordinator import ( AskoheatConfigDataUpdateCoordinator, AskoheatEMADataUpdateCoordinator, + AskoheatOperationDataUpdateCoordinator, AskoheatParameterDataUpdateCoordinator, ) from .data import AskoheatData @@ -49,6 +50,7 @@ async def async_setup_entry( hass=hass, ) config_coordinator = AskoheatConfigDataUpdateCoordinator(hass=hass) + data_coordinator = AskoheatOperationDataUpdateCoordinator(hass=hass) entry.runtime_data = AskoheatData( client=AskoHeatModbusApiClient( host=entry.data[CONF_HOST], @@ -58,6 +60,7 @@ async def async_setup_entry( ema_coordinator=ema_coordinator, config_coordinator=config_coordinator, par_coordinator=par_coordinator, + data_coordinator=data_coordinator, ) await entry.runtime_data.client.connect() @@ -65,6 +68,7 @@ async def async_setup_entry( await par_coordinator.async_config_entry_first_refresh() await ema_coordinator.async_config_entry_first_refresh() await config_coordinator.async_config_entry_first_refresh() + await data_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 a22e5c4..c6452ce 100644 --- a/custom_components/askoheat/api.py +++ b/custom_components/askoheat/api.py @@ -4,6 +4,9 @@ from datetime import time from enum import ReprEnum +from struct import unpack +import struct +from tracemalloc import start from typing import TYPE_CHECKING, Any, TypeVar, cast import numpy as np @@ -18,13 +21,16 @@ IntEnumInputDescriptor, RegisterBlockDescriptor, RegisterInputDescriptor, - SignedIntRegisterInputDescriptor, + SignedInt16RegisterInputDescriptor, StrEnumInputDescriptor, StringRegisterInputDescriptor, + StructRegisterInputDescriptor, TimeRegisterInputDescriptor, - UnsignedIntRegisterInputDescriptor, + UnsignedInt16RegisterInputDescriptor, + UnsignedInt32RegisterInputDescriptor, ) from custom_components.askoheat.api_ema_desc import EMA_REGISTER_BLOCK_DESCRIPTOR +from custom_components.askoheat.api_op_desc import DATA_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.api_par_desc import PARAMETER_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.const import ( LOGGER, @@ -135,6 +141,15 @@ async def async_write_config_data( ) return await self.async_read_config_data() + async def async_read_op_data(self) -> AskoheatDataBlock: + """Read OP data states.""" + data = await self.__async_read_input_registers_data( + DATA_REGISTER_BLOCK_DESCRIPTOR.starting_register, + DATA_REGISTER_BLOCK_DESCRIPTOR.number_of_registers, + ) + LOGGER.debug("async_read_op_data %s", data) + return self.__map_data(DATA_REGISTER_BLOCK_DESCRIPTOR, data) + async def __async_read_single_input_register( self, address: int, @@ -192,9 +207,11 @@ async def _prepare_register_value( ) case ByteRegisterInputDescriptor(): result = _prepare_byte(value) - case UnsignedIntRegisterInputDescriptor(): + case UnsignedInt16RegisterInputDescriptor(): result = _prepare_uint16(value) - case SignedIntRegisterInputDescriptor(): + case UnsignedInt32RegisterInputDescriptor(): + result = _prepare_uint32(value) + case SignedInt16RegisterInputDescriptor(): result = _prepare_int16(value) case Float32RegisterInputDescriptor(): result = _prepare_float32(value) @@ -206,6 +223,8 @@ async def _prepare_register_value( result = _prepare_str(value.value) # type: ignore # noqa: PGH003 case IntEnumInputDescriptor(): result = _prepare_byte(value.value) # type: ignore # noqa: PGH003 + case StructRegisterInputDescriptor(_, _, structure): + result = _prepare_struct(value, structure) case _: LOGGER.error("Cannot read number input from descriptor %r", desc) result = [] @@ -281,7 +300,7 @@ def __map_data( ) -def _read_register_input(data: ModbusPDU, desc: RegisterInputDescriptor) -> object: +def _read_register_input(data: ModbusPDU, desc: RegisterInputDescriptor) -> object: # noqa: PLR0912 match desc: case FlagRegisterInputDescriptor(starting_register, bit): result = _read_flag(data.registers[starting_register], bit) @@ -289,9 +308,17 @@ def _read_register_input(data: ModbusPDU, desc: RegisterInputDescriptor) -> obje result = factory(_read_byte(data.registers[starting_register])) case ByteRegisterInputDescriptor(starting_register): result = _read_byte(data.registers[starting_register]) - case UnsignedIntRegisterInputDescriptor(starting_register): + case UnsignedInt16RegisterInputDescriptor(starting_register): result = _read_uint16(data.registers[starting_register]) - case SignedIntRegisterInputDescriptor(starting_register): + case UnsignedInt32RegisterInputDescriptor(starting_register): + result = _read_uint32( + data.registers[starting_register : starting_register + 2] + ) + case UnsignedInt32RegisterInputDescriptor(starting_register): + result = _read_uint32( + data.registers[starting_register : starting_register + 2] + ) + case SignedInt16RegisterInputDescriptor(starting_register): result = _read_int16(data.registers[starting_register]) case Float32RegisterInputDescriptor(starting_register): result = _read_float32( @@ -314,6 +341,10 @@ def _read_register_input(data: ModbusPDU, desc: RegisterInputDescriptor) -> obje register_value_hours=data.registers[starting_register], register_value_minutes=data.registers[starting_register + 1], ) + case StructRegisterInputDescriptor(starting_register, bytes, structure): + result = _read_struct( + data.registers[starting_register : starting_register + bytes], structure + ) case _: LOGGER.error("Cannot read number input from descriptor %r", desc) result = None @@ -502,7 +533,7 @@ def _read_uint16(register_value: int) -> np.uint16: def _prepare_uint16(value: object) -> list[int]: - """Prepare unsigned int value for writing to registers.""" + """Prepare unsigned int 16 value for writing to registers.""" if not isinstance(value, number | float): LOGGER.error( "Cannot convert value %s as unsigned int, wrong datatype %r", @@ -514,6 +545,28 @@ def _prepare_uint16(value: object) -> list[int]: return ModbusClient.convert_to_registers(int(value), ModbusClient.DATATYPE.UINT16) +def _read_uint32(register_values: list[int]) -> np.uint32: + """Read register value as uint32.""" + return np.uint32( + ModbusClient.convert_from_registers( + register_values, ModbusClient.DATATYPE.UINT32 + ) + ) + + +def _prepare_uint32(value: object) -> list[int]: + """Prepare unsigned int 32 value for writing to registers.""" + if not isinstance(value, number | float): + LOGGER.error( + "Cannot convert value %s as unsigned int, wrong datatype %r", + value, + type(value), + ) + return [] + + return ModbusClient.convert_to_registers(int(value), ModbusClient.DATATYPE.UINT32) + + def _read_float32(register_values: list[int]) -> np.float32: """Read register value as uint16.""" return np.float32( @@ -556,3 +609,30 @@ def _prepare_flag(register_value: int, flag: object, index: int) -> list[int]: # minus flag from current registers value return [register_value - (True << index)] + + +def _read_struct(register_values: list[int], structure: str | bytes) -> Any | None: + """Read register values and unpack using pyhton struct.""" + byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in register_values]) + if byte_string == b"nan\x00": + return None + + try: + val = struct.unpack(structure, byte_string) + except struct.error as err: + recv_size = len(register_values) * 2 + msg = f"Received {recv_size} bytes, unpack error {err}" + LOGGER.error(msg) + return None + LOGGER.debug("read struct:%s, %s => %s", register_values, byte_string, val) + if len(val) == 1: + return val[0] + return val + + +def _prepare_struct(value: object, structure: str | bytes) -> list[int]: + """Pack value based on python struct for writing to registers.""" + as_bytes = struct.pack(structure, value) + return [ + int.from_bytes(as_bytes[i : i + 2], "big") for i in range(0, len(as_bytes), 2) + ] diff --git a/custom_components/askoheat/api_conf_desc.py b/custom_components/askoheat/api_conf_desc.py index 4944f92..556ce80 100644 --- a/custom_components/askoheat/api_conf_desc.py +++ b/custom_components/askoheat/api_conf_desc.py @@ -19,11 +19,11 @@ Float32RegisterInputDescriptor, IntEnumInputDescriptor, RegisterBlockDescriptor, - SignedIntRegisterInputDescriptor, + SignedInt16RegisterInputDescriptor, StrEnumInputDescriptor, StringRegisterInputDescriptor, TimeRegisterInputDescriptor, - UnsignedIntRegisterInputDescriptor, + UnsignedInt16RegisterInputDescriptor, ) from custom_components.askoheat.const import ( Baudrate, @@ -57,7 +57,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:timelapse", - api_descriptor=UnsignedIntRegisterInputDescriptor(0), + api_descriptor=UnsignedInt16RegisterInputDescriptor(0), entity_registry_enabled_default=False, ), AskoheatNumberEntityDescription( @@ -70,7 +70,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:timelapse", - api_descriptor=UnsignedIntRegisterInputDescriptor(1), + api_descriptor=UnsignedInt16RegisterInputDescriptor(1), entity_registry_enabled_default=False, ), AskoheatNumberEntityDescription( @@ -82,7 +82,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:timer-outline", - api_descriptor=UnsignedIntRegisterInputDescriptor(3), + api_descriptor=UnsignedInt16RegisterInputDescriptor(3), ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_CASCADE_PRIORIZATION, @@ -103,7 +103,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:waves", - api_descriptor=UnsignedIntRegisterInputDescriptor(7), + api_descriptor=UnsignedInt16RegisterInputDescriptor(7), ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_LEGIO_PROTECTION_TEMPERATURE, @@ -126,7 +126,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:timelapse", - api_descriptor=UnsignedIntRegisterInputDescriptor(11), + api_descriptor=UnsignedInt16RegisterInputDescriptor(11), ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_NUMBER_OF_HOUSEHOLD_MEMBERS, @@ -147,7 +147,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:timer-sand", - api_descriptor=UnsignedIntRegisterInputDescriptor(39), + api_descriptor=UnsignedInt16RegisterInputDescriptor(39), ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_LOAD_FEEDIN_BASIC_ENERGY_LEVEL, @@ -158,7 +158,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.BOX, icon="mdi:lightning-bolt", - api_descriptor=UnsignedIntRegisterInputDescriptor(40), + api_descriptor=UnsignedInt16RegisterInputDescriptor(40), ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_TIMEZONE_OFFSET, @@ -168,7 +168,7 @@ entity_category=EntityCategory.CONFIG, mode=NumberMode.SLIDER, icon="mdi:map-clock", - api_descriptor=SignedIntRegisterInputDescriptor(41), + api_descriptor=SignedInt16RegisterInputDescriptor(41), ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_RTU_SLAVE_ID, @@ -454,8 +454,8 @@ api_descriptor=ByteRegisterInputDescriptor(89), ), AskoheatNumberEntityDescription( - key=NumberAttrKey.CON_HEAT_PUMP_REQUEST_OFF_STEP, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + key=NumberAttrKey.CON_HEATPUMP_REQUEST_OFF_STEP, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, native_min_value=0, native_max_value=7, native_step=1, @@ -465,8 +465,8 @@ api_descriptor=ByteRegisterInputDescriptor(90), ), AskoheatNumberEntityDescription( - key=NumberAttrKey.CON_HEAT_PUMP_REQUEST_ON_STEP, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + key=NumberAttrKey.CON_HEATPUMP_REQUEST_ON_STEP, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, native_min_value=0, native_max_value=7, native_step=1, @@ -543,7 +543,7 @@ ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_HEATPUMP_REQUEST_TEMPERATURE_LIMIT, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, native_min_value=0, native_max_value=95, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -600,7 +600,7 @@ ), AskoheatSwitchEntityDescription( key=SwitchAttrKey.CON_HEATPUMP_REQUEST_INPUT_ENABLED, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, entity_category=EntityCategory.CONFIG, icon="mdi:heat-pump", api_descriptor=FlagRegisterInputDescriptor(2, 6), @@ -678,8 +678,8 @@ entity_registry_enabled_default=False, ), AskoheatSwitchEntityDescription( - key=SwitchAttrKey.CON_AUTO_OFF_HEAT_PUMP_REQUEST_ENABLED, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + key=SwitchAttrKey.CON_AUTO_OFF_HEATPUMP_REQUEST_ENABLED, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, entity_category=EntityCategory.CONFIG, icon="mdi:timer-cancel", api_descriptor=FlagRegisterInputDescriptor(4, 6), @@ -739,7 +739,7 @@ entity_registry_enabled_default=False, ), AskoheatSwitchEntityDescription( - key=SwitchAttrKey.CON_HEATBUFFER_TYPE_HEAT_PUMP, + key=SwitchAttrKey.CON_HEATBUFFER_TYPE_HEATPUMP, device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, entity_category=EntityCategory.CONFIG, icon="mdi:water-boiler", diff --git a/custom_components/askoheat/api_desc.py b/custom_components/askoheat/api_desc.py index 37a292e..a7be48a 100644 --- a/custom_components/askoheat/api_desc.py +++ b/custom_components/askoheat/api_desc.py @@ -2,6 +2,8 @@ from __future__ import annotations +import ctypes +from struct import pack import typing from abc import ABC from dataclasses import dataclass, field @@ -53,13 +55,18 @@ class Float32RegisterInputDescriptor(RegisterInputDescriptor): @dataclass(frozen=True) -class SignedIntRegisterInputDescriptor(RegisterInputDescriptor): - """Input register representing a signed int.""" +class SignedInt16RegisterInputDescriptor(RegisterInputDescriptor): + """Input register representing a signed int16.""" @dataclass(frozen=True) -class UnsignedIntRegisterInputDescriptor(RegisterInputDescriptor): - """Input register representing an unsigned int.""" +class UnsignedInt16RegisterInputDescriptor(RegisterInputDescriptor): + """Input register representing an unsigned int 16.""" + + +@dataclass(frozen=True) +class UnsignedInt32RegisterInputDescriptor(RegisterInputDescriptor): + """Input register representing an unsigned int 32.""" @dataclass(frozen=True) @@ -74,6 +81,15 @@ class TimeRegisterInputDescriptor(RegisterInputDescriptor): """Input register representing a time string combined of two following registers.""" +@dataclass(frozen=True) +class StructRegisterInputDescriptor(RegisterInputDescriptor): + """Input register packing and unpacking values based on pyhtons struct.""" + + number_of_bytes: int + # format as defined in pyhton struct https://docs.python.org/3/library/struct.html + structure: str | bytes + + E = TypeVar("E", bound=StrEnum) diff --git a/custom_components/askoheat/api_ema_desc.py b/custom_components/askoheat/api_ema_desc.py index f135d6d..ecb40e2 100644 --- a/custom_components/askoheat/api_ema_desc.py +++ b/custom_components/askoheat/api_ema_desc.py @@ -20,8 +20,8 @@ FlagRegisterInputDescriptor, Float32RegisterInputDescriptor, RegisterBlockDescriptor, - SignedIntRegisterInputDescriptor, - UnsignedIntRegisterInputDescriptor, + SignedInt16RegisterInputDescriptor, + UnsignedInt16RegisterInputDescriptor, ) from custom_components.askoheat.const import ( BinarySensorAttrKey, @@ -41,43 +41,43 @@ binary_sensors=[ AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEATER1_ACTIVE, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, icon="mdi:power-plug", device_class=BinarySensorDeviceClass.RUNNING, api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=0), ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEATER2_ACTIVE, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, icon="mdi:power-plug", device_class=BinarySensorDeviceClass.RUNNING, api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=1), ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.HEATER3_ACTIVE, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, icon="mdi:power-plug", device_class=BinarySensorDeviceClass.RUNNING, api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=2), ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.PUMP_ACTIVE, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, icon="mdi:pump", device_class=BinarySensorDeviceClass.RUNNING, api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=3), ), AskoheatBinarySensorEntityDescription( key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, icon="mdi:connection", device_class=BinarySensorDeviceClass.PROBLEM, api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=4), ), # bit 5 ignored AskoheatBinarySensorEntityDescription( - key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE, - device_key=DeviceKey.HEAT_PUMP_CONTROL_UNIT, + key=BinarySensorAttrKey.HEATPUMP_REQUEST_ACTIVE, + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, icon="mdi:heat-pump", device_class=BinarySensorDeviceClass.RUNNING, api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=6), @@ -156,7 +156,7 @@ device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, entity_category=None, - api_descriptor=UnsignedIntRegisterInputDescriptor(17), + api_descriptor=UnsignedInt16RegisterInputDescriptor(17), ), AskoheatSensorEntityDescription( key=SensorAttrKey.ANALOG_INPUT_VALUE, @@ -246,7 +246,7 @@ native_precision=0, native_unit_of_measurement=UnitOfPower.WATT, entity_category=None, - api_descriptor=SignedIntRegisterInputDescriptor(19), + api_descriptor=SignedInt16RegisterInputDescriptor(19), ), AskoheatNumberEntityDescription( key=NumberAttrKey.LOAD_FEEDIN_VALUE, @@ -257,7 +257,7 @@ native_precision=0, native_unit_of_measurement=UnitOfPower.WATT, entity_category=None, - api_descriptor=SignedIntRegisterInputDescriptor(20), + api_descriptor=SignedInt16RegisterInputDescriptor(20), ), ], ) diff --git a/custom_components/askoheat/api_op_desc.py b/custom_components/askoheat/api_op_desc.py new file mode 100644 index 0000000..a6fa1ce --- /dev/null +++ b/custom_components/askoheat/api_op_desc.py @@ -0,0 +1,514 @@ +"""Operating block Api descriptor classes.""" + +# http://www.download.askoma.com/askofamily_plus/modbus/askoheat-modbus.html#Data_Values_Block +from __future__ import annotations + +from homeassistant.components.sensor.const import ( + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import EntityCategory, UnitOfTime + +from custom_components.askoheat.api_desc import ( + ByteRegisterInputDescriptor, + FlagRegisterInputDescriptor, + RegisterBlockDescriptor, + StructRegisterInputDescriptor, + UnsignedInt16RegisterInputDescriptor, + UnsignedInt32RegisterInputDescriptor, +) +from custom_components.askoheat.const import ( + BinarySensorAttrKey, + DeviceKey, + SensorAttrKey, +) +from custom_components.askoheat.model import ( + AskoheatBinarySensorEntityDescription, + AskoheatDurationSensorEntityDescription, + AskoheatSensorEntityDescription, +) + +DATA_REGISTER_BLOCK_DESCRIPTOR = RegisterBlockDescriptor( + starting_register=600, + number_of_registers=98, + sensors=[ + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_MINUTES, + api_descriptor=StructRegisterInputDescriptor(0, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER1_MINUTES, + api_descriptor=StructRegisterInputDescriptor(2, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER2_MINUTES, + api_descriptor=StructRegisterInputDescriptor(4, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER3_MINUTES, + api_descriptor=StructRegisterInputDescriptor(6, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_PUMP_MINUTES, + api_descriptor=StructRegisterInputDescriptor(8, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_VALVE_MINUTES, + api_descriptor=StructRegisterInputDescriptor(10, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_SWITCH_COUNT_RELAY1, + api_descriptor=UnsignedInt32RegisterInputDescriptor(12), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_SWITCH_COUNT_RELAY2, + api_descriptor=UnsignedInt32RegisterInputDescriptor(14), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_SWITCH_COUNT_RELAY3, + api_descriptor=UnsignedInt32RegisterInputDescriptor(16), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_SWITCH_COUNT_RELAY4, + api_descriptor=UnsignedInt32RegisterInputDescriptor(18), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + icon="mdi:counter", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_SINCE_LAST_LEGIO_ACTIVATION_MINUTES, + api_descriptor=StructRegisterInputDescriptor(28, 2, ">L"), + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_LEGIO_PLATEAU_TIMER, + api_descriptor=UnsignedInt16RegisterInputDescriptor(30), + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:av-timer", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_ANALOG_INPUT_STEP, + api_descriptor=ByteRegisterInputDescriptor(39), + device_key=DeviceKey.ANALOG_INPUT_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + native_min_value=0, + native_max_value=7, + icon="mdi:stairs", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_ACTUAL_TEMP_LIMIT, + api_descriptor=UnsignedInt16RegisterInputDescriptor(40), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + native_min_value=0, + native_max_value=99, + icon="mdi:water-thermometer", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_AUTO_HEATER_OFF_COUNTDOWN_MINUTES, + api_descriptor=UnsignedInt32RegisterInputDescriptor(41), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_min_value=0, + native_max_value=1440, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_EMERGENCY_OFF_COUNTDOWN_MINUTES, + api_descriptor=UnsignedInt32RegisterInputDescriptor(43), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_min_value=0, + native_max_value=1440, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_BOOT_COUNT, + api_descriptor=UnsignedInt32RegisterInputDescriptor(45), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:counter", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_SET_HEATER_STEP, + api_descriptor=StructRegisterInputDescriptor(47, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_LOAD_SETPOINT, + api_descriptor=StructRegisterInputDescriptor(49, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_LOAD_FEEDIN, + api_descriptor=StructRegisterInputDescriptor(51, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATPUMP_REQUEST, + api_descriptor=StructRegisterInputDescriptor(53, 2, ">L"), + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_ANALOG_INPUT, + api_descriptor=StructRegisterInputDescriptor(55, 2, ">L"), + device_key=DeviceKey.ANALOG_INPUT_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_EMERGENCY_MODE, + api_descriptor=StructRegisterInputDescriptor(57, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_LEGIO_PROTECTION, + api_descriptor=StructRegisterInputDescriptor(59, 2, ">L"), + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_LOW_TARIFF, + api_descriptor=StructRegisterInputDescriptor(61, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_MINIMAL_TEMP, + api_descriptor=StructRegisterInputDescriptor(63, 2, ">L"), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP1, + api_descriptor=StructRegisterInputDescriptor(65, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP2, + api_descriptor=StructRegisterInputDescriptor(67, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP3, + api_descriptor=StructRegisterInputDescriptor(69, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP4, + api_descriptor=StructRegisterInputDescriptor(71, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP5, + api_descriptor=StructRegisterInputDescriptor(73, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP6, + api_descriptor=StructRegisterInputDescriptor(75, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatDurationSensorEntityDescription( + key=SensorAttrKey.DATA_OPERATING_TIME_HEATER_STEP7, + api_descriptor=StructRegisterInputDescriptor(77, 2, ">L"), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:progress-clock", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_SET_HEATER_STEP, + api_descriptor=UnsignedInt32RegisterInputDescriptor(79), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_LOAD_SETPOINT, + api_descriptor=UnsignedInt32RegisterInputDescriptor(81), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_LOAD_FEEDIN, + api_descriptor=UnsignedInt32RegisterInputDescriptor(83), + device_key=DeviceKey.ENERGY_MANAGER, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_HEATPUMP_REQUEST, + api_descriptor=UnsignedInt32RegisterInputDescriptor(85), + device_key=DeviceKey.HEATPUMP_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_ANALOG_INPUT, + api_descriptor=UnsignedInt32RegisterInputDescriptor(87), + device_key=DeviceKey.ANALOG_INPUT_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_EMERGENCY_MODE, + api_descriptor=UnsignedInt32RegisterInputDescriptor(89), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_LEGIO_PROTECTION, + api_descriptor=UnsignedInt32RegisterInputDescriptor(91), + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_LOW_TARIFF, + api_descriptor=UnsignedInt32RegisterInputDescriptor(93), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_COUNT_MINIMAL_TEMP, + api_descriptor=UnsignedInt32RegisterInputDescriptor(95), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.TOTAL_INCREASING, + native_min_value=0, + native_max_value=100000, + icon="mdi:counter", + ), + AskoheatSensorEntityDescription( + key=SensorAttrKey.DATA_MAX_MEASURED_TEMP, + api_descriptor=ByteRegisterInputDescriptor(97), + device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + native_min_value=0, + native_max_value=255, + icon="mdi:thermometer-high", + ), + ], + binary_sensors=[ + # legio status flags + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_HEATING_UP, + api_descriptor=FlagRegisterInputDescriptor(27, 0), + icon="mdi:thermometer-chevron-up", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_TEMPERATURE_REACHED, + api_descriptor=FlagRegisterInputDescriptor(27, 1), + icon="mdi:thermometer-check", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_TEMP_REACHED_OUTSIDE_INTERVAL, + api_descriptor=FlagRegisterInputDescriptor(27, 2), + icon="mdi:thermometer-check", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + # bit 3 ignored + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_UNEXPECTED_TEMP_DROP, + api_descriptor=FlagRegisterInputDescriptor(27, 4), + icon="mdi:alert-circle", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_ERROR_NO_VALID_TEMP_SENSOR, + api_descriptor=FlagRegisterInputDescriptor(27, 5), + icon="mdi:alert-circle", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_ERROR_CANNOT_REACH_TEMP, + api_descriptor=FlagRegisterInputDescriptor(27, 6), + icon="mdi:alert-circle", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + AskoheatBinarySensorEntityDescription( + key=BinarySensorAttrKey.DATA_LEGIO_STATUS_ERROR_SETTINGS, + api_descriptor=FlagRegisterInputDescriptor(27, 7), + icon="mdi:alert-circle", + entity_category=EntityCategory.DIAGNOSTIC, + device_key=DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT, + ), + ], +) diff --git a/custom_components/askoheat/api_par_desc.py b/custom_components/askoheat/api_par_desc.py index 6dc406a..4924f16 100644 --- a/custom_components/askoheat/api_par_desc.py +++ b/custom_components/askoheat/api_par_desc.py @@ -16,7 +16,7 @@ FlagRegisterInputDescriptor, RegisterBlockDescriptor, StringRegisterInputDescriptor, - UnsignedIntRegisterInputDescriptor, + UnsignedInt16RegisterInputDescriptor, ) from custom_components.askoheat.const import ( BinarySensorAttrKey, @@ -40,7 +40,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_HEATER1_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(17), + api_descriptor=UnsignedInt16RegisterInputDescriptor(17), native_min_value=250, native_max_value=10000, state_class=SensorStateClass.MEASUREMENT, @@ -51,7 +51,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_HEATER2_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(18), + api_descriptor=UnsignedInt16RegisterInputDescriptor(18), native_min_value=250, native_max_value=10000, state_class=SensorStateClass.MEASUREMENT, @@ -62,7 +62,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_HEATER3_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(19), + api_descriptor=UnsignedInt16RegisterInputDescriptor(19), native_min_value=250, native_max_value=10000, state_class=SensorStateClass.MEASUREMENT, @@ -97,7 +97,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_HEATER4_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(50), + api_descriptor=UnsignedInt16RegisterInputDescriptor(50), native_min_value=250, native_max_value=10000, state_class=SensorStateClass.MEASUREMENT, @@ -109,7 +109,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_HEATER5_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(51), + api_descriptor=UnsignedInt16RegisterInputDescriptor(51), native_min_value=250, native_max_value=10000, state_class=SensorStateClass.MEASUREMENT, @@ -121,7 +121,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_HEATER6_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(52), + api_descriptor=UnsignedInt16RegisterInputDescriptor(52), native_min_value=250, native_max_value=10000, state_class=SensorStateClass.MEASUREMENT, @@ -133,7 +133,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_NUMBER_OF_STEPS, - api_descriptor=UnsignedIntRegisterInputDescriptor(53), + api_descriptor=UnsignedInt16RegisterInputDescriptor(53), native_min_value=6, native_max_value=19, device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, @@ -141,7 +141,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_NUMBER_OF_HEATER, - api_descriptor=UnsignedIntRegisterInputDescriptor(54), + api_descriptor=UnsignedInt16RegisterInputDescriptor(54), native_min_value=3, native_max_value=6, device_key=DeviceKey.WATER_HEATER_CONTROL_UNIT, @@ -149,7 +149,7 @@ ), AskoheatSensorEntityDescription( key=SensorAttrKey.PAR_MAX_POWER, - api_descriptor=UnsignedIntRegisterInputDescriptor(55), + api_descriptor=UnsignedInt16RegisterInputDescriptor(55), native_min_value=1750, native_max_value=20000, state_class=SensorStateClass.MEASUREMENT, diff --git a/custom_components/askoheat/binary_sensor.py b/custom_components/askoheat/binary_sensor.py index 6187f33..2571a9b 100644 --- a/custom_components/askoheat/binary_sensor.py +++ b/custom_components/askoheat/binary_sensor.py @@ -12,6 +12,7 @@ from custom_components.askoheat.api_conf_desc import CONF_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.api_ema_desc import EMA_REGISTER_BLOCK_DESCRIPTOR +from custom_components.askoheat.api_op_desc import DATA_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.api_par_desc import PARAMETER_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.model import AskoheatBinarySensorEntityDescription @@ -50,6 +51,10 @@ async def async_setup_entry( entity_description: entry.runtime_data.config_coordinator for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.binary_sensors }, + **{ + entity_description: entry.runtime_data.data_coordinator + for entity_description in DATA_REGISTER_BLOCK_DESCRIPTOR.binary_sensors + }, }.items() ) diff --git a/custom_components/askoheat/const.py b/custom_components/askoheat/const.py index f493e29..0a85dfe 100644 --- a/custom_components/askoheat/const.py +++ b/custom_components/askoheat/const.py @@ -16,7 +16,7 @@ # per coordinator scan intervals SCAN_INTERVAL_EMA = timedelta(seconds=5) SCAN_INTERVAL_CONFIG = timedelta(hours=1) -SCAN_INTERVAL_DATA = timedelta(minutes=1) +SCAN_INTERVAL_OP_DATA = timedelta(minutes=1) CONF_DEVICE_UNIQUE_ID = "device_unique_id" @@ -104,9 +104,9 @@ class NumberAttrKey(StrEnum): CON_ANALOG_INPUT_7_THRESHOLD_TEMPERATURE = "analog_input_7_threshold_temperature" # 0-7 - CON_HEAT_PUMP_REQUEST_OFF_STEP = "heat_pump_request_off_step" + CON_HEATPUMP_REQUEST_OFF_STEP = "heatpump_request_off_step" # 0-7 - CON_HEAT_PUMP_REQUEST_ON_STEP = "heat_pump_request_on_step" + CON_HEATPUMP_REQUEST_ON_STEP = "heatpump_request_on_step" # 0-7 CON_EMERGENCY_MODE_ON_STEP = "emergency_mode_on_step" @@ -177,7 +177,7 @@ class SwitchAttrKey(StrEnum): CON_RESTART_IF_ENERGYMANAGER_CONNECTION_LOST = "restart_if_em_connection_lost" CON_AUTO_OFF_MODBUS_ENABLED = "auto_off_modbus_enabled" CON_AUTO_OFF_ANALOG_INPUT_ENABLED = "auto_off_analog_input_enabled" - CON_AUTO_OFF_HEAT_PUMP_REQUEST_ENABLED = "auto_off_heatpump_request_enabled" + CON_AUTO_OFF_HEATPUMP_REQUEST_ENABLED = "auto_off_heatpump_request_enabled" CON_AUTO_OFF_EMERGENCY_MODE_ENABLED = "auto_off_emergency_mode_enabled" # from auto heater off settings register -- end @@ -190,7 +190,7 @@ class SwitchAttrKey(StrEnum): CON_HEATBUFFER_TYPE_PELLET_FIRING = "heatbuffer_type_pellet_firing" CON_HEATBUFFER_TYPE_GAS_BURNER = "heatbuffer_type_gas_burner" CON_HEATBUFFER_TYPE_OIL_BURNER = "heatbuffer_type_oil_burner" - CON_HEATBUFFER_TYPE_HEAT_PUMP = "heatbuffer_type_heat_pump" + CON_HEATBUFFER_TYPE_HEATPUMP = "heatbuffer_type_heatpump" CON_HEATBUFFER_TYPE_OTHER = "heatbuffer_type_other" # from heatbuffer type settings register -- end @@ -268,7 +268,7 @@ class BinarySensorAttrKey(StrEnum): PUMP_ACTIVE = "status.pump" RELAY_BOARD_CONNECTED = "status.relay_board_connected" EMERGENCY_MODE_ACTIVE = "status.emergency_mode" - HEAT_PUMP_REQUEST_ACTIVE = "status.heat_pump_request" + HEATPUMP_REQUEST_ACTIVE = "status.heatpump_request" LEGIONELLA_PROTECTION_ACTIVE = "status.legionella_protection" ANALOG_INPUT_ACTIVE = "status.analog_input" SETPOINT_ACTIVE = "status.setpoint" @@ -292,6 +292,23 @@ class BinarySensorAttrKey(StrEnum): PAR_TYPE_OEM_VERSION = "oem_version" # from type register -- end + # ----------------------------------------------- + # DATA block binary sensors + # ----------------------------------------------- + # from legio status register -- begin + DATA_LEGIO_STATUS_HEATING_UP = "legio_status_heating_up" + DATA_LEGIO_STATUS_TEMPERATURE_REACHED = "legio_status_temp_reached" + DATA_LEGIO_STATUS_TEMP_REACHED_OUTSIDE_INTERVAL = ( + "legio_status_temp_reached_outside_interval" + ) + DATA_LEGIO_STATUS_UNEXPECTED_TEMP_DROP = "legio_status_unexpected_temp_drop" + DATA_LEGIO_STATUS_ERROR_NO_VALID_TEMP_SENSOR = ( + "legio_status_error_no_valid_temp_sensor" + ) + DATA_LEGIO_STATUS_ERROR_CANNOT_REACH_TEMP = "legio_status_error_cannot_reach_temp" + DATA_LEGIO_STATUS_ERROR_SETTINGS = "legio_status_error_settings" + # from legio status register -- end + class SensorAttrKey(StrEnum): """Askoheat sensor attribute keys.""" @@ -324,6 +341,53 @@ class SensorAttrKey(StrEnum): PAR_NUMBER_OF_HEATER = "number_of_heater" PAR_MAX_POWER = "max_power" + # ----------------------------------------------- + # DATA block enums + # ----------------------------------------------- + DATA_OPERATING_TIME_MINUTES = "operating_time" + DATA_OPERATING_TIME_HEATER1_MINUTES = "operating_time_heater1" + DATA_OPERATING_TIME_HEATER2_MINUTES = "operating_time_heater2" + DATA_OPERATING_TIME_HEATER3_MINUTES = "operating_time_heater3" + DATA_OPERATING_TIME_PUMP_MINUTES = "operating_time_pump" + DATA_OPERATING_TIME_VALVE_MINUTES = "operating_time_valve" + DATA_SWITCH_COUNT_RELAY1 = "switch_count_relay1" + DATA_SWITCH_COUNT_RELAY2 = "switch_count_relay2" + DATA_SWITCH_COUNT_RELAY3 = "switch_count_relay3" + DATA_SWITCH_COUNT_RELAY4 = "switch_count_relay4" + DATA_SINCE_LAST_LEGIO_ACTIVATION_MINUTES = "legio_since_legio_activation" + DATA_LEGIO_PLATEAU_TIMER = "legio_plateau_timer" + DATA_ANALOG_INPUT_STEP = "analog_input_step" + DATA_ACTUAL_TEMP_LIMIT = "actual_temp_limit" + DATA_AUTO_HEATER_OFF_COUNTDOWN_MINUTES = "auto_heater_off_countdown" + DATA_EMERGENCY_OFF_COUNTDOWN_MINUTES = "emergency_off_countdown" + DATA_BOOT_COUNT = "boot_count" + DATA_OPERATING_TIME_SET_HEATER_STEP = "operating_time_set_heater_step" + DATA_OPERATING_TIME_LOAD_SETPOINT = "operating_time_load_setpoint" + DATA_OPERATING_TIME_LOAD_FEEDIN = "operating_time_load_feedin" + DATA_OPERATING_TIME_HEATPUMP_REQUEST = "operating_time_heatpump_request" + DATA_OPERATING_TIME_ANALOG_INPUT = "operating_time_analog_input" + DATA_OPERATING_TIME_EMERGENCY_MODE = "operating_time_emergency_mode" + DATA_OPERATING_TIME_LEGIO_PROTECTION = "operating_time_legio_protection" + DATA_OPERATING_TIME_LOW_TARIFF = "operating_time_low_tariff" + DATA_OPERATING_TIME_MINIMAL_TEMP = "operating_time_minimal_temp" + DATA_OPERATING_TIME_HEATER_STEP1 = "operating_time_heater_step1" + DATA_OPERATING_TIME_HEATER_STEP2 = "operating_time_heater_step2" + DATA_OPERATING_TIME_HEATER_STEP3 = "operating_time_heater_step3" + DATA_OPERATING_TIME_HEATER_STEP4 = "operating_time_heater_step4" + DATA_OPERATING_TIME_HEATER_STEP5 = "operating_time_heater_step5" + DATA_OPERATING_TIME_HEATER_STEP6 = "operating_time_heater_step6" + DATA_OPERATING_TIME_HEATER_STEP7 = "operating_time_heater_step7" + DATA_COUNT_SET_HEATER_STEP = "count_set_heater_step" + DATA_COUNT_LOAD_SETPOINT = "count_load_setpoint" + DATA_COUNT_LOAD_FEEDIN = "count_load_feedin" + DATA_COUNT_HEATPUMP_REQUEST = "count_heatpump_request" + DATA_COUNT_ANALOG_INPUT = "count_analog_input" + DATA_COUNT_EMERGENCY_MODE = "count_emergency_mode" + DATA_COUNT_LEGIO_PROTECTION = "count_legio_protection" + DATA_COUNT_LOW_TARIFF = "count_low_tariff" + DATA_COUNT_MINIMAL_TEMP = "count_minimal_temp" + DATA_MAX_MEASURED_TEMP = "max_measured_temp" + class Baudrate(StrEnum): """Available Baudrates.""" @@ -373,7 +437,7 @@ class DeviceKey(StrEnum): """Device keys.""" MODBUS_MASTER = "modbus_master" - HEAT_PUMP_CONTROL_UNIT = "heat_pump_control_unit" + HEATPUMP_CONTROL_UNIT = "heatpump_control_unit" ANALOG_INPUT_CONTROL_UNIT = "analog_input_control_unit" ENERGY_MANAGER = "energy_manager" WATER_HEATER_CONTROL_UNIT = "water_heater_control_unit" diff --git a/custom_components/askoheat/coordinator.py b/custom_components/askoheat/coordinator.py index 3a7cebb..87b9ad7 100644 --- a/custom_components/askoheat/coordinator.py +++ b/custom_components/askoheat/coordinator.py @@ -12,7 +12,13 @@ AskoHeatModbusApiClient, AskoheatModbusApiClientError, ) -from .const import DOMAIN, LOGGER, SCAN_INTERVAL_CONFIG, SCAN_INTERVAL_EMA +from .const import ( + DOMAIN, + LOGGER, + SCAN_INTERVAL_CONFIG, + SCAN_INTERVAL_OP_DATA, + SCAN_INTERVAL_EMA, +) if TYPE_CHECKING: from datetime import timedelta @@ -153,6 +159,27 @@ async def async_write(self, _: RegisterInputDescriptor, __: object) -> None: raise UpdateFailed("Writing values to parameters not allowed") +class AskoheatOperationDataUpdateCoordinator(AskoheatDataUpdateCoordinator): + """Class to manage fetching askoheat operation data states.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize.""" + super().__init__(hass=hass, scan_interval=SCAN_INTERVAL_OP_DATA) + + async def _async_update_data(self) -> dict[str, Any]: + """Update config data via library.""" + try: + async with async_timeout.timeout(10): + data = await self.config_entry.runtime_data.client.async_read_op_data() + return _map_data_block_to_dict(data) + except AskoheatModbusApiClientError as exception: + raise UpdateFailed(exception) from exception + + async def async_write(self, _: RegisterInputDescriptor, __: object) -> None: + """Write parameter data block of Askoheat.""" + raise UpdateFailed("Writing values to data block not allowed") + + def _map_data_block_to_dict(data: AskoheatDataBlock) -> dict[str, Any]: """Map askoheat data block to dict.""" result: dict[str, Any] = {} diff --git a/custom_components/askoheat/data.py b/custom_components/askoheat/data.py index c121a03..ef0589f 100644 --- a/custom_components/askoheat/data.py +++ b/custom_components/askoheat/data.py @@ -25,6 +25,7 @@ from custom_components.askoheat.coordinator import ( AskoheatConfigDataUpdateCoordinator, AskoheatEMADataUpdateCoordinator, + AskoheatOperationDataUpdateCoordinator, AskoheatParameterDataUpdateCoordinator, ) @@ -42,6 +43,7 @@ class AskoheatData: ema_coordinator: AskoheatEMADataUpdateCoordinator config_coordinator: AskoheatConfigDataUpdateCoordinator par_coordinator: AskoheatParameterDataUpdateCoordinator + data_coordinator: AskoheatOperationDataUpdateCoordinator integration: Integration diff --git a/custom_components/askoheat/model.py b/custom_components/askoheat/model.py index 1310fff..6d9b29d 100644 --- a/custom_components/askoheat/model.py +++ b/custom_components/askoheat/model.py @@ -23,11 +23,13 @@ Float32RegisterInputDescriptor, IntEnumInputDescriptor, RegisterInputDescriptor, - SignedIntRegisterInputDescriptor, + SignedInt16RegisterInputDescriptor, StrEnumInputDescriptor, StringRegisterInputDescriptor, + StructRegisterInputDescriptor, TimeRegisterInputDescriptor, - UnsignedIntRegisterInputDescriptor, + UnsignedInt16RegisterInputDescriptor, + UnsignedInt32RegisterInputDescriptor, ) from custom_components.askoheat.const import ( DOMAIN, @@ -109,10 +111,12 @@ class AskoheatSensorEntityDescription( AskoheatEntityDescription[ SensorAttrKey, ByteRegisterInputDescriptor - | UnsignedIntRegisterInputDescriptor - | SignedIntRegisterInputDescriptor + | UnsignedInt16RegisterInputDescriptor + | UnsignedInt32RegisterInputDescriptor + | SignedInt16RegisterInputDescriptor | Float32RegisterInputDescriptor - | StringRegisterInputDescriptor, + | StringRegisterInputDescriptor + | StructRegisterInputDescriptor, ], SensorEntityDescription, ): @@ -133,13 +137,18 @@ def data_key(self) -> str: return f"sensor.{self.key}" +@dataclass(frozen=True) +class AskoheatDurationSensorEntityDescription(AskoheatSensorEntityDescription): + """Class describing an askoheat specific duration sensor entity.""" + + @dataclass(frozen=True) class AskoheatNumberEntityDescription( AskoheatEntityDescription[ NumberAttrKey, ByteRegisterInputDescriptor - | UnsignedIntRegisterInputDescriptor - | SignedIntRegisterInputDescriptor + | UnsignedInt16RegisterInputDescriptor + | SignedInt16RegisterInputDescriptor | Float32RegisterInputDescriptor, ], NumberEntityDescription, diff --git a/custom_components/askoheat/sensor.py b/custom_components/askoheat/sensor.py index 7e6af38..2df0af9 100644 --- a/custom_components/askoheat/sensor.py +++ b/custom_components/askoheat/sensor.py @@ -2,27 +2,56 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from datetime import date, datetime +from decimal import Decimal +from typing import TYPE_CHECKING, Any import numpy as np from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity +from homeassistant.const import UnitOfTime from homeassistant.core import callback from custom_components.askoheat.api_conf_desc import CONF_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.api_ema_desc import EMA_REGISTER_BLOCK_DESCRIPTOR +from custom_components.askoheat.api_op_desc import DATA_REGISTER_BLOCK_DESCRIPTOR from custom_components.askoheat.api_par_desc import PARAMETER_REGISTER_BLOCK_DESCRIPTOR -from custom_components.askoheat.model import AskoheatSensorEntityDescription +from custom_components.askoheat.model import ( + AskoheatDurationSensorEntityDescription, + AskoheatSensorEntityDescription, +) from .entity import AskoheatEntity +from custom_components.askoheat import entity if TYPE_CHECKING: from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import StateType from .coordinator import AskoheatDataUpdateCoordinator from .data import AskoheatConfigEntry +def _instanciate( + entry: AskoheatConfigEntry, + coordinator: AskoheatDataUpdateCoordinator, + entity_description: AskoheatSensorEntityDescription, +) -> AskoheatSensor: + match entity_description: + case AskoheatDurationSensorEntityDescription(): + return AskoheatDurationSensor( + entry=entry, + coordinator=coordinator, + entity_description=entity_description, + ) + case _: + return AskoheatSensor( + entry=entry, + coordinator=coordinator, + entity_description=entity_description, + ) + + async def async_setup_entry( hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` entry: AskoheatConfigEntry, @@ -30,7 +59,7 @@ async def async_setup_entry( ) -> None: """Set up the sensor platform.""" async_add_entities( - AskoheatSensor( + _instanciate( entry=entry, coordinator=coordinator, entity_description=entity_description, @@ -48,6 +77,10 @@ async def async_setup_entry( entity_description: entry.runtime_data.config_coordinator for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.sensors }, + **{ + entity_description: entry.runtime_data.data_coordinator + for entity_description in DATA_REGISTER_BLOCK_DESCRIPTOR.sensors + }, }.items() ) @@ -77,24 +110,58 @@ def _handle_coordinator_update(self) -> None: if data is None: return - self._attr_native_value = data[self.entity_description.data_key] - - if self._attr_native_value is None: - pass - - elif isinstance( - self._attr_native_value, float | int | np.floating | np.integer - ) and ( - self.entity_description.factor is not None - or self.entity_description.native_precision is not None - ): - float_value = float(self._attr_native_value) - if self.entity_description.factor is not None: - float_value *= self.entity_description.factor - if self.entity_description.native_precision is not None: - float_value = round( - float_value, self.entity_description.native_precision - ) - self._attr_native_value = float_value + raw_value = data[self.entity_description.data_key] + + if raw_value is None: + self._attr_native_value = None + + else: + converted_value = self._convert_value(raw_value) + + if isinstance(converted_value, float | int | np.floating | np.integer) and ( + self.entity_description.factor is not None + or self.entity_description.native_precision is not None + ): + float_value = float(converted_value) + if self.entity_description.factor is not None: + float_value *= self.entity_description.factor + if self.entity_description.native_precision is not None: + float_value = round( + float_value, self.entity_description.native_precision + ) + self._attr_native_value = float_value + else: + self._attr_native_value = converted_value super()._handle_coordinator_update() + + def _convert_value(self, value: Any) -> StateType | date | datetime | Decimal: + return value + + +class AskoheatDurationSensor(AskoheatSensor): + """askoheat Sensor class representing a duration.""" + + def __init__( + self, + entry: AskoheatConfigEntry, + coordinator: AskoheatDataUpdateCoordinator, + entity_description: AskoheatSensorEntityDescription, + ) -> None: + """Initialize the duration sensor class.""" + super().__init__(entry, coordinator, entity_description) + + def _convert_value(self, value: Any) -> StateType | date | datetime | Decimal: + time_as_int = int(value) + minutes = int(time_as_int / 2**0) & 0xFF + hours = int(time_as_int / 2**8) & 0xFF + days = int(time_as_int / 2**16) & 0xFFFF + + match self.entity_description.native_unit_of_measurement: + case UnitOfTime.DAYS: + return (minutes / (60 * 24)) + (hours / 24) + days + case UnitOfTime.HOURS: + return (minutes / 60) + hours + (days * 24) + case _: + # by default convert to minutes + return minutes + (hours * 60) + (days * 24 * 60)