diff --git a/custom_components/weishaupt_modbus/config_flow.py b/custom_components/weishaupt_modbus/config_flow.py index 094545a..d1596c0 100644 --- a/custom_components/weishaupt_modbus/config_flow.py +++ b/custom_components/weishaupt_modbus/config_flow.py @@ -34,8 +34,8 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: # throw CannotConnect # If the authentication is wrong: # InvalidAuth - if not await validateModbusConnection(data["host"], data["port"]): - raise CannotConnect + # if not await validateModbusConnection(data["host"], data["port"]): + # raise CannotConnect # Return info that you want to store in the config entry. # "Title" is what is displayed to the user for this hub device # It is stored internally in HA as part of the device config. @@ -74,16 +74,9 @@ async def async_step_user(self, user_input=None): ) -async def validateModbusConnection(host, port): - """Validate the host.""" - client = AsyncModbusTcpClient(host=host, port=port) - await client.connect() - return client.connected - - class InvalidHost(exceptions.HomeAssistantError): """Error to indicate there is an invalid hostname.""" -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate that the connecton to the heatpump failed.""" +class ConnectionFailed(exceptions.HomeAssistantError): + """Error to indicate there is an invalid hostname.""" diff --git a/custom_components/weishaupt_modbus/const.py b/custom_components/weishaupt_modbus/const.py index 14eee76..c72ccb3 100644 --- a/custom_components/weishaupt_modbus/const.py +++ b/custom_components/weishaupt_modbus/const.py @@ -1,6 +1,14 @@ from dataclasses import dataclass from datetime import timedelta -from homeassistant.const import UnitOfEnergy, UnitOfTemperature, UnitOfTime, UnitOfVolumeFlowRate, UnitOfPower, PERCENTAGE +from homeassistant.const import ( + UnitOfEnergy, + UnitOfTemperature, + UnitOfTime, + UnitOfVolumeFlowRate, + UnitOfPower, + PERCENTAGE, +) + @dataclass(frozen=True) class MainConstants: @@ -10,8 +18,10 @@ class MainConstants: APPID = 100 KENNFELDFILE = "weishaupt_wbb_kennfeld.json" + CONST = MainConstants() + @dataclass(frozen=True) class FormatConstants: TEMPERATUR = UnitOfTemperature.CELSIUS @@ -25,8 +35,10 @@ class FormatConstants: TIME_MIN = UnitOfTime.MINUTES TIME_H = UnitOfTime.HOURS + FORMATS = FormatConstants() + @dataclass(frozen=True) class TypeConstants: SENSOR = "Sensor" @@ -35,5 +47,5 @@ class TypeConstants: NUMBER = "Number" NUMBER_RO = "Number_RO" -TYPES = TypeConstants() +TYPES = TypeConstants() diff --git a/custom_components/weishaupt_modbus/entities.py b/custom_components/weishaupt_modbus/entities.py index ca301ad..378ad8e 100644 --- a/custom_components/weishaupt_modbus/entities.py +++ b/custom_components/weishaupt_modbus/entities.py @@ -1,4 +1,4 @@ -# import warnings +import warnings from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT from homeassistant.components.sensor import ( @@ -109,8 +109,8 @@ async def translateVal(self): return val return val / self._divider - @translateVal.setter - async def translateVal(self, value): + # @translateVal.setter + async def settranslateVal(self, value): # translates and writes a value to the modbus mbo = ModbusObject(self._config_entry, self._modbus_item) if mbo == None: @@ -122,7 +122,7 @@ async def translateVal(self, value): val = self._modbus_item.getNumberFromText(value) case _: val = value * self._divider - mbo.value = val + await mbo.setvalue(val) # = val def my_device_info(self) -> DeviceInfo: # helper to build the device info @@ -189,7 +189,8 @@ async def translateVal(self): mbo_x = ModbusObject(self._config_entry, mb_x) if mbo_x == None: return None - val_x = self.calcTemperature(await mbo_x.value) / 10 + t_temp = await mbo_x.value + val_x = self.calcTemperature(t_temp) / 10 mb_y = ModbusItem( self._modbus_item.getNumberFromText("y"), "y", @@ -199,9 +200,10 @@ async def translateVal(self): TEMPRANGE_STD, ) mbo_y = ModbusObject(self._config_entry, mb_y) - if mbo_x == None: + if mbo_y == None: return None - val_y = self.calcTemperature(await mbo_y.value) / 10 + t_temp = await mbo_y.value + val_y = self.calcTemperature(t_temp) / 10 match self._modbus_item.format: case FORMATS.POWER: @@ -232,8 +234,8 @@ def __init__(self, config_entry, modbus_item) -> None: self._attr_native_step = self._modbus_item.getNumberFromText("step") async def async_set_native_value(self, value: float) -> None: - self.translateVal = value - self._attr_native_value = self.translateVal + await self.settranslateVal(value) + self._attr_native_value = await self.translateVal self.async_write_ha_state() async def async_update(self) -> None: @@ -263,8 +265,8 @@ def __init__(self, config_entry, modbus_item) -> None: async def async_select_option(self, option: str) -> None: # the synching is done by the ModbusObject of the entity - self.translateVal = option - self._attr_current_option = self.translateVal + await self.settranslateVal(option) + self._attr_current_option = await self.translateVal self.async_write_ha_state() async def async_update(self) -> None: diff --git a/custom_components/weishaupt_modbus/hpconst.py b/custom_components/weishaupt_modbus/hpconst.py index b718335..8b8ec83 100644 --- a/custom_components/weishaupt_modbus/hpconst.py +++ b/custom_components/weishaupt_modbus/hpconst.py @@ -629,9 +629,11 @@ class DeviceConstants: 34101, "Status 2. WEZ", FORMATS.STATUS, TYPES.SENSOR, DEVICES.W2, W2_STATUS ), ModbusItem( - 34102, "Betriebsstunden 2. WEZ", FORMATS.TIME_H, TYPES.SENSOR, DEVICES.W2 + 34102, "Schaltspiele E-Heizung 1", FORMATS.NUMBER, TYPES.SENSOR, DEVICES.W2 + ), + ModbusItem( + 34103, "Schaltspiele E-Heizung 2", FORMATS.NUMBER, TYPES.SENSOR, DEVICES.W2 ), - ModbusItem(34103, "Schaltspiele 2. WEZ", FORMATS.NUMBER, TYPES.SENSOR, DEVICES.W2), ModbusItem( 34104, "Status E-Heizung 1", FORMATS.STATUS, TYPES.SENSOR, DEVICES.W2, W2_STATUS ), diff --git a/custom_components/weishaupt_modbus/kennfeld.py b/custom_components/weishaupt_modbus/kennfeld.py index 495a646..ebeca48 100644 --- a/custom_components/weishaupt_modbus/kennfeld.py +++ b/custom_components/weishaupt_modbus/kennfeld.py @@ -1,27 +1,64 @@ -#from scipy.interpolate import CubicSpline +# from scipy.interpolate import CubicSpline from numpy.polynomial import Chebyshev import numpy as np -#import matplotlib.pyplot as plt +# import matplotlib.pyplot as plt import json from .const import CONST -class PowerMap(): + +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: + # 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_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 ]] + 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 + # at first, all flow temoperatures are lineary interpolated steps = 21 r_to_interpolate = np.linspace(35, 55, steps) @@ -31,36 +68,38 @@ class PowerMap(): interp_y = [] def __init__(self) -> None: - #try to load values from json file - + # try to load values from json file + try: openfile = open(CONST.KENNFELDFILE, "r") except IOError: - kennfeld = {'known_x': self.known_x, - 'known_y': self.known_y, - 'known_t': self.known_t} + kennfeld = { + "known_x": self.known_x, + "known_y": self.known_y, + "known_t": self.known_t, + } with open(CONST.KENNFELDFILE, "w") as outfile: json.dump(kennfeld, outfile) else: json_object = json.load(openfile) - self.known_x = json_object['known_x'] - self.known_y = json_object['known_y'] - self.known_t = json_object['known_t'] + self.known_x = json_object["known_x"] + self.known_y = json_object["known_y"] + self.known_t = json_object["known_t"] openfile.close() # 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) + 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]] + yk = [self.interp_y[0][idx], self.interp_y[self.steps - 1][idx]] - #linear interpolation + # linear interpolation ip = np.interp(self.r_to_interpolate, self.known_t, yk) # sort the interpolated values into the array @@ -72,59 +111,55 @@ def __init__(self) -> None: 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') - f = Chebyshev.fit(self.known_x, self.interp_y[idx], deg = 8) + # f = CubicSpline(self.known_x, self.interp_y[idx], bc_type='natural') + f = Chebyshev.fit(self.known_x, self.interp_y[idx], deg=8) self.max_power.append(f(t)) - def map(self,x,y): - - numrows = len(self.max_power) # 3 rows in your example - - numcols = len(self.max_power[0]) # 2 columns in your example - x=x-self.known_x[0] - if x<0: - x=0 - if x>70: - x= 70 - y=y-self.known_t[0] - if y<0: - y=0 - if y> (self.steps-1): - y=self.steps-1 - + def map(self, x, y): + numrows = len(self.max_power) # 3 rows in your example + + numcols = len(self.max_power[0]) # 2 columns in your example + x = x - self.known_x[0] + if x < 0: + x = 0 + if x > 70: + x = 70 + y = y - self.known_t[0] + if y < 0: + y = 0 + if y > (self.steps - 1): + y = self.steps - 1 + return self.max_power[int(y)][int(x)] -#map = PowerMap() +# map = PowerMap() -#plt.plot(t,np.transpose(map.max_power)) -#plt.ylabel('Max Power') -#plt.xlabel('°C') -#plt.show() +# plt.plot(t,np.transpose(map.max_power)) +# plt.ylabel('Max Power') +# plt.xlabel('°C') +# plt.show() -#kennfeld = {'known_x': map.known_x, +# kennfeld = {'known_x': map.known_x, # 'known_y': map.known_y, # 'known_t': map.known_t} -#with open("sample1.json", "w") as outfile: +# with open("sample1.json", "w") as outfile: # outfile.write(kennfeld) - -#with open("sample2.json", "w") as outfile: +# with open("sample2.json", "w") as outfile: # json.dump(kennfeld, outfile) -#with open('sample2.json', 'r') as openfile: - - # Reading from json file - #json_object = json.load(openfile) - -#map.known_x = json_object['known_x'] -#map.known_y = json_object['known_y'] -#map.known_t = json_object['known_t'] +# with open('sample2.json', 'r') as openfile: -#print(map.known_x) -#print(map.known_y) -#print(map.known_t) +# Reading from json file +# json_object = json.load(openfile) +# map.known_x = json_object['known_x'] +# map.known_y = json_object['known_y'] +# map.known_t = json_object['known_t'] +# print(map.known_x) +# print(map.known_y) +# print(map.known_t) diff --git a/custom_components/weishaupt_modbus/manifest.json b/custom_components/weishaupt_modbus/manifest.json index 322d9b9..c0531f7 100644 --- a/custom_components/weishaupt_modbus/manifest.json +++ b/custom_components/weishaupt_modbus/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://github.com/OStrama/weishaupt_modbus/", "iot_class": "local_polling", "issue_tracker": "https://github.com/OStrama/weishaupt_modbus/issues", - "requirements": [], - "version": "0.0.6" + "requirements": ["pymodbus"], + "version": "0.0.8" } diff --git a/custom_components/weishaupt_modbus/modbusobject.py b/custom_components/weishaupt_modbus/modbusobject.py index fac44f3..4654e47 100644 --- a/custom_components/weishaupt_modbus/modbusobject.py +++ b/custom_components/weishaupt_modbus/modbusobject.py @@ -1,6 +1,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT -from pymodbus.client import AsyncModbusTcpClient as AsyncModbusTcpClient +from pymodbus.client import AsyncModbusTcpClient from .const import FORMATS, TYPES import warnings @@ -22,13 +22,12 @@ 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 - async def connect(self, modbus_item): + async def connect(self): try: self._ModbusClient = AsyncModbusTcpClient(host=self._ip, port=self._port) await self._ModbusClient.connect() @@ -55,19 +54,18 @@ async def value(self): self._ModbusItem.address, slave=1 ) ).registers[0] - except: # noqa: E722 return None - @value.setter - async def value(self, value) -> None: + # @value.setter + async def setvalue(self, value) -> None: try: match self._ModbusItem.type: case TYPES.SENSOR | TYPES.NUMBER_RO | TYPES.SENSOR_CALC: # Sensor entities are read-only return case _: - self.connect() + await self.connect() await self._ModbusClient.write_register( self._ModbusItem.address, int(value), slave=1 )