Skip to content

Commit

Permalink
implemented writing of number inputs. Fix representation of heater st…
Browse files Browse the repository at this point in the history
…eps in ema config block, make it writable
  • Loading branch information
toggm committed Nov 14, 2024
1 parent 4311143 commit 2664d1b
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 86 deletions.
26 changes: 16 additions & 10 deletions custom_components/askoheat/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async def async_write_config_data(
CONF_REGISTER_BLOCK_DESCRIPTOR.absolute_register_index(api_desc),
register_values,
)
return await self.async_read_ema_data()
return await self.async_read_config_data()

async def __async_read_single_input_register(
self,
Expand Down Expand Up @@ -583,8 +583,10 @@ def _read_byte(register_value: int) -> np.byte:

def _prepare_byte(value: object) -> list[int]:
"""Prepare byte value to be able to write to a register."""
if not isinstance(value, np.signedinteger):
LOGGER.error("Cannot convert value %s as byte, wrong datatype %r", value, value)
if not isinstance(value, number | float):
LOGGER.error(
"Cannot convert value %s as byte, wrong datatype %r", value, type(value)
)
return []

return ModbusClient.convert_to_registers(int(value), ModbusClient.DATATYPE.INT16)
Expand All @@ -601,9 +603,11 @@ def _read_int16(register_value: int) -> np.int16:

def _prepare_int16(value: object) -> list[int]:
"""Prepare signed int value to be able to write to a register."""
if not isinstance(value, np.signedinteger):
if not isinstance(value, number | float):
LOGGER.error(
"Cannot convert value %s as signed int, wrong datatype %r", value, value
"Cannot convert value %s as signed int, wrong datatype %r",
value,
type(value),
)
return []
return ModbusClient.convert_to_registers(int(value), ModbusClient.DATATYPE.INT16)
Expand All @@ -620,9 +624,11 @@ def _read_uint16(register_value: int) -> np.uint16:

def _prepare_uint16(value: object) -> list[int]:
"""Prepare unsigned int value to be able to write to a register."""
if not isinstance(value, np.unsignedinteger):
if not isinstance(value, number | float):
LOGGER.error(
"Cannot convert value %s as unsigned int, wrong datatype %r", value, value
"Cannot convert value %s as unsigned int, wrong datatype %r",
value,
type(value),
)
return []

Expand All @@ -640,9 +646,9 @@ def _read_float32(register_values: list[int]) -> np.float32:

def _prepare_float32(value: object) -> list[int]:
"""Prepare float32 value to be able to write to a register."""
if not isinstance(value, np.floating):
if not isinstance(value, number | float):
LOGGER.error(
"Cannot convert value %s as float32, wrong datatype %r", value, value
"Cannot convert value %s as float32, wrong datatype %r", value, type(value)
)
return []
return ModbusClient.convert_to_registers(
Expand All @@ -661,7 +667,7 @@ def _prepare_flag(register_value: int, flag: object, index: int) -> list[int]:
LOGGER.error(
"Cannot convert value %s as flag, wrong datatype %r",
flag,
flag,
type(flag),
)
return []

Expand Down
80 changes: 37 additions & 43 deletions custom_components/askoheat/api_ema_desc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.number import NumberMode
from homeassistant.components.sensor.const import (
SensorDeviceClass,
SensorStateClass,
Expand All @@ -15,6 +16,7 @@
)

from custom_components.askoheat.api_desc import (
ByteRegisterInputDescriptor,
FlagRegisterInputDescriptor,
Float32RegisterInputDescriptor,
RegisterBlockDescriptor,
Expand All @@ -23,13 +25,13 @@
)
from custom_components.askoheat.const import (
BinarySensorAttrKey,
NumberAttrKey,
SensorAttrKey,
SwitchAttrKey,
)
from custom_components.askoheat.model import (
AskoheatBinarySensorEntityDescription,
AskoheatNumberEntityDescription,
AskoheatSensorEntityDescription,
AskoheatSwitchEntityDescription,
)

EMA_REGISTER_BLOCK_DESCRIPTOR = RegisterBlockDescriptor(
Expand Down Expand Up @@ -129,26 +131,6 @@
api_descriptor=FlagRegisterInputDescriptor(starting_register=16, bit=15),
),
],
switches=[
AskoheatSwitchEntityDescription(
key=SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER1,
icon="mdi:heat-wave",
entity_category=None,
api_descriptor=FlagRegisterInputDescriptor(starting_register=18, bit=0),
),
AskoheatSwitchEntityDescription(
key=SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER2,
icon="mdi:heat-wave",
entity_category=None,
api_descriptor=FlagRegisterInputDescriptor(starting_register=18, bit=1),
),
AskoheatSwitchEntityDescription(
key=SwitchAttrKey.EMA_SET_HEATER_STEP_HEATER3,
icon="mdi:heat-wave",
entity_category=None,
api_descriptor=FlagRegisterInputDescriptor(starting_register=18, bit=2),
),
],
sensors=[
AskoheatSensorEntityDescription(
key=SensorAttrKey.HEATER_LOAD,
Expand All @@ -157,27 +139,7 @@
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
entity_category=None,
api_descriptor=UnsignedIntRegisterInputDescriptor(16),
),
AskoheatSensorEntityDescription(
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,
api_descriptor=SignedIntRegisterInputDescriptor(19),
),
AskoheatSensorEntityDescription(
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,
api_descriptor=SignedIntRegisterInputDescriptor(20),
api_descriptor=UnsignedIntRegisterInputDescriptor(17),
),
AskoheatSensorEntityDescription(
key=SensorAttrKey.ANALOG_INPUT_VALUE,
Expand Down Expand Up @@ -240,4 +202,36 @@
api_descriptor=Float32RegisterInputDescriptor(33),
),
],
number_inputs=[
AskoheatNumberEntityDescription(
key=NumberAttrKey.SET_HEADER_STEP_VALUE,
icon="mdi:lightning-bolt",
native_min_value=0,
native_max_value=7,
native_step=1,
entity_category=None,
mode=NumberMode.SLIDER,
api_descriptor=ByteRegisterInputDescriptor(18),
),
AskoheatNumberEntityDescription(
key=NumberAttrKey.LOAD_SETPOINT_VALUE,
icon="mdi:lightning-bolt",
native_min_value=250,
native_max_value=30000,
native_precision=0,
native_unit_of_measurement=UnitOfPower.WATT,
entity_category=None,
api_descriptor=SignedIntRegisterInputDescriptor(19),
),
AskoheatNumberEntityDescription(
key=NumberAttrKey.LOAD_FEEDIN_VALUE,
icon="mdi:solar-power",
native_min_value=-30000,
native_max_value=30000,
native_precision=0,
native_unit_of_measurement=UnitOfPower.WATT,
entity_category=None,
api_descriptor=SignedIntRegisterInputDescriptor(20),
),
],
)
18 changes: 7 additions & 11 deletions custom_components/askoheat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
class NumberAttrKey(StrEnum):
"""Askoheat number entities attribute keys."""

