diff --git a/custom_components/askoheat/api.py b/custom_components/askoheat/api.py index 66bfcdd..a9c44b8 100644 --- a/custom_components/askoheat/api.py +++ b/custom_components/askoheat/api.py @@ -2,6 +2,9 @@ from __future__ import annotations +from ast import Num +from datetime import datetime, time +from numbers import Number from typing import TYPE_CHECKING, Any import numpy as np @@ -9,11 +12,17 @@ from custom_components.askoheat.const import ( LOGGER, - BinarySensorEMAAttrKey, - SensorEMAAttrKey, - SwitchEMAAttrKey, + Baurate, + BinarySensorAttrKey, + EnergyMeterType, + NumberAttrKey, + SelectAttrKey, + SensorAttrKey, + SwitchAttrKey, + TextAttrKey, + TimeAttrKey, ) -from custom_components.askoheat.data import AskoheatEMAData +from custom_components.askoheat.data import AskoheatDataBlock if TYPE_CHECKING: from pymodbus.pdu import ModbusPDU @@ -46,11 +55,18 @@ def close(self) -> None: """Close comnection to modbus client.""" self._client.close() - async def async_read_ema_data(self) -> AskoheatEMAData: + async def async_read_ema_data(self) -> AskoheatDataBlock: """Read EMA states.""" # http://www.download.askoma.com/askofamily_plus/modbus/askoheat-modbus.html#EM_Block data = await self.async_read_input_registers_data(300, 37) - LOGGER.info("async_read_ema_data %s", data) + LOGGER.debug("async_read_ema_data %s", data) + return self._map_ema_data(data) + + 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) + LOGGER.debug("async_read_config_data %s", data) return self._map_ema_data(data) async def async_read_input_registers_data( @@ -73,80 +89,435 @@ async def async_read_holding_registers_data( return await self._client.read_holding_registers(address=address, count=count) - def _map_ema_data(self, data: ModbusPDU) -> AskoheatEMAData: - """Map modbus result to EMA data structure.""" - return AskoheatEMAData( + def _map_ema_data(self, data: ModbusPDU) -> AskoheatDataBlock: + """Map modbus result of ema data block.""" + return AskoheatDataBlock( binary_sensors=self._map_register_to_status(data.registers[16]), sensors={ - SensorEMAAttrKey.HEATER_LOAD: _read_uint16(data.registers[17]), - SensorEMAAttrKey.LOAD_SETPOINT_VALUE: _read_int16(data.registers[19]), - SensorEMAAttrKey.LOAD_FEEDIN_VALUE: _read_int16(data.registers[20]), - SensorEMAAttrKey.ANALOG_INPUT_VALUE: _read_float32( - data.registers[23:25] - ), - SensorEMAAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE: _read_float32( + SensorAttrKey.HEATER_LOAD: _read_uint16(data.registers[17]), + SensorAttrKey.LOAD_SETPOINT_VALUE: _read_int16(data.registers[19]), + SensorAttrKey.LOAD_FEEDIN_VALUE: _read_int16(data.registers[20]), + SensorAttrKey.ANALOG_INPUT_VALUE: _read_float32(data.registers[23:25]), + SensorAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE: _read_float32( data.registers[25:27] ), - SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE: _read_float32( + SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE: _read_float32( data.registers[27:29] ), - SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE: _read_float32( + SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE: _read_float32( data.registers[29:31] ), - SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE: _read_float32( + SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE: _read_float32( data.registers[31:33] ), - SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE: _read_float32( + SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE: _read_float32( data.registers[33:35] ), }, switches=self._map_register_to_heater_step(data.registers[18]), ) + def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock: + """Map modbus result of config data block.""" + return AskoheatDataBlock( + number_inputs={ + NumberAttrKey.CON_RELAY_SEC_COUNT_SECONDS: _read_uint16( + data.registers[0] + ), + NumberAttrKey.CON_PUMP_SEC_COUNT_SECONDS: _read_uint16( + data.registers[1] + ), + NumberAttrKey.CON_AUTO_HEATER_OFF_MINUTES: _read_uint16( + data.registers[3] + ), + NumberAttrKey.CON_CASCADE_PRIORIZATION: _read_byte(data.registers[5]), + NumberAttrKey.CON_HEATBUFFER_VOLUME: _read_uint16(data.registers[7]), + NumberAttrKey.CON_LEGIO_PROTECTION_TEMPERATURE: _read_byte( + data.registers[10] + ), + NumberAttrKey.CON_LEGIO_PROTECTION_HEATUP_MINUTES: _read_uint16( + data.registers[11] + ), + NumberAttrKey.CON_NUMBER_OF_HOUSEHOLD_MEMBERS: _read_byte( + data.registers[21] + ), + NumberAttrKey.CON_LOAD_FEEDIN_DELAY_SECONDS: _read_uint16( + data.registers[39] + ), + NumberAttrKey.CON_LOAD_FEEDIN_BASIC_ENERGY_LEVEL: _read_uint16( + data.registers[40] + ), + NumberAttrKey.CON_TIMEZONE_OFFSET: _read_int16(data.registers[41]), + NumberAttrKey.CON_RTU_SLAVE_ID: _read_byte(data.registers[50]), + NumberAttrKey.CON_ANALOG_INPUT_HYSTERESIS: _read_float32( + data.registers[56:58] + ), + # Analog 0 + 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_TEMPERATURE: _read_byte( + data.registers[61] + ), + # Analog 1 + 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_TEMPERATURE: _read_byte( + data.registers[65] + ), + # Analog 2 + 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_TEMPERATURE: _read_byte( + data.registers[69] + ), + # Analog 3 + 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_TEMPERATURE: _read_byte( + data.registers[73] + ), + # Analog 4 + 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_TEMPERATURE: _read_byte( + data.registers[77] + ), + # Analog 5 + 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_TEMPERATURE: _read_byte( + data.registers[81] + ), + # Analog 6 + 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_TEMPERATURE: _read_byte( + data.registers[65] + ), + # Analog 7 + 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_TEMPERATURE: _read_byte( + data.registers[89] + ), + NumberAttrKey.CON_HEAT_PUMP_REQUEST_OFF_STEP: _read_byte( + data.registers[90] + ), + NumberAttrKey.CON_HEAT_PUMP_REQUEST_ON_STEP: _read_byte( + data.registers[91] + ), + NumberAttrKey.CON_EMERGENCY_MODE_ON_STOP: _read_byte( + data.registers[92] + ), + NumberAttrKey.CON_TEMPERATURE_HYSTERESIS: _read_byte( + data.registers[93] + ), + NumberAttrKey.CON_MINIMAL_TEMPERATURE: _read_byte(data.registers[95]), + NumberAttrKey.CON_SET_HEATER_STEP_TEMPERATURE_LIMIT: _read_byte( + data.registers[96] + ), + NumberAttrKey.CON_LOAD_FEEDIN_OR_SETPOINT_TEMPERATURE_LIMIT: _read_byte( + data.registers[97] + ), + NumberAttrKey.CON_LOW_TARIFF_TEMPERATURE_LIMIT: _read_byte( + data.registers[98] + ), + NumberAttrKey.CON_HEATPUMP_REQUEST_TEMPERATURE_LIMIT: _read_byte( + data.registers[99] + ), + }, + switches={ + # input settings register + # low byte + SwitchAttrKey.CON_MISSING_CURRENT_FLOW_TRIGGERS_ERROR: _read_flag( + data.registers[2], 0 + ), + SwitchAttrKey.CON_HEATER_LOAD_VALUE_ONLY_IF_CURRENT_FLOWS: _read_flag( + data.registers[2], 1 + ), + SwitchAttrKey.CON_LOAD_FEEDIN_VALUE_ENABLED: _read_flag( + data.registers[2], 2 + ), + SwitchAttrKey.CON_LOAD_SETPOINT_VALUE_ENABLED: _read_flag( + data.registers[2], 3 + ), + SwitchAttrKey.CON_SET_HEATER_STEP_VALUE_ENABLED: _read_flag( + data.registers[2], 4 + ), + SwitchAttrKey.CON_SET_ANALOG_INPUT_ENABLED: _read_flag( + data.registers[2], 5 + ), + SwitchAttrKey.CON_HEATPUMP_REQUEST_INPUT_ENABLED: _read_flag( + data.registers[2], 6 + ), + SwitchAttrKey.CON_EMERGENCY_MODE_ENABLED: _read_flag( + data.registers[2], 7 + ), + # high byte + SwitchAttrKey.CON_HOLD_MINIMAL_TEMPERATURE_ENABELD: _read_flag( + data.registers[2], 8 + ), + SwitchAttrKey.CON_HOLD_MINIMAL_TEMPERATURE_ENABELD: _read_flag( + data.registers[2], 9 + ), + SwitchAttrKey.CON_SOFTWARE_CONTROL_SMA_SEMP_ENABLED: _read_flag( + data.registers[2], 10 + ), + SwitchAttrKey.CON_SOFTWARE_CONTROL_SENEC_HOME_ENABLED: _read_flag( + data.registers[2], 11 + ), + # auto heater off settings register + # low byte + SwitchAttrKey.CON_AUTO_OFF_ENABLED: _read_flag(data.registers[4], 0), + SwitchAttrKey.CON_RESTART_IF_ENERGYMANAGER_CONNECTION_LOST: _read_flag( + data.registers[4], 1 + ), + SwitchAttrKey.CON_AUTO_OFF_MODBUS_ENABLED: _read_flag( + data.registers[4], 4 + ), + SwitchAttrKey.CON_AUTO_OFF_ANALOG_INPUT_ENABLED: _read_flag( + data.registers[4], 5 + ), + SwitchAttrKey.CON_AUTO_OFF_HEAT_PUMP_REQUEST_ENABLED: _read_flag( + data.registers[4], 6 + ), + SwitchAttrKey.CON_AUTO_OFF_EMERGENCY_MODE_ENABLED: _read_flag( + data.registers[4], 7 + ), + # heatbuffer type register + SwitchAttrKey.CON_HEATBUFFER_TYPE_TAP_WATER: _read_flag( + data.registers[6], 0 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_HEATING_WATER: _read_flag( + data.registers[6], 1 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_COMBINED_HEAT_AND_POWER_UNIT: _read_flag( + data.registers[6], 2 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_PELLET_FIRING: _read_flag( + data.registers[6], 3 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_GAS_BURNER: _read_flag( + data.registers[6], 4 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_OIL_BURNER: _read_flag( + data.registers[6], 5 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_HEAT_PUMP: _read_flag( + data.registers[6], 6 + ), + SwitchAttrKey.CON_HEATBUFFER_TYPE_OTHER: _read_flag( + data.registers[6], 7 + ), + # heater position register + SwitchAttrKey.CON_HEATER_POSITION_BOTTOM: _read_flag( + data.registers[8], 0 + ), + SwitchAttrKey.CON_HEATER_POSITION_MIDDLE: _read_flag( + data.registers[8], 1 + ), + SwitchAttrKey.CON_HEATER_POSITION_ASKOWALL: _read_flag( + data.registers[8], 7 + ), + # legio settings register + # low byte + SwitchAttrKey.CON_LEGIO_SETTINGS_USE_INTERNAL_TEMP_SENSOR: _read_flag( + data.registers[9], 0 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR1: _read_flag( + data.registers[9], 1 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR2: _read_flag( + data.registers[9], 2 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR3: _read_flag( + data.registers[9], 3 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR4: _read_flag( + data.registers[9], 4 + ), + # high byte + SwitchAttrKey.CON_LEGIO_SETTINGS_INTERVAL_DAILY: _read_flag( + data.registers[9], 8 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_INTERVAL_WEEKLY: _read_flag( + data.registers[9], 9 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_INTERVAL_FORTNIGHTLY: _read_flag( + data.registers[9], 10 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_INTERVAL_MONTHLY: _read_flag( + data.registers[9], 11 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_PREFER_FEEDIN_ENERGY: _read_flag( + data.registers[9], 12 + ), + SwitchAttrKey.CON_LEGIO_SETTINGS_PROTECTION_ENABLED: _read_flag( + data.registers[9], 13 + ), + # house type settings register + # low byte + SwitchAttrKey.CON_HOUSE_TYPE_SINGLE_FAMILY_HOUSE: _read_flag( + data.registers[20], 0 + ), + SwitchAttrKey.CON_HOUSE_TYPE_TWO_FAMILY_HOUSE: _read_flag( + data.registers[20], 1 + ), + SwitchAttrKey.CON_HOUSE_TYPE_APPARTMENT_BUILDING: _read_flag( + data.registers[20], 2 + ), + SwitchAttrKey.CON_HOUSE_TYPE_COMMERCIAL_BUILDING: _read_flag( + data.registers[20], 7 + ), + # Summer time int 1/0 as bool + SwitchAttrKey.CON_SUMMERTIME_ENABLED: _read_int16(data.registers[42]) + == 1, + # rtu settings register + # low byte + SwitchAttrKey.CON_RTU_SEND_TWO_STOP_BITS: _read_flag( + data.registers[49], 0 + ), + SwitchAttrKey.CON_RTU_SEND_PARITY_EVEN: _read_flag( + data.registers[49], 1 + ), + SwitchAttrKey.CON_RTU_SEND_PARITY_ODD: _read_flag( + data.registers[49], 2 + ), + SwitchAttrKey.CON_RTU_SLAVE_MODE_ACTIVE: _read_flag( + data.registers[49], 7 + ), + # high byte + SwitchAttrKey.CON_RTU_MASTER_MODE_ACTIVE: _read_flag( + data.registers[49], 15 + ), + # temperature settings register + # low byte + SwitchAttrKey.CON_USE_INTERNAL_TEMP_SENSOR: _read_flag( + data.registers[94], 0 + ), + SwitchAttrKey.CON_USE_EXTERNAL_TEMP_SENSOR1: _read_flag( + data.registers[94], 1 + ), + SwitchAttrKey.CON_USE_EXTERNAL_TEMP_SENSOR2: _read_flag( + data.registers[94], 2 + ), + SwitchAttrKey.CON_USE_EXTERNAL_TEMP_SENSOR3: _read_flag( + data.registers[94], 3 + ), + SwitchAttrKey.CON_USE_EXTERNAL_TEMP_SENSOR4: _read_flag( + data.registers[94], 4 + ), + }, + time_inputs={ + TimeAttrKey.CON_LEGIO_PROTECTION_PREFERRED_START_TIME: _read_time( + data.registers[12:16] + ), + TimeAttrKey.CON_LOW_TARIFF_START_TIME: time( + hour=_read_byte(data.registers[52]), + minute=_read_byte(data.registers[53]), + ), + TimeAttrKey.CON_LOW_TARIFF_END_TIME: time( + hour=_read_byte(data.registers[54]), + minute=_read_byte(data.registers[55]), + ), + }, + 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_ENERGY_METER_TYPE: EnergyMeterType( + _read_byte(data.registers[51]) + ), + }, + ) + def _map_register_to_status( self, register_value: int - ) -> dict[BinarySensorEMAAttrKey, bool]: + ) -> dict[BinarySensorAttrKey, bool]: """Map modbus register status.""" return { # low byte values - BinarySensorEMAAttrKey.HEATER1_ACTIVE: _read_flag(register_value, 0), - BinarySensorEMAAttrKey.HEATER2_ACTIVE: _read_flag(register_value, 1), - BinarySensorEMAAttrKey.HEATER3_ACTIVE: _read_flag(register_value, 2), - BinarySensorEMAAttrKey.PUMP_ACTIVE: _read_flag(register_value, 3), - BinarySensorEMAAttrKey.RELAY_BOARD_CONNECTED: _read_flag(register_value, 4), + BinarySensorAttrKey.HEATER1_ACTIVE: _read_flag(register_value, 0), + BinarySensorAttrKey.HEATER2_ACTIVE: _read_flag(register_value, 1), + BinarySensorAttrKey.HEATER3_ACTIVE: _read_flag(register_value, 2), + BinarySensorAttrKey.PUMP_ACTIVE: _read_flag(register_value, 3), + BinarySensorAttrKey.RELAY_BOARD_CONNECTED: _read_flag(register_value, 4), # bit 5 ignored - BinarySensorEMAAttrKey.HEAT_PUMP_REQUEST_ACTIVE: _read_flag( - register_value, 6 - ), - BinarySensorEMAAttrKey.EMERGENCY_MODE_ACTIVE: _read_flag(register_value, 7), + BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE: _read_flag(register_value, 6), + BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE: _read_flag(register_value, 7), # high byte values - BinarySensorEMAAttrKey.LEGIONELLA_PROTECTION_ACTIVE: _read_flag( + BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE: _read_flag( register_value, 8 ), - BinarySensorEMAAttrKey.ANALOG_INPUT_ACTIVE: _read_flag(register_value, 9), - BinarySensorEMAAttrKey.SETPOINT_ACTIVE: _read_flag(register_value, 10), - BinarySensorEMAAttrKey.LOAD_FEEDIN_ACTIVE: _read_flag(register_value, 11), - BinarySensorEMAAttrKey.AUTOHEATER_OFF_ACTIVE: _read_flag( - register_value, 12 - ), - BinarySensorEMAAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE: _read_flag( + BinarySensorAttrKey.ANALOG_INPUT_ACTIVE: _read_flag(register_value, 9), + BinarySensorAttrKey.SETPOINT_ACTIVE: _read_flag(register_value, 10), + BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE: _read_flag(register_value, 11), + BinarySensorAttrKey.AUTOHEATER_ACTIVE: _read_flag(register_value, 12), + BinarySensorAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE: _read_flag( register_value, 13 ), - BinarySensorEMAAttrKey.TEMP_LIMIT_REACHED: _read_flag(register_value, 14), - BinarySensorEMAAttrKey.ERROR_OCCURED: _read_flag(register_value, 15), + BinarySensorAttrKey.TEMP_LIMIT_REACHED: _read_flag(register_value, 14), + BinarySensorAttrKey.ERROR_OCCURED: _read_flag(register_value, 15), } def _map_register_to_heater_step( self, register_value: int - ) -> dict[SwitchEMAAttrKey, bool]: + ) -> dict[SwitchAttrKey, bool]: """Map modbus register to status class.""" return { - SwitchEMAAttrKey.SET_HEATER_STEP_HEATER1: _read_flag(register_value, 0), - SwitchEMAAttrKey.SET_HEATER_STEP_HEATER2: _read_flag(register_value, 1), - SwitchEMAAttrKey.SET_HEATER_STEP_HEATER3: _read_flag(register_value, 2), + SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER1: _read_flag(register_value, 0), + SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER2: _read_flag(register_value, 1), + SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER3: _read_flag(register_value, 2), } +def _read_time(register_values: list[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) + + +def _read_str(register_values: list[int]) -> str: + """Read register values as str.""" + return str( + ModbusClient.convert_from_registers( + register_values, ModbusClient.DATATYPE.STRING + ) + ) + + +def _read_byte(register_value: int) -> np.byte: + """Read register value as byte.""" + return np.byte( + ModbusClient.convert_from_registers( + [register_value], ModbusClient.DATATYPE.INT16 + ) + ) + + def _read_int16(register_value: int) -> np.int16: """Read register value as int16.""" return np.int16( diff --git a/custom_components/askoheat/binary_sensor.py b/custom_components/askoheat/binary_sensor.py index 544e2df..76f4d2b 100644 --- a/custom_components/askoheat/binary_sensor.py +++ b/custom_components/askoheat/binary_sensor.py @@ -13,8 +13,6 @@ from custom_components.askoheat.binary_sensor_entities_ema import ( EMA_BINARY_SENSOR_ENTITY_DESCRIPTIONS, ) -from custom_components.askoheat.const import LOGGER -from custom_components.askoheat.data import AskoheatEMAData from .entity import AskoheatEntity @@ -67,6 +65,11 @@ def _handle_coordinator_update(self) -> 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: @@ -75,3 +78,5 @@ def _handle_coordinator_update(self) -> 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 6b24aee..c09d56d 100644 --- a/custom_components/askoheat/binary_sensor_entities_ema.py +++ b/custom_components/askoheat/binary_sensor_entities_ema.py @@ -1,83 +1,101 @@ """Predefined energy managementt askoheat sensors.""" -from custom_components.askoheat.const import BinarySensorEMAAttrKey +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=BinarySensorEMAAttrKey.HEATER1_ACTIVE, - translation_key=BinarySensorEMAAttrKey.HEATER1_ACTIVE, + key=BinarySensorAttrKey.HEATER1_ACTIVE, + translation_key=BinarySensorAttrKey.HEATER1_ACTIVE, icon="mdi:power-plug", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.HEATER2_ACTIVE, - translation_key=BinarySensorEMAAttrKey.HEATER2_ACTIVE, + key=BinarySensorAttrKey.HEATER2_ACTIVE, + translation_key=BinarySensorAttrKey.HEATER2_ACTIVE, icon="mdi:power-plug", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.HEATER3_ACTIVE, - translation_key=BinarySensorEMAAttrKey.HEATER3_ACTIVE, + key=BinarySensorAttrKey.HEATER3_ACTIVE, + translation_key=BinarySensorAttrKey.HEATER3_ACTIVE, icon="mdi:power-plug", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.PUMP_ACTIVE, - translation_key=BinarySensorEMAAttrKey.PUMP_ACTIVE, + key=BinarySensorAttrKey.PUMP_ACTIVE, + translation_key=BinarySensorAttrKey.PUMP_ACTIVE, icon="mdi:pump", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.RELAY_BOARD_CONNECTED, - translation_key=BinarySensorEMAAttrKey.RELAY_BOARD_CONNECTED, + key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED, + translation_key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED, icon="mdi:connection", + device_class=BinarySensorDeviceClass.PROBLEM, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.HEAT_PUMP_REQUEST_ACTIVE, - translation_key=BinarySensorEMAAttrKey.HEAT_PUMP_REQUEST_ACTIVE, + key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE, + translation_key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE, icon="mdi:heat_pump", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.EMERGENCY_MODE_ACTIVE, - translation_key=BinarySensorEMAAttrKey.EMERGENCY_MODE_ACTIVE, + key=BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE, + translation_key=BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE, icon="mdi:car-emergency", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.LEGIONELLA_PROTECTION_ACTIVE, - translation_key=BinarySensorEMAAttrKey.LEGIONELLA_PROTECTION_ACTIVE, + key=BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE, + translation_key=BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE, icon="mdi:shield-sun", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.ANALOG_INPUT_ACTIVE, - translation_key=BinarySensorEMAAttrKey.ANALOG_INPUT_ACTIVE, + key=BinarySensorAttrKey.ANALOG_INPUT_ACTIVE, + translation_key=BinarySensorAttrKey.ANALOG_INPUT_ACTIVE, icon="mdi:sine-wave", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.SETPOINT_ACTIVE, - translation_key=BinarySensorEMAAttrKey.SETPOINT_ACTIVE, + key=BinarySensorAttrKey.SETPOINT_ACTIVE, + translation_key=BinarySensorAttrKey.SETPOINT_ACTIVE, icon="mdi:finance", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.LOAD_FEEDIN_ACTIVE, - translation_key=BinarySensorEMAAttrKey.LOAD_FEEDIN_ACTIVE, + key=BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE, + translation_key=BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE, icon="mdi:solar-power", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.AUTOHEATER_OFF_ACTIVE, - translation_key=BinarySensorEMAAttrKey.AUTOHEATER_OFF_ACTIVE, + key=BinarySensorAttrKey.AUTOHEATER_ACTIVE, + translation_key=BinarySensorAttrKey.AUTOHEATER_ACTIVE, icon="mdi:water-boiler-auto", inverted=True, + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE, - translation_key=BinarySensorEMAAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE, + 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=BinarySensorEMAAttrKey.TEMP_LIMIT_REACHED, - translation_key=BinarySensorEMAAttrKey.TEMP_LIMIT_REACHED, + key=BinarySensorAttrKey.TEMP_LIMIT_REACHED, + translation_key=BinarySensorAttrKey.TEMP_LIMIT_REACHED, icon="mdi:water-boiler-auto", + device_class=BinarySensorDeviceClass.RUNNING, ), AskoheatBinarySensorEntityDescription( - key=BinarySensorEMAAttrKey.ERROR_OCCURED, - translation_key=BinarySensorEMAAttrKey.ERROR_OCCURED, + 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 38389f9..ccd6a42 100644 --- a/custom_components/askoheat/const.py +++ b/custom_components/askoheat/const.py @@ -1,7 +1,7 @@ """Constants for askoheat.""" from datetime import timedelta -from enum import StrEnum +from enum import IntEnum, StrEnum from logging import Logger, getLogger LOGGER: Logger = getLogger(__package__) @@ -19,18 +19,257 @@ SCAN_INTERVAL_DATA = timedelta(minutes=1) -class SwitchEMAAttrKey(StrEnum): - """Askoheat EMA binary switch attribute keys.""" +class NumberAttrKey(StrEnum): + """Askoheat number entities attribute keys.""" - SET_HEATER_STEP_HEATER1 = "set_heater_step_heater1" - SET_HEATER_STEP_HEATER2 = "set_heater_step_heater2" - SET_HEATER_STEP_HEATER3 = "set_heater_step_heater3" + # ----------------------------------------------- + # config block enums + # ----------------------------------------------- + # 0-16 (default 5) + CON_RELAY_SEC_COUNT_SECONDS = "relay_switch_on_inhibit_seconds" + # 0-240 (default 30) + CON_PUMP_SEC_COUNT_SECONDS = "pump_follow_up_time_seconds" + # 2-10080 + CON_AUTO_HEATER_OFF_MINUTES = "auto_heater_off_minutes" + # 0-255 + CON_CASCADE_PRIORIZATION = "cascade_priorization" -class BinarySensorEMAAttrKey(StrEnum): - """Askoheat EMA binary sensor attribute keys.""" + # 0-1000 liter + CON_HEATBUFFER_VOLUME = "heatbuffer_volume_liter" - # from status register + # 50-65 + CON_LEGIO_PROTECTION_TEMPERATURE = "legio_protection_temperature" + # 0-1440 (default 240) + CON_LEGIO_PROTECTION_HEATUP_MINUTES = "legio_protection_heatup_minutes" + # 1-255 + CON_NUMBER_OF_HOUSEHOLD_MEMBERS = "number_of_household_members" + # 0-10'000 watt + CON_LOAD_FEEDIN_BASIC_ENERGY_LEVEL = "load_feedin_basic_energy_level" + # -12 - 12 + CON_TIMEZONE_OFFSET = "timezone_offset" + # 0-120 + CON_LOAD_FEEDIN_DELAY_SECONDS = "load_feedin_delay_seconds" + # 0-240 + CON_RTU_SLAVE_ID = "rtu_slave_id" + CON_ANALOG_INPUT_HYSTERESIS = "analog_input_hysteresis" + + # 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_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_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_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_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_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_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_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_TEMPERATURE = "analog_input_7_threshold_temperature" + + # 0-7 + CON_HEAT_PUMP_REQUEST_OFF_STEP = "heat_pump_request_off_step" + # 0-7 + CON_HEAT_PUMP_REQUEST_ON_STEP = "heat_pump_request_on_step" + # 0-7 + CON_EMERGENCY_MODE_ON_STOP = "emergency_mode_on_step" + + # 0-95 degree + CON_TEMPERATURE_HYSTERESIS = "temperature_hysteresis" + # 0-95 degree + CON_MINIMAL_TEMPERATURE = "minimal_temperature" + # 0-95 degree + CON_SET_HEATER_STEP_TEMPERATURE_LIMIT = "set_heater_step_temp_limit" + # 0-95 degree + CON_LOAD_FEEDIN_OR_SETPOINT_TEMPERATURE_LIMIT = "load_feedin_or_setpoint_temp_limit" + # 0-95 degree + CON_LOW_TARIFF_TEMPERATURE_LIMIT = "low_tariff_temp_limit" + # 0-95 degree + CON_HEATPUMP_REQUEST_TEMPERATURE_LIMIT = "heatpump_request_temp_limit" + + +class TimeAttrKey(StrEnum): + """Askoheat time entities attribute keys.""" + + # i.e. 12:00 AM + CON_LEGIO_PROTECTION_PREFERRED_START_TIME = "legio_protection_preferred_start_time" + CON_LOW_TARIFF_START_TIME = "low_tariff_start_time" + CON_LOW_TARIFF_END_TIME = "low_tariff_end_time" + + +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" + + +class SelectAttrKey(StrEnum): + """Askoheat select entities attribute keys.""" + + CON_RTU_BAUDRATE = "rtu_baudrate" + CON_SMART_METER_TYPE = "smart_meter_type" + CON_ENERGY_METER_TYPE = "energy_meter_type" + + +class SwitchAttrKey(StrEnum): + """Askoheat binary switch attribute keys.""" + + # ----------------------------------------------- + # EMA block enums + # ----------------------------------------------- + EMA_SET_HEATER_STEP_HEATER1 = "set_heater_step_heater1" + EMA_SET_HEATER_STEP_HEATER2 = "set_heater_step_heater2" + EMA_SET_HEATER_STEP_HEATER3 = "set_heater_step_heater3" + + # ----------------------------------------------- + # config block enums + # ----------------------------------------------- + # from input settings register -- begin + # low byte + CON_MISSING_CURRENT_FLOW_TRIGGERS_ERROR = "missing_current_flow_triggers_error" + CON_HEATER_LOAD_VALUE_ONLY_IF_CURRENT_FLOWS = ( + "heater_load_value_only_if_current_flows" + ) + CON_LOAD_FEEDIN_VALUE_ENABLED = "load_feedin_value_enabled" + CON_LOAD_SETPOINT_VALUE_ENABLED = "load_setpoint_value_enabled" + CON_SET_HEATER_STEP_VALUE_ENABLED = "set_heater_step_value_enabled" + CON_SET_ANALOG_INPUT_ENABLED = "analog_input_enabled" + CON_HEATPUMP_REQUEST_INPUT_ENABLED = "heatpump_request_enabled" + CON_EMERGENCY_MODE_ENABLED = "emergency_mode_enabled" + # high byte + CON_LOW_TARIFF_OPTION_ENABLED = "low_tariff_option_enabled" + CON_HOLD_MINIMAL_TEMPERATURE_ENABELD = "hold_minimal_temperature_enabled" + CON_SOFTWARE_CONTROL_SMA_SEMP_ENABLED = "sw_control_sma_semp_enabled" + CON_SOFTWARE_CONTROL_SENEC_HOME_ENABLED = "sw_control_senec_home_enabled" + # from input settings register -- end + + # from auto heater off settings register -- begin + CON_AUTO_OFF_ENABLED = "auto_off_enabled" + 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_EMERGENCY_MODE_ENABLED = "auto_off_emergency_mode_enabled" + # from auto heater off settings register -- end + + # from heatbuffer type settings register -- begin + CON_HEATBUFFER_TYPE_TAP_WATER = "heatbuffer_type_tap_water" + CON_HEATBUFFER_TYPE_HEATING_WATER = "heatbuffer_type_heating_water" + CON_HEATBUFFER_TYPE_COMBINED_HEAT_AND_POWER_UNIT = ( + "heatbuffer_type_combined_heat_and_power_unit" + ) + 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_OTHER = "heatbuffer_type_other" + # from heatbuffer type settings register -- end + + # from heater position register -- begin + CON_HEATER_POSITION_BOTTOM = "heat_position_bottom" + CON_HEATER_POSITION_MIDDLE = "heat_position_middle" + CON_HEATER_POSITION_ASKOWALL = "heat_position_askowall" + # from heater position register -- end + + # from legio setting register -- begin + # low byte + CON_LEGIO_SETTINGS_USE_INTERNAL_TEMP_SENSOR = ( + "legio_settings_use_internal_temp_sensor" + ) + CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR1 = ( + "legio_settings_use_external_temp_sensor1" + ) + CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR2 = ( + "legio_settings_use_external_temp_sensor2" + ) + CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR3 = ( + "legio_settings_use_external_temp_sensor3" + ) + CON_LEGIO_SETTINGS_USE_EXTERNAL_TEMP_SENSOR4 = ( + "legio_settings_use_external_temp_sensor4" + ) + # high byte + CON_LEGIO_SETTINGS_INTERVAL_DAILY = "legio_settings_interval_daily" + CON_LEGIO_SETTINGS_INTERVAL_WEEKLY = "legio_settings_interval_weekly" + CON_LEGIO_SETTINGS_INTERVAL_FORTNIGHTLY = "legio_settings_interval_fortnightly" + CON_LEGIO_SETTINGS_INTERVAL_MONTHLY = "legio_settings_interval_monthly" + CON_LEGIO_SETTINGS_PREFER_FEEDIN_ENERGY = "legio_settings_prefer_feedin_energy" + CON_LEGIO_SETTINGS_PROTECTION_ENABLED = "legio_Setting_protection_enabled" + # from legio_settings register -- end + + # from house type register -- begin + CON_HOUSE_TYPE_SINGLE_FAMILY_HOUSE = "house_type_single_family_house" + CON_HOUSE_TYPE_TWO_FAMILY_HOUSE = "house_type_two_family_house" + CON_HOUSE_TYPE_APPARTMENT_BUILDING = "house_type_appartment_building" + CON_HOUSE_TYPE_COMMERCIAL_BUILDING = "house_type_commercial_building" + # from house type register -- end + + CON_SUMMERTIME_ENABLED = "summertime_enabled" + + # from rtu settings register -- begin + # low byte + CON_RTU_SEND_TWO_STOP_BITS = "rtu_send_two_stop_bits" + CON_RTU_SEND_PARITY_EVEN = "rtu_send_parity_even" + CON_RTU_SEND_PARITY_ODD = "rtu_send_parity_odd" + CON_RTU_SLAVE_MODE_ACTIVE = "rtu_slave_mode_active" + # high byte + CON_RTU_MASTER_MODE_ACTIVE = "rtu_master_mode_active" + # from rtu settings register -- end + + # from temp settings register -- begin + # low byte + CON_USE_INTERNAL_TEMP_SENSOR = "use_internal_temp_sensor" + CON_USE_EXTERNAL_TEMP_SENSOR1 = "use_external_temp_sensor1" + CON_USE_EXTERNAL_TEMP_SENSOR2 = "use_external_temp_sensor2" + CON_USE_EXTERNAL_TEMP_SENSOR3 = "use_external_temp_sensor3" + CON_USE_EXTERNAL_TEMP_SENSOR4 = "use_external_temp_sensor4" + # from temp settings register -- end + + +class BinarySensorAttrKey(StrEnum): + """Askoheat binary sensor attribute keys.""" + + # ----------------------------------------------- + # EMA block enums + # ----------------------------------------------- + # from status register -- begin HEATER1_ACTIVE = "status.heater1" HEATER2_ACTIVE = "status.heater2" HEATER3_ACTIVE = "status.heater3" @@ -42,14 +281,15 @@ class BinarySensorEMAAttrKey(StrEnum): ANALOG_INPUT_ACTIVE = "status.analog_input" SETPOINT_ACTIVE = "status.setpoint" LOAD_FEEDIN_ACTIVE = "status.load_feedin" - AUTOHEATER_OFF_ACTIVE = "status.autoheater_off" + AUTOHEATER_ACTIVE = "status.autoheater" PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE = "status.pump_relay_follow_up_time_active" TEMP_LIMIT_REACHED = "status.temp_limit_reached" ERROR_OCCURED = "status.error" + # from status register -- end -class SensorEMAAttrKey(StrEnum): - """Askoheat EMA sensor attribute keys.""" +class SensorAttrKey(StrEnum): + """Askoheat sensor attribute keys.""" # 250-30000 watt HEATER_LOAD = "heater_load" @@ -64,3 +304,47 @@ class SensorEMAAttrKey(StrEnum): EXTERNAL_TEMPERATUR_SENSOR2_VALUE = "external_temp_sensor2" EXTERNAL_TEMPERATUR_SENSOR3_VALUE = "external_temp_sensor3" EXTERNAL_TEMPERATUR_SENSOR4_VALUE = "external_temp_sensor4" + + +class Baurate(StrEnum): + """Available Baudrates.""" + + BAUD_RATE_1200 = "1200" + BAUD_RATE_2400 = "2400" + BAUD_RATE_4800 = "4800" + BAUD_RATE_9600 = "9600" + BAUD_RATE_14400 = "14400" + BAUD_RATE_19200 = "19200" + BAUD_RATE_28800 = "28800" + BAUD_RATE_38400 = "38400" + BAUD_RATE_57600 = "57600" + BAUD_RATE_76800 = "76800" + BAUD_RATE_115200 = "115200" + BAUD_RATE_230400 = "230400" + + +class SmartMeterType(StrEnum): + """Supported SmartMeter types.""" + + SM_NOT_INSTALLED = "not installed" + SM_ASKOMA_100A = "Askoma smart meter up to 100A" + SM_ASKOMA_200A = "Askoma smart meter up to 200A" + SM_EM340 = "Carlo Gavazzi EM340...S1 PFA" + SM_ASKOMA_RTU_III = "Askoma smart meter RTU III" + SM_OPEC = "Optec (ECS M3)" + SM_EASTRON = "Eastron SDM72D-M" + SM_ALPA_ESS = "ALPHA-ESS Smart Grid Value" + SM_CHNT = "CHNT DTSU666" + SM_SONNENKRAFT = "SONNENKRAFT SK-HWR-6/8/10/12" + SM_FOX_HYBRID_H3 = "FOX HYBRID H3" + SM_FRONIUS = "FRONIUS MODBUS RTU" + SM_M_TEC_ENERGY_BUTLER = "M-TEC ENERGY BUTLER RTU" + + +class EnergyMeterType(IntEnum): + """Supported EnergyMeter types.""" + + EM_NOT_INSTALLED = 0x000 + EM_AUTOMATION_ONE_TYPE_A1EM_BIMOD = 0x001 + EM_AUTOMATION_ONE_TYPE_A1EM_MOD = 0x02 + EM300 = 0x010 diff --git a/custom_components/askoheat/coordinator.py b/custom_components/askoheat/coordinator.py index 1d961cb..0506fb6 100644 --- a/custom_components/askoheat/coordinator.py +++ b/custom_components/askoheat/coordinator.py @@ -10,14 +10,14 @@ from .api import ( AskoheatModbusApiClientError, ) -from .const import DOMAIN, LOGGER, SCAN_INTERVAL_EMA +from .const import DOMAIN, LOGGER, SCAN_INTERVAL_CONFIG, SCAN_INTERVAL_EMA if TYPE_CHECKING: from datetime import timedelta from homeassistant.core import HomeAssistant - from custom_components.askoheat.data import AskoheatEMAData + from custom_components.askoheat.data import AskoheatDataBlock from .data import AskoheatConfigEntry @@ -46,7 +46,6 @@ class AskoheatEMADataUpdateCoordinator(AskoheatDataUpdateCoordinator): """Class to manage fetching askoheat energymanager states.""" config_entry: AskoheatConfigEntry - data: AskoheatEMAData def __init__(self, hass: HomeAssistant) -> None: """Initialize.""" @@ -57,15 +56,59 @@ async def _async_update_data(self) -> dict[str, Any]: try: async with async_timeout.timeout(10): data = await self.config_entry.runtime_data.client.async_read_ema_data() - result: dict[str, Any] = {} - result.update( - { - f"binary_sensor.{k}": data.binary_sensors[k] - for k in data.binary_sensors - } + return _map_data_block_to_dict(data) + except AskoheatModbusApiClientError as exception: + raise UpdateFailed(exception) from exception + + +class AskoheatConfigDataUpdateCoordinator(AskoheatDataUpdateCoordinator): + """Class to manage fetching askoheat energymanager states.""" + + config_entry: AskoheatConfigEntry + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize.""" + super().__init__(hass=hass, scan_interval=SCAN_INTERVAL_CONFIG) + + 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_config_data() ) - result.update({f"sensor.{k}": data.sensors[k] for k in data.sensors}) - result.update({f"switch.{k}": data.switches[k] for k in data.switches}) - return result + return _map_data_block_to_dict(data) except AskoheatModbusApiClientError as exception: raise UpdateFailed(exception) from exception + + +def _map_data_block_to_dict(data: AskoheatDataBlock) -> dict[str, Any]: + """Map askoheat data block to dict.""" + result: dict[str, Any] = {} + if data.binary_sensors: + result.update( + {f"binary_sensor.{k}": data.binary_sensors[k] for k in data.binary_sensors} + ) + + if data.sensors: + result.update({f"sensor.{k}": data.sensors[k] for k in data.sensors}) + + if data.switches: + result.update({f"switch.{k}": data.switches[k] for k in data.switches}) + + if data.number_inputs: + result.update( + {f"number.{k}": data.number_inputs[k] for k in data.number_inputs} + ) + + if data.text_intputs: + result.update({f"text.{k}": data.text_intputs[k] for k in data.text_intputs}) + + if data.select_inputs: + result.update( + {f"select.{k}": data.select_inputs[k] for k in data.select_inputs} + ) + + if data.time_inputs: + result.update({f"time.{k}": data.time_inputs[k] for k in data.time_inputs}) + return result diff --git a/custom_components/askoheat/data.py b/custom_components/askoheat/data.py index a3644f2..15febf5 100644 --- a/custom_components/askoheat/data.py +++ b/custom_components/askoheat/data.py @@ -5,16 +5,23 @@ from dataclasses import dataclass from typing import TYPE_CHECKING -from custom_components.askoheat.const import ( - BinarySensorEMAAttrKey, - SensorEMAAttrKey, - SwitchEMAAttrKey, -) - if TYPE_CHECKING: + from datetime import time + from enum import ReprEnum + from homeassistant.config_entries import ConfigEntry from homeassistant.loader import Integration - + from numpy import number + + from custom_components.askoheat.const import ( + BinarySensorAttrKey, + NumberAttrKey, + SelectAttrKey, + SensorAttrKey, + SwitchAttrKey, + TextAttrKey, + TimeAttrKey, + ) from custom_components.askoheat.coordinator import AskoheatEMADataUpdateCoordinator from .api import AskoHeatModbusApiClient @@ -33,9 +40,13 @@ class AskoheatData: @dataclass -class AskoheatEMAData: +class AskoheatDataBlock: """Data returnes when querying EMA attributes of askoheat.""" - binary_sensors: dict[BinarySensorEMAAttrKey, bool] - sensors: dict[SensorEMAAttrKey, object] - switches: dict[SwitchEMAAttrKey, bool] + binary_sensors: dict[BinarySensorAttrKey, bool] | None = None + sensors: dict[SensorAttrKey, object] | None = None + switches: dict[SwitchAttrKey, bool] | None = None + text_intputs: dict[TextAttrKey, str] | None = None + select_inputs: dict[SelectAttrKey, ReprEnum] | None = None + number_inputs: dict[NumberAttrKey, number] | None = None + time_inputs: dict[TimeAttrKey, time | None] | None = None diff --git a/custom_components/askoheat/entity.py b/custom_components/askoheat/entity.py index 7098715..b859b76 100644 --- a/custom_components/askoheat/entity.py +++ b/custom_components/askoheat/entity.py @@ -5,14 +5,19 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTION +from custom_components.askoheat.model import AdkoheatEntityDescription + +from .const import ATTRIBUTION, LOGGER from .coordinator import AskoheatDataUpdateCoordinator +from homeassistant.core import callback + class AskoheatEntity(CoordinatorEntity[AskoheatDataUpdateCoordinator]): """AskoheatEntity class.""" _attr_attribution = ATTRIBUTION + entity_description: AdkoheatEntityDescription def __init__(self, coordinator: AskoheatDataUpdateCoordinator) -> None: """Initialize.""" @@ -26,3 +31,19 @@ def __init__(self, coordinator: AskoheatDataUpdateCoordinator) -> None: ), }, ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + descr = self.entity_description + + # Calc icon: + icon_state = self._attr_state + if hasattr(self, "_attr_is_on"): + icon_state = self._attr_is_on # type: ignore # noqa: PGH003 + if descr.icon_by_state is not None and icon_state in descr.icon_by_state: + self._attr_icon = descr.icon_by_state.get(icon_state) + else: + self._attr_icon = descr.icon + + super()._handle_coordinator_update() diff --git a/custom_components/askoheat/model.py b/custom_components/askoheat/model.py index 0c12758..3b4e344 100644 --- a/custom_components/askoheat/model.py +++ b/custom_components/askoheat/model.py @@ -3,6 +3,8 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import date, datetime +from decimal import Decimal from functools import cached_property from typing import TYPE_CHECKING @@ -10,22 +12,32 @@ 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 if TYPE_CHECKING: from custom_components.askoheat.const import ( - BinarySensorEMAAttrKey, - SensorEMAAttrKey, - SwitchEMAAttrKey, + BinarySensorAttrKey, + SensorAttrKey, + SwitchAttrKey, ) @dataclass(frozen=True) -class AskoheatBinarySensorEntityDescription(BinarySensorEntityDescription): +class AdkoheatEntityDescription(EntityDescription): + """Class describing base askoheat entity.""" + + icon_by_state: dict[date | datetime | Decimal, str] | None = None + + +@dataclass(frozen=True) +class AskoheatBinarySensorEntityDescription( + AdkoheatEntityDescription, BinarySensorEntityDescription +): """Class describing Askoheat binary sensor entities.""" - key: BinarySensorEMAAttrKey + key: BinarySensorAttrKey platform = Platform.BINARY_SENSOR on_state: str | bool = True on_states: list[str] | None = None @@ -41,11 +53,12 @@ def data_key(self) -> str: @dataclass(frozen=True) class AskoheatSwitchEntityDescription( + AdkoheatEntityDescription, SwitchEntityDescription, ): """Class describing Askoheat switch entities.""" - key: SwitchEMAAttrKey + key: SwitchAttrKey platform = Platform.SWITCH on_state: str | bool = True on_states: list[str] | None = None @@ -61,11 +74,12 @@ def data_key(self) -> str: @dataclass(frozen=True) class AskoheatSensorEntityDescription( + AdkoheatEntityDescription, SensorEntityDescription, ): """Class describing Askoheat sensor entities.""" - key: SensorEMAAttrKey + key: SensorAttrKey platform = Platform.SENSOR factor: float | None = None native_precision: int | None = None diff --git a/custom_components/askoheat/sensor.py b/custom_components/askoheat/sensor.py index b7d2e04..8ad286c 100644 --- a/custom_components/askoheat/sensor.py +++ b/custom_components/askoheat/sensor.py @@ -83,3 +83,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 27983f5..038d4d8 100644 --- a/custom_components/askoheat/sensor_entities_ema.py +++ b/custom_components/askoheat/sensor_entities_ema.py @@ -1,90 +1,107 @@ """Predefined energy managementt askoheat sensors.""" -from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass -from homeassistant.const import UnitOfElectricPotential, UnitOfPower, UnitOfTemperature +from homeassistant.components.sensor.const import ( + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + EntityCategory, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTemperature, +) -from custom_components.askoheat.const import SensorEMAAttrKey +from custom_components.askoheat.const import SensorAttrKey from custom_components.askoheat.model import AskoheatSensorEntityDescription EMA_SENSOR_ENTITY_DESCRIPTIONS = ( AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.ANALOG_INPUT_VALUE, - translation_key=SensorEMAAttrKey.ANALOG_INPUT_VALUE, + key=SensorAttrKey.ANALOG_INPUT_VALUE, + translation_key=SensorAttrKey.ANALOG_INPUT_VALUE, icon="mdi:gauge", native_precision=0, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.HEATER_LOAD, - translation_key=SensorEMAAttrKey.HEATER_LOAD, + key=SensorAttrKey.HEATER_LOAD, + translation_key=SensorAttrKey.HEATER_LOAD, icon="mdi:lightning-bold", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.LOAD_FEEDIN_VALUE, - translation_key=SensorEMAAttrKey.LOAD_FEEDIN_VALUE, + key=SensorAttrKey.LOAD_FEEDIN_VALUE, + translation_key=SensorAttrKey.LOAD_FEEDIN_VALUE, icon="mdi:solar-power", native_precision=0, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.LOAD_SETPOINT_VALUE, - translation_key=SensorEMAAttrKey.LOAD_SETPOINT_VALUE, + key=SensorAttrKey.LOAD_SETPOINT_VALUE, + translation_key=SensorAttrKey.LOAD_SETPOINT_VALUE, icon="mdi:lightning-bolt", native_precision=0, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE, - translation_key=SensorEMAAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE, + key=SensorAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE, + translation_key=SensorAttrKey.INTERNAL_TEMPERATUR_SENSOR_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE, - translation_key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE, + key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE, + translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR1_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE, - translation_key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE, + key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE, + translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR2_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE, - translation_key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE, + key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE, + translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR3_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=None, ), AskoheatSensorEntityDescription( - key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE, - translation_key=SensorEMAAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE, + key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE, + translation_key=SensorAttrKey.EXTERNAL_TEMPERATUR_SENSOR4_VALUE, icon="mdi:thermometer", native_precision=1, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=None, ), ) diff --git a/custom_components/askoheat/switch.py b/custom_components/askoheat/switch.py index 86da492..5be8a85 100644 --- a/custom_components/askoheat/switch.py +++ b/custom_components/askoheat/switch.py @@ -6,6 +6,8 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from custom_components.askoheat.model import AskoheatSwitchEntityDescription + from .entity import AskoheatEntity if TYPE_CHECKING: @@ -39,13 +41,15 @@ async def async_setup_entry( # ) -class IntegrationBlueprintSwitch(AskoheatEntity, SwitchEntity): +class AskoHeatSwitch(AskoheatEntity, SwitchEntity): """askoheat switch class.""" + entity_description: AskoheatSwitchEntityDescription + def __init__( self, coordinator: AskoheatDataUpdateCoordinator, - entity_description: SwitchEntityDescription, + entity_description: AskoheatSwitchEntityDescription, ) -> None: """Initialize the switch class.""" super().__init__(coordinator)