Skip to content

Commit

Permalink
* fixed integration of number entities
Browse files Browse the repository at this point in the history
* added pre-defined number entity configurations for config block
* initialize entities when registered the first time
  • Loading branch information
toggm committed Nov 13, 2024
1 parent 50e1e43 commit b12429e
Show file tree
Hide file tree
Showing 15 changed files with 724 additions and 129 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ These are some next steps you may want to look into:
- Create your first release.
- Share your integration on the [Home Assistant Forum](https://community.home-assistant.io/).
- Submit your integration to [HACS](https://hacs.xyz/docs/publish/start).


## TODO
- [ ] Integration configuration parameter sections
- [ ] Integration data sensors
- [ ] Integrate write operations for switches and number inputs
- [ ] Add translations for en and de
- [ ] Create service to start auto-feed linked to solar entity and a reserve
- [ ] Provide meatures
- [ ] Cleanup and document
9 changes: 8 additions & 1 deletion custom_components/askoheat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
from homeassistant.loader import async_get_loaded_integration

from .api import AskoHeatModbusApiClient
from .coordinator import AskoheatEMADataUpdateCoordinator
from .coordinator import (
AskoheatConfigDataUpdateCoordinator,
AskoheatEMADataUpdateCoordinator,
)
from .data import AskoheatData

if TYPE_CHECKING:
Expand All @@ -26,6 +29,7 @@
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.NUMBER,
]


Expand All @@ -38,18 +42,21 @@ async def async_setup_entry(
ema_coordinator = AskoheatEMADataUpdateCoordinator(
hass=hass,
)
config_coordinator = AskoheatConfigDataUpdateCoordinator(hass=hass)
entry.runtime_data = AskoheatData(
client=AskoHeatModbusApiClient(
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
),
integration=async_get_loaded_integration(hass, entry.domain),
ema_coordinator=ema_coordinator,
config_coordinator=config_coordinator,
)
await entry.runtime_data.client.connect()

# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
await ema_coordinator.async_config_entry_first_refresh()
await config_coordinator.async_config_entry_first_refresh()

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
Expand Down
85 changes: 55 additions & 30 deletions custom_components/askoheat/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"""Sample API Client."""
"""Modbus API Client."""

from __future__ import annotations

from ast import Num
from datetime import datetime, time
from numbers import Number
from typing import TYPE_CHECKING, Any
from datetime import time
from typing import TYPE_CHECKING, Any, TypeVar

import numpy as np
from pymodbus.client import AsyncModbusTcpClient as ModbusClient
Expand All @@ -25,6 +23,8 @@
from custom_components.askoheat.data import AskoheatDataBlock

if TYPE_CHECKING:
from collections.abc import Callable

from pymodbus.pdu import ModbusPDU


Expand Down Expand Up @@ -65,9 +65,9 @@ async def async_read_ema_data(self) -> AskoheatDataBlock:
async def async_read_config_data(self) -> AskoheatDataBlock:
"""Read EMA states."""
# http://www.download.askoma.com/askofamily_plus/modbus/askoheat-modbus.html#Configuration_Block
data = await self.async_read_input_registers_data(500, 100)
data = await self.async_read_holding_registers_data(500, 100)
LOGGER.debug("async_read_config_data %s", data)
return self._map_ema_data(data)
return self._map_config_data(data)

async def async_read_input_registers_data(
self, address: int, count: int
Expand Down Expand Up @@ -156,63 +156,79 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock:
NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD: _read_float32(
data.registers[58:60]
),
NumberAttrKey.CON_ANALOG_INPUT_0_STEP: _read_byte(data.registers[60]),
NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD_STEP: _read_byte(
data.registers[60]
),
NumberAttrKey.CON_ANALOG_INPUT_0_THRESHOLD_TEMPERATURE: _read_byte(
data.registers[61]
),
# 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_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_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_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_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_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_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_STEP: _read_byte(
data.registers[88]
),
NumberAttrKey.CON_ANALOG_INPUT_7_THRESHOLD_TEMPERATURE: _read_byte(
data.registers[89]
),
Expand Down Expand Up @@ -426,7 +442,8 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock:
},
time_inputs={
TimeAttrKey.CON_LEGIO_PROTECTION_PREFERRED_START_TIME: _read_time(
data.registers[12:16]
register_value_hours=data.registers[12],
register_value_minutes=data.registers[13],
),
TimeAttrKey.CON_LOW_TARIFF_START_TIME: time(
hour=_read_byte(data.registers[52]),
Expand All @@ -438,12 +455,11 @@ def _map_config_data(self, data: ModbusPDU) -> AskoheatDataBlock:
),
},
text_intputs={
TextAttrKey.CON_WATER_HARDNESS: _read_str(data.registers[16:20]),
TextAttrKey.CON_INFO_STRING: _read_str(data.registers[22:38]),
},
select_inputs={
SelectAttrKey.CON_RTU_BAUDRATE: Baurate(
_read_str(data.registers[46:49])
SelectAttrKey.CON_RTU_BAUDRATE: _read_enum(
data.registers[46:49], Baurate
),
SelectAttrKey.CON_ENERGY_METER_TYPE: EnergyMeterType(
_read_byte(data.registers[51])
Expand Down Expand Up @@ -491,22 +507,31 @@ def _map_register_to_heater_step(
}


def _read_time(register_values: list[int]) -> time | None:
def _read_time(register_value_hours: int, register_value_minutes: int) -> time | None:
"""Read register values as string and parse as time."""
time_string = _read_str(register_values)
try:
return datetime.strptime(time_string, "%H:%M %p").time # type: ignore # noqa: DTZ007, PGH003
except Exception as err: # noqa: BLE001
LOGGER.warning("Could not read time from string %s, %s", time_string, err)
hours = _read_uint16(register_value_hours)
minutes = _read_uint16(register_value_minutes)
return time(hour=hours, minute=minutes)


T = TypeVar("T")


def _read_enum(register_values: list[int], factory: Callable[[str], T]) -> T:
"""Read register values as enum."""
str_value = _read_str(register_values)
return factory(str_value)


def _read_str(register_values: list[int]) -> str:
"""Read register values as str."""
return str(
ModbusClient.convert_from_registers(
register_values, ModbusClient.DATATYPE.STRING
)
)
# custom implementation as strings a represented with little endian
byte_list = bytearray()
for x in register_values:
byte_list.extend(int.to_bytes(x, 2, "little"))
if byte_list[-1:] == b"\00":
byte_list = byte_list[:-1]
return byte_list.decode("utf-8")


def _read_byte(register_value: int) -> np.byte:
Expand Down
4 changes: 1 addition & 3 deletions custom_components/askoheat/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ def __init__(
entity_description: AskoheatBinarySensorEntityDescription,
) -> None:
"""Initialize the binary_sensor class."""
super().__init__(coordinator)
self.entity_description = entity_description
super().__init__(coordinator, entity_description)
self.entity_id = ENTITY_ID_FORMAT.format(entity_description.key)
self._attr_unique_id = self.entity_id

Expand All @@ -77,6 +76,5 @@ def _handle_coordinator_update(self) -> None:
self.entity_description.on_states is not None
and self._attr_state in self.entity_description.on_states
)
self.async_write_ha_state()

super()._handle_coordinator_update()
18 changes: 1 addition & 17 deletions custom_components/askoheat/binary_sensor_entities_ema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,83 @@

from homeassistant.components.binary_sensor import BinarySensorDeviceClass


from custom_components.askoheat.const import BinarySensorAttrKey
from custom_components.askoheat.model import AskoheatBinarySensorEntityDescription

EMA_BINARY_SENSOR_ENTITY_DESCRIPTIONS = (
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.HEATER1_ACTIVE,
translation_key=BinarySensorAttrKey.HEATER1_ACTIVE,
icon="mdi:power-plug",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.HEATER2_ACTIVE,
translation_key=BinarySensorAttrKey.HEATER2_ACTIVE,
icon="mdi:power-plug",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.HEATER3_ACTIVE,
translation_key=BinarySensorAttrKey.HEATER3_ACTIVE,
icon="mdi:power-plug",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.PUMP_ACTIVE,
translation_key=BinarySensorAttrKey.PUMP_ACTIVE,
icon="mdi:pump",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED,
translation_key=BinarySensorAttrKey.RELAY_BOARD_CONNECTED,
icon="mdi:connection",
device_class=BinarySensorDeviceClass.PROBLEM,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE,
translation_key=BinarySensorAttrKey.HEAT_PUMP_REQUEST_ACTIVE,
icon="mdi:heat_pump",
icon="mdi:heat-pump",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE,
translation_key=BinarySensorAttrKey.EMERGENCY_MODE_ACTIVE,
icon="mdi:car-emergency",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE,
translation_key=BinarySensorAttrKey.LEGIONELLA_PROTECTION_ACTIVE,
icon="mdi:shield-sun",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.ANALOG_INPUT_ACTIVE,
translation_key=BinarySensorAttrKey.ANALOG_INPUT_ACTIVE,
icon="mdi:sine-wave",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.SETPOINT_ACTIVE,
translation_key=BinarySensorAttrKey.SETPOINT_ACTIVE,
icon="mdi:finance",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE,
translation_key=BinarySensorAttrKey.LOAD_FEEDIN_ACTIVE,
icon="mdi:solar-power",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.AUTOHEATER_ACTIVE,
translation_key=BinarySensorAttrKey.AUTOHEATER_ACTIVE,
icon="mdi:water-boiler-auto",
inverted=True,
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE,
translation_key=BinarySensorAttrKey.PUMP_RELAY_FOLLOW_UP_TIME_ACTIVE,
icon="mdi:water-boiler-auto",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.TEMP_LIMIT_REACHED,
translation_key=BinarySensorAttrKey.TEMP_LIMIT_REACHED,
icon="mdi:water-boiler-auto",
device_class=BinarySensorDeviceClass.RUNNING,
),
AskoheatBinarySensorEntityDescription(
key=BinarySensorAttrKey.ERROR_OCCURED,
translation_key=BinarySensorAttrKey.ERROR_OCCURED,
icon="mdi:water-thermometer",
device_class=BinarySensorDeviceClass.PROBLEM,
),
Expand Down
Loading

0 comments on commit b12429e

Please sign in to comment.