diff --git a/custom_components/weishaupt_modbus/const.py b/custom_components/weishaupt_modbus/const.py index d8b787f..ddaefb1 100644 --- a/custom_components/weishaupt_modbus/const.py +++ b/custom_components/weishaupt_modbus/const.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import timedelta -from homeassistant.const import UnitOfEnergy, UnitOfTemperature, UnitOfTime, UnitOfVolumeFlowRate, PERCENTAGE +from homeassistant.const import UnitOfEnergy, UnitOfTemperature, UnitOfTime, UnitOfVolumeFlowRate, UnitOfPower, PERCENTAGE @dataclass(frozen=True) class MainConstants: @@ -15,6 +15,7 @@ class MainConstants: class FormatConstants: TEMPERATUR = UnitOfTemperature.CELSIUS ENERGY = UnitOfEnergy.KILO_WATT_HOUR + POWER = UnitOfPower.WATT PERCENTAGE = PERCENTAGE NUMBER = "" STATUS = "Status" @@ -28,6 +29,7 @@ class FormatConstants: @dataclass(frozen=True) class TypeConstants: SENSOR = "Sensor" + SENSOR_CALC = "Sensor_Calc" SELECT = "Select" NUMBER = "Number" NUMBER_RO = "Number_RO" diff --git a/custom_components/weishaupt_modbus/entities.py b/custom_components/weishaupt_modbus/entities.py index 30852ee..f49de8e 100644 --- a/custom_components/weishaupt_modbus/entities.py +++ b/custom_components/weishaupt_modbus/entities.py @@ -7,6 +7,9 @@ from homeassistant.helpers.device_registry import DeviceInfo from .const import CONST, FORMATS, TYPES from .modbusobject import ModbusObject +from .items import ModbusItem +from .hpconst import TEMPRANGE_STD, DEVICES +from .kennfeld import PowerMap def BuildEntityList(entries, config_entry, modbusitems, type): # this builds a list of entities that can be used as parameter by async_setup_entry() @@ -19,6 +22,8 @@ def BuildEntityList(entries, config_entry, modbusitems, type): # here the entities are created with the parameters provided by the ModbusItem object case TYPES.SENSOR | TYPES.NUMBER_RO: entries.append(MySensorEntity(config_entry, modbusitems[index])) + case TYPES.SENSOR_CALC: + entries.append(MyCalcSensorEntity(config_entry, modbusitems[index])) case TYPES.SELECT: entries.append(MySelectEntity(config_entry, modbusitems[index])) case TYPES.NUMBER: @@ -50,6 +55,8 @@ def __init__(self, config_entry, modbus_item) -> None: self._attr_state_class = SensorStateClass.TOTAL_INCREASING if self._modbus_item._format == FORMATS.TEMPERATUR: self._attr_state_class = SensorStateClass.MEASUREMENT + if self._modbus_item._format == FORMATS.POWER: + self._attr_state_class = SensorStateClass.MEASUREMENT if self._modbus_item.resultlist != None: self._attr_native_min_value = self._modbus_item.getNumberFromText("min") @@ -70,13 +77,15 @@ def calcTemperature(self,val: float): return val / self._divider def calcPercentage(self,val: float): + if val == None: + return None if val == 65535: return None return val / self._divider @property def translateVal(self): - # reads an translates a value from the modbua + # reads an translates a value from the modbus mbo = ModbusObject(self._config_entry, self._modbus_item) val = mbo.value match self._modbus_item.format: @@ -132,6 +141,49 @@ async def async_update(self) -> None: def device_info(self) -> DeviceInfo: return MyEntity.my_device_info(self) + +class MyCalcSensorEntity(MySensorEntity): + # class that represents a sensor entity derived from Sensorentity + # and decorated with general parameters from MyEntity + # calculates output from map + my_map = PowerMap() + + def __init__(self, config_entry, modbus_item) -> None: + MySensorEntity.__init__(self, config_entry, modbus_item) + + async def async_update(self) -> None: + # the synching is done by the ModbusObject of the entity + self._attr_native_value = self.translateVal + + def calcPower(self, val, x, y): + if val == None: + return val + return (val / 100) * self.my_map.map(0,0) + + @property + def translateVal(self): + # reads an translates a value from the modbus + mbo = ModbusObject(self._config_entry, self._modbus_item) + val = self.calcPercentage(mbo.value) + + mb_x = ModbusItem(self._modbus_item.getNumberFromText("x"),"x",FORMATS.TEMPERATUR,TYPES.SENSOR_CALC,DEVICES.SYS, TEMPRANGE_STD) + mbo_x = ModbusObject(self._config_entry, mb_x) + val_x = self.calcTemperature(mbo_x.value) / 10 + mb_y = ModbusItem(self._modbus_item.getNumberFromText("y"),"y",FORMATS.TEMPERATUR,TYPES.SENSOR_CALC,DEVICES.WP, TEMPRANGE_STD) + mbo_y = ModbusObject(self._config_entry, mb_y) + val_y = self.calcTemperature(mbo_y.value) / 10 + + match self._modbus_item.format: + case FORMATS.POWER: + return self.calcPower(val,val_x,val_y) + case _: + return val / self._divider + + @property + def device_info(self) -> DeviceInfo: + return MySensorEntity.my_device_info(self) + + class MyNumberEntity(NumberEntity, MyEntity): # class that represents a sensor entity derived from Sensorentity # and decorated with general parameters from MyEntity diff --git a/custom_components/weishaupt_modbus/hpconst.py b/custom_components/weishaupt_modbus/hpconst.py index 3c1c759..288a85c 100644 --- a/custom_components/weishaupt_modbus/hpconst.py +++ b/custom_components/weishaupt_modbus/hpconst.py @@ -302,6 +302,12 @@ class DeviceConstants: StatusItem(1,"divider"), ] +RANGE_CALCPOWER = [ + StatusItem(-1,SensorDeviceClass.POWER), + StatusItem(1,"divider"), + StatusItem(30002,"x"), + StatusItem(33104,"y") +] ############################################################################################################################## # Modbus Register List: # @@ -320,6 +326,7 @@ class DeviceConstants: ModbusItem(33101,"Betrieb",FORMATS.STATUS,TYPES.SENSOR,DEVICES.WP, HP_BETRIEB), ModbusItem(33102,"Störmeldung",FORMATS.STATUS,TYPES.SENSOR,DEVICES.WP, HP_STOERMELDUNG), ModbusItem(33103,"Leistungsanforderung",FORMATS.PERCENTAGE,TYPES.SENSOR,DEVICES.WP), + ModbusItem(33103,"Wärmeleistung",FORMATS.POWER,TYPES.SENSOR_CALC,DEVICES.WP,RANGE_CALCPOWER), ModbusItem(33104,"Vorlauftemperatur",FORMATS.TEMPERATUR,TYPES.SENSOR,DEVICES.WP, TEMPRANGE_STD), ModbusItem(33105,"Rücklauftemperatur",FORMATS.TEMPERATUR,TYPES.SENSOR,DEVICES.WP, TEMPRANGE_STD), ModbusItem(43101,"Konfiguration ",FORMATS.NUMBER,TYPES.NUMBER_RO,DEVICES.WP), diff --git a/custom_components/weishaupt_modbus/kennfeld.py b/custom_components/weishaupt_modbus/kennfeld.py index 4d660eb..87a44a4 100644 --- a/custom_components/weishaupt_modbus/kennfeld.py +++ b/custom_components/weishaupt_modbus/kennfeld.py @@ -1,71 +1,62 @@ from scipy.interpolate import CubicSpline import numpy as np -# visual debugging ;-) -# import matplotlib.pyplot as plt - -# these are values extracted from the characteristic curves of heating power found ion the documentation of my heat pump. -# there are two diagrams: -# - heating power vs. outside temperature @ 35 °C flow temperature -# - heating power vs. outside temperature @ 55 °C flow temperature -known_x = [ -30, -25, -22, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40] -# known power values read out from the graphs plotted in documentation. Only 35 °C and 55 °C available -known_y = [[ 5700, 5700, 5700, 5700, 6290, 7580, 8660, 9625, 10300, 10580, 10750, 10790, 10830, 11000, 11000, 11000 ], - [ 5700, 5700, 5700, 5700, 6860, 7300, 8150, 9500, 10300, 10580, 10750, 10790, 10830, 11000, 11000, 11000 ]] - -# the known x values for linear interpolation -known_t = [35, 55] - -# the aim is generating a 2D power map that gives back the actual power for a certain flow temperature and a given outside temperature -# the map should have values on every integer temperature point -# at first, all flow temoperatures are lineary interpolated -steps = 21 -r_to_interpolate = np.linspace(35, 55, steps) - -# build the matrix with linear interpolated samples -# 1st and last row are populated by known values from diagrem, the rest is zero -interp_y = [] -interp_y.append(known_y[0]) -v = np.linspace(0, steps-3, steps-2) -for idx in v: - interp_y.append(np.zeros_like(known_x)) -interp_y.append(known_y[1]) - -# visual debugging ;-) -#plt.plot(np.transpose(interp_y)) -#plt.ylabel('Max Power') -#plt.xlabel('°C') -#plt.show() - -for idx in range(0, len(known_x)): - # the known y for every column - yk = [interp_y[0][idx], interp_y[steps-1][idx]] - - #linear interpolation - ip = np.interp(r_to_interpolate, known_t, yk) - - # sort the interpolated values into the array - for r in range(0, len(r_to_interpolate)): - interp_y[r][idx] = ip[r] - -# visual debugging ;-) -#plt.plot(np.transpose(interp_y)) -#plt.ylabel('Max Power') -#plt.xlabel('°C') -#plt.show() - -# at second step, power vs. outside temp are interpolated using cubic splines -# the output matrix -max_power = [] -# we want to have samples at every integer °C -t = np.linspace(-30, 40, 71) -# cubic spline interpolation of power curves -for idx in range(0, len(r_to_interpolate)): - f = CubicSpline(known_x, interp_y[idx], bc_type='natural') - max_power.append(f(t)) - -# visual debugging ;-) -#plt.plot(t,np.transpose(max_power)) -#plt.ylabel('Max Power') -#plt.xlabel('°C') -#plt.show() +class PowerMap(): + # these are values extracted from the characteristic curves of heating power found ion the documentation of my heat pump. + # there are two diagrams: + # - heating power vs. outside temperature @ 35 °C flow temperature + # - heating power vs. outside temperature @ 55 °C flow temperature + known_x = [ -30, -25, -22, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40] + # known power values read out from the graphs plotted in documentation. Only 35 °C and 55 °C available + known_y = [[ 5700, 5700, 5700, 5700, 6290, 7580, 8660, 9625, 10300, 10580, 10750, 10790, 10830, 11000, 11000, 11000 ], + [ 5700, 5700, 5700, 5700, 6860, 7300, 8150, 9500, 10300, 10580, 10750, 10790, 10830, 11000, 11000, 11000 ]] + + # the known x values for linear interpolation + known_t = [35, 55] + + # the aim is generating a 2D power map that gives back the actual power for a certain flow temperature and a given outside temperature + # the map should have values on every integer temperature point + # at first, all flow temoperatures are lineary interpolated + steps = 21 + r_to_interpolate = np.linspace(35, 55, steps) + + # the output matrix + max_power = [] + + interp_y = [] + + def __init__(self) -> None: + # build the matrix with linear interpolated samples + # 1st and last row are populated by known values from diagrem, the rest is zero + self.interp_y.append(self.known_y[0]) + v = np.linspace(0, self.steps-3, self.steps-2) + for idx in v: + self.interp_y.append(np.zeros_like(self.known_x)) + self.interp_y.append(self.known_y[1]) + + for idx in range(0, len(self.known_x)): + # the known y for every column + yk = [self.interp_y[0][idx], self.interp_y[self.steps-1][idx]] + + #linear interpolation + ip = np.interp(self.r_to_interpolate, self.known_t, yk) + + # sort the interpolated values into the array + for r in range(0, len(self.r_to_interpolate)): + self.interp_y[r][idx] = ip[r] + + # at second step, power vs. outside temp are interpolated using cubic splines + # we want to have samples at every integer °C + t = np.linspace(-30, 40, 71) + # cubic spline interpolation of power curves + for idx in range(0, len(self.r_to_interpolate)): + f = CubicSpline(self.known_x, self.interp_y[idx], bc_type='natural') + self.max_power.append(f(t)) + + def map(self,x,y): + + numrows = len(self.max_power) # 3 rows in your example + return numrows + numcols = len(self.max_power[0]) # 2 columns in your example + return numcols + return self.max_power[x][y] \ No newline at end of file diff --git a/custom_components/weishaupt_modbus/modbusobject.py b/custom_components/weishaupt_modbus/modbusobject.py index f6a364d..c9890df 100644 --- a/custom_components/weishaupt_modbus/modbusobject.py +++ b/custom_components/weishaupt_modbus/modbusobject.py @@ -13,7 +13,7 @@ class ModbusObject(): _ModbusItem = None _DataFormat = None - + _ip = None _port = None _ModbusClient = None @@ -21,11 +21,11 @@ class ModbusObject(): def __init__(self, config_entry, modbus_item): self._ModbusItem = modbus_item #self._HeatPump = heatpump - + self._ip = config_entry.data[CONF_HOST] self._port = config_entry.data[CONF_PORT] self._ModbusClient = None - + def connect(self): try: self._ModbusClient = ModbusClient(host=self._ip, port=self._port) @@ -38,7 +38,7 @@ def value(self): try: self.connect() match self._ModbusItem.type: - case TYPES.SENSOR: + case TYPES.SENSOR | TYPES.SENSOR_CALC: # Sensor entities are read-only return self._ModbusClient.read_input_registers(self._ModbusItem.address, slave=1).registers[0] case TYPES.SELECT | TYPES.NUMBER | TYPES.NUMBER_RO: @@ -48,8 +48,13 @@ def value(self): @value.setter def value(self,value) -> None: - if self._ModbusItem.type == TYPES.SENSOR | self._ModbusItem.type == TYPES.NUMBER_RO: - # Sensor entities are read-only - return - self.connect() - self._ModbusClient.write_register(self._ModbusItem.address, int(value), slave=1) + try: + match self._ModbusItem.type: + case TYPES.SENSOR | TYPES.NUMBER_RO | TYPES.SENSOR_CALC: + # Sensor entities are read-only + return + case _: + self.connect() + self._ModbusClient.write_register(self._ModbusItem.address, int(value), slave=1) + except: # noqua: E722 + return None diff --git a/custom_components/weishaupt_modbus/sensor.py b/custom_components/weishaupt_modbus/sensor.py index 470039e..1e485fa 100644 --- a/custom_components/weishaupt_modbus/sensor.py +++ b/custom_components/weishaupt_modbus/sensor.py @@ -20,6 +20,6 @@ async def async_setup_entry( Entries = [] Entries = BuildEntityList(Entries, config_entry, MODBUS_SYS_ITEMS, TYPES.NUMBER_RO) - + Entries = BuildEntityList(Entries, config_entry, MODBUS_SYS_ITEMS, TYPES.SENSOR_CALC) async_add_entities(BuildEntityList(Entries, config_entry, MODBUS_SYS_ITEMS,TYPES.SENSOR), update_before_add=True)