Skip to content

Commit

Permalink
added sensors of data/operation block including calculated duration i…
Browse files Browse the repository at this point in the history
…n minutes
  • Loading branch information
toggm committed Nov 21, 2024
1 parent c50cfed commit 644557a
Show file tree
Hide file tree
Showing 13 changed files with 878 additions and 90 deletions.
4 changes: 4 additions & 0 deletions custom_components/askoheat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .coordinator import (
AskoheatConfigDataUpdateCoordinator,
AskoheatEMADataUpdateCoordinator,
AskoheatOperationDataUpdateCoordinator,
AskoheatParameterDataUpdateCoordinator,
)
from .data import AskoheatData
Expand Down Expand Up @@ -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],
Expand All @@ -58,13 +60,15 @@ 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()

# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
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))
Expand Down
96 changes: 88 additions & 8 deletions custom_components/askoheat/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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 = []
Expand Down Expand Up @@ -281,17 +300,25 @@ 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)
case IntEnumInputDescriptor(starting_register, factory):
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(
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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)
]
38 changes: 19 additions & 19 deletions custom_components/askoheat/api_conf_desc.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
Float32RegisterInputDescriptor,
IntEnumInputDescriptor,
RegisterBlockDescriptor,
SignedIntRegisterInputDescriptor,
SignedInt16RegisterInputDescriptor,
StrEnumInputDescriptor,
StringRegisterInputDescriptor,
TimeRegisterInputDescriptor,
UnsignedIntRegisterInputDescriptor,
UnsignedInt16RegisterInputDescriptor,
)
from custom_components.askoheat.const import (
Baudrate,
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 644557a

Please sign in to comment.