# 1-7
SET_HEADER_STEP_VALUE = "set_heater_step"
# 250-30000 watt
LOAD_SETPOINT_VALUE = "load_setpoint"
# -30000-30000 watt
LOAD_FEEDIN_VALUE = "load_feedin"

# -----------------------------------------------
# config block enums
# -----------------------------------------------
Expand Down Expand Up @@ -141,13 +148,6 @@ class SelectAttrKey(StrEnum):
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
# -----------------------------------------------
Expand Down Expand Up @@ -283,10 +283,6 @@ class SensorAttrKey(StrEnum):

# 250-30000 watt
HEATER_LOAD = "heater_load"
# 250-30000 watt
LOAD_SETPOINT_VALUE = "load_setpoint"
# -30000-30000 watt
LOAD_FEEDIN_VALUE = "load_feedin"
# 0-10V
ANALOG_INPUT_VALUE = "analog_input"
INTERNAL_TEMPERATUR_SENSOR_VALUE = "internal_temp_sensor"
Expand Down
22 changes: 13 additions & 9 deletions custom_components/askoheat/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
from __future__ import annotations

from abc import abstractmethod
from enum import StrEnum
from typing import TYPE_CHECKING, Any

import async_timeout
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from custom_components.askoheat.api_desc import RegisterInputDescriptor

from .api import (
AskoheatModbusApiClientError,
)
Expand All @@ -21,6 +18,7 @@

from homeassistant.core import HomeAssistant

from custom_components.askoheat.api_desc import RegisterInputDescriptor
from custom_components.askoheat.data import AskoheatDataBlock

