Skip to content

Commit

Permalink
power
Browse files Browse the repository at this point in the history
  • Loading branch information
OStrama committed Oct 11, 2024
1 parent cab14f1 commit e158d57
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 80 deletions.
4 changes: 3 additions & 1 deletion custom_components/weishaupt_modbus/const.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -15,6 +15,7 @@ class MainConstants:
class FormatConstants:
TEMPERATUR = UnitOfTemperature.CELSIUS
ENERGY = UnitOfEnergy.KILO_WATT_HOUR
POWER = UnitOfPower.WATT
PERCENTAGE = PERCENTAGE
NUMBER = ""
STATUS = "Status"
Expand All @@ -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"
Expand Down
54 changes: 53 additions & 1 deletion custom_components/weishaupt_modbus/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions custom_components/weishaupt_modbus/hpconst.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: #
Expand All @@ -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),
Expand Down
127 changes: 59 additions & 68 deletions custom_components/weishaupt_modbus/kennfeld.py
Original file line number Diff line number Diff line change
@@ -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]
23 changes: 14 additions & 9 deletions custom_components/weishaupt_modbus/modbusobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
class ModbusObject():
_ModbusItem = None
_DataFormat = None

_ip = None
_port = None
_ModbusClient = None

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)
Expand All @@ -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:
Expand All @@ -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
2 changes: 1 addition & 1 deletion custom_components/weishaupt_modbus/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit e158d57

Please sign in to comment.