from .data import AskoheatConfigEntry
Expand All @@ -31,6 +29,7 @@ class AskoheatDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching state of askoheat through a single API call."""

config_entry: AskoheatConfigEntry
_writing: bool = False

def __init__(self, hass: HomeAssistant, scan_interval: timedelta) -> None:
"""Initialize."""
Expand All @@ -48,7 +47,7 @@ def __init__(self, hass: HomeAssistant, scan_interval: timedelta) -> None:
@abstractmethod
async def async_write(
self, api_desc: RegisterInputDescriptor, value: object
) -> dict[str, Any]:
) -> None:
"""Write parameter to Askoheat."""


Expand All @@ -63,6 +62,9 @@ def __init__(self, hass: HomeAssistant) -> None:

async def _async_update_data(self) -> dict[str, Any]:
"""Update ema data via library."""
# prevent concurrent reads while writing is in progress
if self._writing:
return self.data
try:
async with async_timeout.timeout(10):
data = await self.config_entry.runtime_data.client.async_read_ema_data()
Expand All @@ -72,23 +74,25 @@ async def _async_update_data(self) -> dict[str, Any]:

async def async_write(
self, api_desc: RegisterInputDescriptor, value: object
) -> dict[str, Any]:
) -> None:
"""Write parameter ema block of Askoheat."""
try:
self._writing = True
async with async_timeout.timeout(10):
data = await self.config_entry.runtime_data.client.async_write_ema_data(
api_desc, value
)
return _map_data_block_to_dict(data)
self.data = _map_data_block_to_dict(data)
except AskoheatModbusApiClientError as exception:
raise UpdateFailed(exception) from exception
finally:
self._writing = False


class AskoheatConfigDataUpdateCoordinator(AskoheatDataUpdateCoordinator):
"""Class to manage fetching askoheat energymanager states."""

config_entry: AskoheatConfigEntry
_writing: bool = False

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
Expand All @@ -110,7 +114,7 @@ async def _async_update_data(self) -> dict[str, Any]:

async def async_write(
self, api_desc: RegisterInputDescriptor, value: object
) -> dict[str, Any]:
) -> None:
"""Write parameter ema block of Askoheat."""
try:
self._writing = True
Expand All @@ -120,7 +124,7 @@ async def async_write(
api_desc, value
)
)
return _map_data_block_to_dict(data)
self.data = _map_data_block_to_dict(data)
except AskoheatModbusApiClientError as exception:
raise UpdateFailed(exception) from exception
finally:
Expand Down
24 changes: 24 additions & 0 deletions custom_components/askoheat/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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.const import LOGGER
from custom_components.askoheat.model import (
AskoheatNumberEntityDescription,
)
Expand All @@ -28,6 +30,13 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the number platform."""
async_add_entities(
AskoHeatNumber(
coordinator=entry.runtime_data.ema_coordinator,
entity_description=entity_description,
)
for entity_description in EMA_REGISTER_BLOCK_DESCRIPTOR.number_inputs
)
async_add_entities(
AskoHeatNumber(
coordinator=entry.runtime_data.config_coordinator,
Expand Down Expand Up @@ -69,3 +78,18 @@ def _handle_coordinator_update(self) -> None:
)
self.async_write_ha_state()
super()._handle_coordinator_update()

async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
if self.entity_description.api_descriptor is None:
LOGGER.error(
"Cannot set native value, missing api_descriptor on entity %s",
self.entity_id,
)
return
if self.entity_description.factor is not None:
value = int(value / self.entity_description.factor)
await self.coordinator.async_write(
self.entity_description.api_descriptor, value
)
self._handle_coordinator_update()
8 changes: 8 additions & 0 deletions custom_components/askoheat/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity
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.model import AskoheatSensorEntityDescription

Expand All @@ -34,6 +35,13 @@ async def async_setup_entry(
)
for entity_description in EMA_REGISTER_BLOCK_DESCRIPTOR.sensors
)
async_add_entities(
AskoheatSensor(
coordinator=entry.runtime_data.config_coordinator,
entity_description=entity_description,
)
for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.sensors
)


class AskoheatSensor(AskoheatEntity[AskoheatSensorEntityDescription], SensorEntity):
Expand Down
Loading

0 comments on commit 2664d1b

Please sign in to comment.