From 61131d2cdf0d14c3bfb049b19d4a24e9ba2c6a0f Mon Sep 17 00:00:00 2001 From: MadOne Date: Tue, 22 Oct 2024 18:11:40 +0000 Subject: [PATCH] import --- .../weishaupt_modbus/__init__.py | 35 + .../weishaupt_modbus/config_flow.py | 80 ++ .../weishaupt_modbus/const.py | 51 + .../weishaupt_modbus/entities.py | 279 ++++++ .../weishaupt_modbus/hpconst.py | 896 ++++++++++++++++++ .../weishaupt_modbus/items.py | 102 ++ .../weishaupt_modbus/kennfeld.py | 165 ++++ .../weishaupt_modbus/manifest.json | 12 + .../weishaupt_modbus/modbusobject.py | 71 ++ .../weishaupt_modbus/number.py | 27 + .../weishaupt_modbus/select.py | 21 + .../weishaupt_modbus/sensor.py | 30 + .../weishaupt_modbus/strings.json | 32 + .../weishaupt_modbus/switch.py | 93 ++ .../weishaupt_modbus/translations/en.json | 32 + 15 files changed, 1926 insertions(+) create mode 100644 custom_components/custom_components/weishaupt_modbus/__init__.py create mode 100644 custom_components/custom_components/weishaupt_modbus/config_flow.py create mode 100644 custom_components/custom_components/weishaupt_modbus/const.py create mode 100644 custom_components/custom_components/weishaupt_modbus/entities.py create mode 100644 custom_components/custom_components/weishaupt_modbus/hpconst.py create mode 100644 custom_components/custom_components/weishaupt_modbus/items.py create mode 100644 custom_components/custom_components/weishaupt_modbus/kennfeld.py create mode 100644 custom_components/custom_components/weishaupt_modbus/manifest.json create mode 100644 custom_components/custom_components/weishaupt_modbus/modbusobject.py create mode 100644 custom_components/custom_components/weishaupt_modbus/number.py create mode 100644 custom_components/custom_components/weishaupt_modbus/select.py create mode 100644 custom_components/custom_components/weishaupt_modbus/sensor.py create mode 100644 custom_components/custom_components/weishaupt_modbus/strings.json create mode 100644 custom_components/custom_components/weishaupt_modbus/switch.py create mode 100644 custom_components/custom_components/weishaupt_modbus/translations/en.json diff --git a/custom_components/custom_components/weishaupt_modbus/__init__.py b/custom_components/custom_components/weishaupt_modbus/__init__.py new file mode 100644 index 0000000..46ea264 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/__init__.py @@ -0,0 +1,35 @@ +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import CONST + +PLATFORMS: list[str] = [ + "number", + "select", + "sensor", + # "switch", +] + + +# Return boolean to indicate that initialization was successful. +# return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + # Store an instance of the "connecting" class that does the work of speaking + # with your actual devices. + # hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub.Hub(hass, entry.data["host"]) + + # This creates each HA object for each platform your device requires. + # It's done by calling the `async_setup_entry` function in each platform module. + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + # This is called when an entry/configured device is to be removed. The class + # needs to unload itself, and remove callbacks. See the classes for further + # details + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[CONST.DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/custom_components/custom_components/weishaupt_modbus/config_flow.py b/custom_components/custom_components/weishaupt_modbus/config_flow.py new file mode 100644 index 0000000..3866969 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/config_flow.py @@ -0,0 +1,80 @@ +from typing import Any +import voluptuous as vol +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv + +# from . import wp +from .const import CONST + +# DATA_SCHEMA = vol.Schema({("host"): str, ("port"): cv.port}) +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): str, vol.Optional(CONF_PORT, default="502"): cv.port} +) + + +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: + # Validate the data can be used to set up a connection. + + # This is a simple example to show an error in the UI for a short hostname + # The exceptions are defined at the end of this file, and are used in the + # `async_step_user` method below. + if len(data["host"]) < 3: + raise InvalidHost + + # If your PyPI package is not built with async, pass your methods + # to the executor: + # await hass.async_add_executor_job( + # your_validate_func, data["username"], data["password"] + # ) + + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # 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. + # See `async_step_user` below for how this is used + return {"title": data["host"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=CONST.DOMAIN): + VERSION = 1 + # Pick one of the available connection classes in homeassistant/config_entries.py + # This tells HA if it should be asking for updates, or it'll be notified of updates + # automatically. This example uses PUSH, as the dummy hub will notify HA of + # changes. + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + # This goes through the steps to take the user through the setup process. + # Using this it is possible to update the UI and prompt for additional + # information. This example provides a single form (built from `DATA_SCHEMA`), + # and when that has some validated input, it calls `async_create_entry` to + # actually create the HA config entry. Note the "title" value is returned by + # `validate_input` above. + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + + except Exception: # noqa: BLE001 + errors["base"] = "unknown" + + # If there is no user input or there were errors, show the form again, including any errors that were found with the input. + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate there is an invalid hostname.""" + + +class ConnectionFailed(exceptions.HomeAssistantError): + """Error to indicate there is an invalid hostname.""" diff --git a/custom_components/custom_components/weishaupt_modbus/const.py b/custom_components/custom_components/weishaupt_modbus/const.py new file mode 100644 index 0000000..c72ccb3 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/const.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass +from datetime import timedelta +from homeassistant.const import ( + UnitOfEnergy, + UnitOfTemperature, + UnitOfTime, + UnitOfVolumeFlowRate, + UnitOfPower, + PERCENTAGE, +) + + +@dataclass(frozen=True) +class MainConstants: + DOMAIN = "weishaupt_wbb" + SCAN_INTERVAL = timedelta(minutes=1) + UNIQUE_ID = "unique_id" + APPID = 100 + KENNFELDFILE = "weishaupt_wbb_kennfeld.json" + + +CONST = MainConstants() + + +@dataclass(frozen=True) +class FormatConstants: + TEMPERATUR = UnitOfTemperature.CELSIUS + ENERGY = UnitOfEnergy.KILO_WATT_HOUR + POWER = UnitOfPower.WATT + PERCENTAGE = PERCENTAGE + NUMBER = "" + STATUS = "Status" + VOLUMENSTROM = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR + KENNLINIE = "Stg." + TIME_MIN = UnitOfTime.MINUTES + TIME_H = UnitOfTime.HOURS + + +FORMATS = FormatConstants() + + +@dataclass(frozen=True) +class TypeConstants: + SENSOR = "Sensor" + SENSOR_CALC = "Sensor_Calc" + SELECT = "Select" + NUMBER = "Number" + NUMBER_RO = "Number_RO" + + +TYPES = TypeConstants() diff --git a/custom_components/custom_components/weishaupt_modbus/entities.py b/custom_components/custom_components/weishaupt_modbus/entities.py new file mode 100644 index 0000000..378ad8e --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/entities.py @@ -0,0 +1,279 @@ +import warnings +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.components.select import SelectEntity +from homeassistant.components.number import NumberEntity +from homeassistant.const import UnitOfEnergy, UnitOfTemperature +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() + # type of list is defined by the ModbusItem's type flag + # so the app only holds one list of entities that is build from a list of ModbusItem + # stored in hpconst.py so far, will be provided by an external file in future + for index, item in enumerate(modbusitems): + if item.type == type: + match 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: + entries.append(MyNumberEntity(config_entry, modbusitems[index])) + + return entries + + +class MyEntity: + # The base class for entities that hold general parameters + _config_entry = None + _modbus_item = None + _divider = 1 + _attr_name = "" + _attr_unique_id = "" + _attr_should_poll = True + _dev_device = "" + + def __init__(self, config_entry, modbus_item) -> None: + self._config_entry = config_entry + self._modbus_item = modbus_item + self._attr_name = self._modbus_item.name + self._attr_unique_id = CONST.DOMAIN + self._attr_name + self._dev_device = self._modbus_item.device + + if self._modbus_item._format != FORMATS.STATUS: + self._attr_native_unit_of_measurement = self._modbus_item._format + + if self._modbus_item._format == FORMATS.ENERGY: + 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") + self._attr_native_max_value = self._modbus_item.getNumberFromText("max") + self._attr_native_step = self._modbus_item.getNumberFromText("step") + self._divider = self._modbus_item.getNumberFromText("divider") + self._attr_device_class = self._modbus_item.getTextFromNumber(-1) + + def calcTemperature(self, val: float): + if val == None: + return None + if val == -32768: + return -1 + if val == -32767: + return -2 + if val == 32768: + return None + return val / self._divider + + def calcPercentage(self, val: float): + if val == None: + return None + if val == 65535: + return None + return val / self._divider + + @property + async def translateVal(self): + # reads an translates a value from the modbus + mbo = ModbusObject(self._config_entry, self._modbus_item) + if mbo == None: + return None + val = await mbo.value + match self._modbus_item.format: + case FORMATS.TEMPERATUR: + return self.calcTemperature(val) + case FORMATS.PERCENTAGE: + return self.calcPercentage(val) + case FORMATS.STATUS: + return self._modbus_item.getTextFromNumber(val) + case _: + if val == None: + return val + return val / self._divider + + # @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: + return + val = None + match self._modbus_item.format: + # logically, this belongs to the ModbusItem, but doing it here + case FORMATS.STATUS: + val = self._modbus_item.getNumberFromText(value) + case _: + val = value * self._divider + await mbo.setvalue(val) # = val + + def my_device_info(self) -> DeviceInfo: + # helper to build the device info + return { + "identifiers": {(CONST.DOMAIN, self._dev_device)}, + "name": self._dev_device, + "sw_version": "Device_SW_Version", + "model": "Device_model", + "manufacturer": "Weishaupt", + } + + +class MySensorEntity(SensorEntity, MyEntity): + # class that represents a sensor entity derived from Sensorentity + # and decorated with general parameters from MyEntity + _attr_native_unit_of_measurement = None + _attr_device_class = None + _attr_state_class = None + + def __init__(self, config_entry, modbus_item) -> None: + MyEntity.__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 = await self.translateVal + + @property + 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 = await self.translateVal + + def calcPower(self, val, x, y): + if val == None: + return val + return (val / 100) * self.my_map.map(x, y) + + @property + async def translateVal(self): + # reads an translates a value from the modbus + mbo = ModbusObject(self._config_entry, self._modbus_item) + val = self.calcPercentage(await 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) + if mbo_x == None: + return None + t_temp = await mbo_x.value + val_x = self.calcTemperature(t_temp) / 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) + if mbo_y == None: + return None + t_temp = await mbo_y.value + val_y = self.calcTemperature(t_temp) / 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 + _attr_native_unit_of_measurement = None + _attr_device_class = None + _attr_state_class = None + _attr_native_min_value = 10 + _attr_native_max_value = 60 + + def __init__(self, config_entry, modbus_item) -> None: + MyEntity.__init__(self, config_entry, modbus_item) + + if self._modbus_item.resultlist != None: + self._attr_native_min_value = self._modbus_item.getNumberFromText("min") + self._attr_native_max_value = self._modbus_item.getNumberFromText("max") + self._attr_native_step = self._modbus_item.getNumberFromText("step") + + async def async_set_native_value(self, value: float) -> None: + await self.settranslateVal(value) + self._attr_native_value = await self.translateVal + self.async_write_ha_state() + + async def async_update(self) -> None: + # the synching is done by the ModbusObject of the entity + self._attr_native_value = await self.translateVal + + @property + def device_info(self) -> DeviceInfo: + return MyEntity.my_device_info(self) + + +class MySelectEntity(SelectEntity, MyEntity): + # class that represents a sensor entity derived from Sensorentity + # and decorated with general parameters from MyEntity + options = [] + _attr_current_option = "FEHLER" + + def __init__(self, config_entry, modbus_item) -> None: + MyEntity.__init__(self, config_entry, modbus_item) + self.async_internal_will_remove_from_hass_port = self._config_entry.data[ + CONF_PORT + ] + # option list build from the status list of the ModbusItem + self.options = [] + for index, item in enumerate(self._modbus_item._resultlist): + self.options.append(item.text) + + async def async_select_option(self, option: str) -> None: + # the synching is done by the ModbusObject of the entity + await self.settranslateVal(option) + self._attr_current_option = await self.translateVal + self.async_write_ha_state() + + async def async_update(self) -> None: + # the synching is done by the ModbusObject of the entity + # await self.coordinator.async_request_refresh() + self._attr_current_option = await self.translateVal + + @property + def device_info(self) -> DeviceInfo: + return MyEntity.my_device_info(self) diff --git a/custom_components/custom_components/weishaupt_modbus/hpconst.py b/custom_components/custom_components/weishaupt_modbus/hpconst.py new file mode 100644 index 0000000..8b8ec83 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/hpconst.py @@ -0,0 +1,896 @@ +from dataclasses import dataclass +from .const import TYPES, FORMATS +from .items import ModbusItem, StatusItem +from homeassistant.components.sensor import SensorDeviceClass + + +@dataclass(frozen=True) +class DeviceConstants: + SYS = "System" + WP = "Wärmepumpe" + WW = "Warmwasser" + HZ = "Heizkreis" + W2 = "2. Wärmeerzeuger" + ST = "Statistik" + + +DEVICES = DeviceConstants() + +############################################################################################################################## +# Listen mit Fehlermeldungen, Warnmeldungen und Statustexte +# Beschreibungstext ist ebenfalls m�glich +# class StatusItem(): def __init__(self, number, text, description = None): +############################################################################################################################## + +SYS_FEHLER = [ + StatusItem(65535, "kein Fehler"), +] + +SYS_WARNUNG = [ + StatusItem(65535, "keine Warnung"), + StatusItem(32, "Fehler Kältesatz (32)"), +] + +SYS_FEHLERFREI = [ + StatusItem(0, "Fehler aktiv"), + StatusItem(1, "Störungsfreier Betrieb"), +] + +SYS_BETRIEBSANZEIGE = [ + StatusItem(0, "undefiniert"), + StatusItem(1, "Relaistest"), + StatusItem(2, "Notaus"), + StatusItem(3, "Diagnose"), + StatusItem(4, "Handbetrieb"), + StatusItem(5, "Handbetrieb Heizen"), + StatusItem(6, "Handbetrieb Kühlen"), + StatusItem(7, "Manueller Abtaubetrieb"), + StatusItem(8, "Abtauen"), + StatusItem(9, "2. WEZ"), + StatusItem(10, "EVU_SPERRE"), + StatusItem(11, "SG Tarif"), + StatusItem(12, "SG Maximal"), + StatusItem(13, "Tarifladung"), + StatusItem(14, "Erhöhter Betrieb"), + StatusItem(15, "Standzeit"), + StatusItem(16, "Standby"), + StatusItem(17, "Spülen"), + StatusItem(18, "Frostschutz"), + StatusItem(19, "Heizbetrieb"), + StatusItem(20, "Warmwasserbetrieb"), + StatusItem(21, "Legionellenschutz"), + StatusItem(22, "Umschaltung HZ KU"), + StatusItem(23, "Kühlbetrieb"), + StatusItem(24, "Passive Kühlung"), + StatusItem(25, "Sommerbetrieb"), + StatusItem(26, "Schwimmbadbetrieb"), + StatusItem(27, "Urlaub"), + StatusItem(28, "Estrichprogramm"), + StatusItem(29, "Gesperrt"), + StatusItem(30, "Sperre AT"), + StatusItem(31, "Sperre Sommer"), + StatusItem(32, "Sperre Winter"), + StatusItem(33, "Einsatzgrenze"), + StatusItem(34, "HK Sperre"), + StatusItem(35, "Absenkbetrieb"), + StatusItem(43, "Ölrückführung"), +] + +SYS_BETRIEBSART = [ + StatusItem(0, "Automatik"), + StatusItem(1, "Heizen"), + StatusItem(2, "Kühlen"), + StatusItem(3, "Sommer"), + StatusItem(4, "Standby"), + StatusItem(5, "2.WEZ"), +] + +HP_BETRIEB = [ + StatusItem(0, "Undefiniert"), + StatusItem(1, "Relaistest"), + StatusItem(2, "Notaus"), + StatusItem(3, "Diagnose"), + StatusItem(4, "Handbetrieb"), + StatusItem(5, "Handbetrieb Heizen"), + StatusItem(6, "Handbetrieb Kühlen"), + StatusItem(7, "Manueller Abtaubetrieb"), + StatusItem(8, "Abtauen"), + StatusItem(9, "WEZ2"), + StatusItem(10, "EVU_SPERRE"), + StatusItem(11, "SG Tarif"), + StatusItem(12, "SG Maximal"), + StatusItem(13, "Tarifladung"), + StatusItem(14, "Erhöhter Betrieb"), + StatusItem(15, "Standzeit"), + StatusItem(16, "Standbybetrieb"), + StatusItem(17, "Spülbetrieb"), + StatusItem(18, "Frostschutz"), + StatusItem(19, "Heizbetrieb"), + StatusItem(20, "Warmwasserbetrieb"), + StatusItem(21, "Legionellenschutz"), + StatusItem(22, "Umschaltung HZ KU"), + StatusItem(23, "Kühlbetrieb"), + StatusItem(24, "Passive Kühlung"), + StatusItem(25, "Sommerbetrieb"), + StatusItem(26, "Schwimmbad"), + StatusItem(27, "Urlaub"), + StatusItem(28, "Estrich"), + StatusItem(29, "Gesperrt"), + StatusItem(30, "Sperre AT"), + StatusItem(31, "Sperre Sommer"), + StatusItem(32, "Sperre Winter"), + StatusItem(33, "Einsatzgrenze"), + StatusItem(34, "HK Sperre"), + StatusItem(35, "Absenk"), + StatusItem(43, "Ölrückführung"), +] + +HP_STOERMELDUNG = [ + StatusItem(0, "Störung"), + StatusItem(1, "Störungsfrei"), +] + +HP_RUHEMODUS = [ + StatusItem(0, "aus"), + StatusItem(1, "80 %"), + StatusItem(2, "60 %"), + StatusItem(3, "40 %"), +] + +HZ_KONFIGURATION = [ + StatusItem(0, "aus"), + StatusItem(1, "Pumpenkreis"), + StatusItem(2, "Mischkreis"), + StatusItem(3, "Sollwert (Pumpe M1)"), +] + +HZ_ANFORDERUNG = [ + StatusItem(0, "aus"), + StatusItem(1, "witterungsgeführt"), + StatusItem(2, "konstant"), +] + +HZ_BETRIEBSART = [ + StatusItem(0, "Automatik"), + StatusItem(1, "Komfort"), + StatusItem(2, "Normal"), + StatusItem(3, "Absenkbetrieb"), + StatusItem(4, "Standby"), +] + +HZ_PARTY_PAUSE = [ + StatusItem(1, "Pause 12.0h"), + StatusItem(2, "Pause 11.5h"), + StatusItem(3, "Pause 11.0h"), + StatusItem(4, "Pause 10.5h"), + StatusItem(5, "Pause 10.0h"), + StatusItem(6, "Pause 9.5h"), + StatusItem(7, "Pause 9.0h"), + StatusItem(8, "Pause 8.5h"), + StatusItem(9, "Pause 8.0h"), + StatusItem(10, "Pause 7.5h"), + StatusItem(11, "Pause 7.0h"), + StatusItem(12, "Pause 6.5h"), + StatusItem(13, "Pause 6.0h"), + StatusItem(14, "Pause 5.5h"), + StatusItem(15, "Pause 5.0h"), + StatusItem(16, "Pause 4.5h"), + StatusItem(17, "Pause 4.0h"), + StatusItem(18, "Pause 3.5h"), + StatusItem(19, "Pause 3.0h"), + StatusItem(20, "Pause 2.5h"), + StatusItem(21, "Pause 2.0h"), + StatusItem(22, "Pause 1.5h"), + StatusItem(23, "Pause 1.0h"), + StatusItem(24, "Pause 0.5h"), + StatusItem(25, "Automatik"), + StatusItem(26, "Party 0.5h"), + StatusItem(27, "Party 1.0h"), + StatusItem(28, "Party 1.5h"), + StatusItem(29, "Party 2.0h"), + StatusItem(30, "Party 2.5h"), + StatusItem(31, "Party 3.0h"), + StatusItem(32, "Party 3.5h"), + StatusItem(33, "Party 4.0h"), + StatusItem(34, "Party 4.5h"), + StatusItem(35, "Party 5.0h"), + StatusItem(36, "Party 5.5h"), + StatusItem(37, "Party 6.0h"), + StatusItem(38, "Party 6.5h"), + StatusItem(39, "Party 7.0h"), + StatusItem(40, "Party 7.5h"), + StatusItem(41, "Party 8.0h"), + StatusItem(42, "Party 8.5h"), + StatusItem(43, "Party 9.0h"), + StatusItem(44, "Party 9.5h"), + StatusItem(45, "Party 10.0h"), + StatusItem(46, "Party 10.5h"), + StatusItem(47, "Party 11.0h"), + StatusItem(48, "Party 11.5h"), + StatusItem(49, "Party 12.0h"), +] + +WW_KONFIGURATION = [ + StatusItem(0, "aus"), + StatusItem(1, "Umlenkventil"), + StatusItem(2, "Pumpe"), +] + +W2_STATUS = [ + StatusItem(0, "aus"), + StatusItem(1, "ein"), +] + +W2_KONFIG = [ + StatusItem(0, "0"), + StatusItem(1, "1"), +] + +##################################################### +# Description of physical units via the status list # +##################################################### + +RANGE_PERCENTAGE = [ + StatusItem(0, "min"), + StatusItem(100, "max"), + StatusItem(1, "step"), + StatusItem(1, "divider"), +] + +TEMPRANGE_ROOM = [ + StatusItem(16, "min"), + StatusItem(30, "max"), + StatusItem(0.5, "step"), + StatusItem(10, "divider"), + StatusItem(-1, SensorDeviceClass.TEMPERATURE), +] + +TEMPRANGE_WATER = [ + StatusItem(30, "min"), + StatusItem(60, "max"), + StatusItem(0.5, "step"), + StatusItem(10, "divider"), + StatusItem(-1, SensorDeviceClass.TEMPERATURE), +] + +TEMPRANGE_SGREADY = [ + StatusItem(0, "min"), + StatusItem(10, "max"), + StatusItem(0.5, "step"), + StatusItem(10, "divider"), + StatusItem(-1, SensorDeviceClass.TEMPERATURE), +] + +TEMPRANGE_BIVALENZ = [ + StatusItem(-20, "min"), + StatusItem(10, "max"), + StatusItem(0.5, "step"), + StatusItem(10, "divider"), + StatusItem(-1, SensorDeviceClass.TEMPERATURE), +] + +TEMPRANGE_STD = [ + StatusItem(-60, "min"), + StatusItem(100, "max"), + StatusItem(0.5, "step"), + StatusItem(10, "divider"), + StatusItem(-1, SensorDeviceClass.TEMPERATURE), +] + + +RANGE_HZKENNLINIE = [ + StatusItem(0, "min"), + StatusItem(3, "max"), + StatusItem(0.05, "step"), + StatusItem(100, "divider"), +] + +TIMERANGE_WWPUSH = [ + StatusItem(0, "min"), + StatusItem(240, "max"), + StatusItem(5, "step"), + StatusItem(1, "divider"), +] + +RANGE_FLOWRATE = [ + StatusItem(0, "min"), + StatusItem(3, "max"), + StatusItem(0.1, "step"), + StatusItem(100, "divider"), +] + +RANGE_ENERGY = [ + StatusItem(-1, SensorDeviceClass.ENERGY), + StatusItem(1, "divider"), +] + +RANGE_CALCPOWER = [ + StatusItem(-1, SensorDeviceClass.POWER), + StatusItem(1, "divider"), + StatusItem(30002, "x"), + StatusItem(33104, "y"), +] + +############################################################################################################################## +# Modbus Register List: # +# https://docs.google.com/spreadsheets/d/1EZ3QgyB41xaXo4B5CfZe0Pi8KPwzIGzK/edit?gid=1730751621#gid=1730751621 # +############################################################################################################################## + +MODBUS_SYS_ITEMS = [ + ModbusItem( + 30001, + "Aussentemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.SYS, + TEMPRANGE_STD, + ), + ModbusItem( + 30002, + "Luftansaugtemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.SYS, + TEMPRANGE_STD, + ), + ModbusItem(30003, "Fehler", FORMATS.STATUS, TYPES.SENSOR, DEVICES.SYS, SYS_FEHLER), + ModbusItem( + 30004, "Warnung", FORMATS.STATUS, TYPES.SENSOR, DEVICES.SYS, SYS_WARNUNG + ), + ModbusItem( + 30005, "Fehlerfrei", FORMATS.STATUS, TYPES.SENSOR, DEVICES.SYS, SYS_FEHLERFREI + ), + ModbusItem( + 30006, + "Betriebsanzeige", + FORMATS.STATUS, + TYPES.SENSOR, + DEVICES.SYS, + SYS_BETRIEBSANZEIGE, + ), + ModbusItem( + 40001, + "Systembetriebsart", + FORMATS.STATUS, + TYPES.SELECT, + DEVICES.SYS, + SYS_BETRIEBSART, + ), + 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), + ModbusItem( + 43102, "Ruhemodus", FORMATS.STATUS, TYPES.NUMBER_RO, DEVICES.WP, HP_RUHEMODUS + ), + ModbusItem( + 43103, "Pumpe Einschaltart", FORMATS.NUMBER, TYPES.NUMBER_RO, DEVICES.WP + ), + ModbusItem( + 43104, + "Pumpe Leistung Heizen", + FORMATS.PERCENTAGE, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_PERCENTAGE, + ), + ModbusItem( + 43105, + "Pumpe Leistung Kühlen", + FORMATS.PERCENTAGE, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_PERCENTAGE, + ), + ModbusItem( + 43106, + "Pumpe Leistung Warmwasser", + FORMATS.PERCENTAGE, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_PERCENTAGE, + ), + ModbusItem( + 43107, + "Pumpe Leistung Abtaubetrieb", + FORMATS.PERCENTAGE, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_PERCENTAGE, + ), + ModbusItem( + 43108, + "Volumenstrom Heizen", + FORMATS.VOLUMENSTROM, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_FLOWRATE, + ), + ModbusItem( + 43109, + "Volumenstrom Kühlen", + FORMATS.VOLUMENSTROM, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_FLOWRATE, + ), + ModbusItem( + 43110, + "Volumenstrom Warmwasser", + FORMATS.VOLUMENSTROM, + TYPES.NUMBER_RO, + DEVICES.WP, + RANGE_FLOWRATE, + ), + ModbusItem( + 31101, + "Raumsolltemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 31102, + "Raumtemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem(31103, "Raumfeuchte", FORMATS.PERCENTAGE, TYPES.SENSOR, DEVICES.HZ), + ModbusItem( + 31104, + "Vorlaufsolltemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.HZ, + TEMPRANGE_STD, + ), + ModbusItem( + 31105, + "HZ_Vorlauftemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.HZ, + TEMPRANGE_STD, + ), + ModbusItem( + 41101, + "HZ_Konfiguration", + FORMATS.STATUS, + TYPES.NUMBER_RO, + DEVICES.HZ, + HZ_KONFIGURATION, + ), + ModbusItem( + 41102, + "Anforderung Typ", + FORMATS.STATUS, + TYPES.NUMBER_RO, + DEVICES.HZ, + HZ_ANFORDERUNG, + ), + ModbusItem( + 41103, "Betriebsart", FORMATS.STATUS, TYPES.SELECT, DEVICES.HZ, HZ_BETRIEBSART + ), + ModbusItem( + 41104, "Pause / Party", FORMATS.STATUS, TYPES.SELECT, DEVICES.HZ, HZ_PARTY_PAUSE + ), + ModbusItem( + 41105, + "Raumsolltemperatur Komfort", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 41106, + "Raumsolltemperatur Normal", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 41107, + "Raumsolltemperatur Absenk", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 41108, + "Heizkennlinie", + FORMATS.KENNLINIE, + TYPES.NUMBER, + DEVICES.HZ, + RANGE_HZKENNLINIE, + ), + ModbusItem( + 41109, + "Sommer Winter Umschaltung", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 41110, + "Heizen Konstanttemperatur", + FORMATS.TEMPERATUR, + TYPES.NUMBER_RO, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 41111, + "Heizen Konstanttemp Absenk", + FORMATS.TEMPERATUR, + TYPES.NUMBER_RO, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 41112, + "Kühlen Konstanttemperatur", + FORMATS.TEMPERATUR, + TYPES.NUMBER_RO, + DEVICES.HZ, + TEMPRANGE_ROOM, + ), + ModbusItem( + 32101, + "Warmwassersolltemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.WW, + TEMPRANGE_WATER, + ), + ModbusItem( + 32102, + "Warmwassertemperatur", + FORMATS.TEMPERATUR, + TYPES.SENSOR, + DEVICES.WW, + TEMPRANGE_WATER, + ), + ModbusItem( + 42101, + "WW_Konfiguration", + FORMATS.STATUS, + TYPES.NUMBER_RO, + DEVICES.WW, + WW_KONFIGURATION, + ), + ModbusItem( + 42102, + "Warmwasser Push", + FORMATS.TIME_MIN, + TYPES.NUMBER, + DEVICES.WW, + TIMERANGE_WWPUSH, + ), + ModbusItem( + 42103, + "Warmwasser Normal", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.WW, + TEMPRANGE_WATER, + ), + ModbusItem( + 42104, + "Warmwasser Absenk", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.WW, + TEMPRANGE_WATER, + ), + ModbusItem( + 42105, + "SG Ready Anhebung", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.WW, + TEMPRANGE_SGREADY, + ), + ModbusItem( + 34101, "Status 2. WEZ", FORMATS.STATUS, TYPES.SENSOR, DEVICES.W2, W2_STATUS + ), + ModbusItem( + 34102, "Schaltspiele E-Heizung 1", FORMATS.NUMBER, TYPES.SENSOR, DEVICES.W2 + ), + ModbusItem( + 34103, "Schaltspiele E-Heizung 2", FORMATS.NUMBER, TYPES.SENSOR, DEVICES.W2 + ), + ModbusItem( + 34104, "Status E-Heizung 1", FORMATS.STATUS, TYPES.SENSOR, DEVICES.W2, W2_STATUS + ), + ModbusItem( + 34105, "Status E-Heizung 2", FORMATS.STATUS, TYPES.SENSOR, DEVICES.W2, W2_STATUS + ), + ModbusItem(34106, "Betriebsstunden E1", FORMATS.TIME_H, TYPES.SENSOR, DEVICES.W2), + ModbusItem(34107, "Betriebsstunden E2", FORMATS.TIME_H, TYPES.SENSOR, DEVICES.W2), + ModbusItem( + 44101, "W2_Konfiguration", FORMATS.STATUS, TYPES.SENSOR, DEVICES.W2, W2_KONFIG + ), + ModbusItem( + 44102, + "Grenztemperatur", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.W2, + TEMPRANGE_BIVALENZ, + ), + ModbusItem( + 44103, + "Bivalenztemperatur", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.W2, + TEMPRANGE_BIVALENZ, + ), + ModbusItem( + 44104, + "Bivalenztemperatur WW", + FORMATS.TEMPERATUR, + TYPES.NUMBER, + DEVICES.W2, + TEMPRANGE_BIVALENZ, + ), + ModbusItem( + 36101, + "Gesamt Energie heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36102, + "Gesamt Energie gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36103, + "Gesamt Energie Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36104, + "Gesamt Energie Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36201, + "Heizen Energie heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36202, + "Heizen Energie gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36203, + "Heizen Energie Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36204, + "Heizen Energie Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36301, + "Warmwasser Energie heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36302, + "Warmwasser Energie gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36303, + "Warmwasser Energie Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36304, + "Warmwasser Energie Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36401, + "Kühlen Energie heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36402, + "Kühlen Energie gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36403, + "Kühlen Energie Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36404, + "Kühlen Energie Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36501, + "Abtauen Energie heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36502, + "Abtauen Energie gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36503, + "Abtauen Energie Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36504, + "Abtauen Energie Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36601, + "Gesamt Energie II heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36602, + "Gesamt Energie II gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36603, + "Gesamt Energie II Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36604, + "Gesamt Energie II Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36701, + "Elektr. Energie heute", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36702, + "Elektr. Energie gestern", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36703, + "Elektr. Energie Monat", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), + ModbusItem( + 36704, + "Elektr. Energie Jahr", + FORMATS.ENERGY, + TYPES.SENSOR, + DEVICES.ST, + RANGE_ENERGY, + ), +] diff --git a/custom_components/custom_components/weishaupt_modbus/items.py b/custom_components/custom_components/weishaupt_modbus/items.py new file mode 100644 index 0000000..b81c735 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/items.py @@ -0,0 +1,102 @@ +from .const import TYPES + + +# An item of a status, e.g. error code and error text along with a precise description +# A class is intentionally defined here because the assignment via dictionaries would not work so elegantly in the end, +# especially when searching backwards. (At least I don't know how...) +class StatusItem: + _number = None + _text = None + _description = None + + def __init__(self, number, text, description=None): + self._number = number + self._text = text + self._description = description + + @property + def number(self): + return self._number + + @number.setter + def number(self, value) -> None: + self._number = value + + @property + def text(self): + return self._text + + @text.setter + def text(self, value) -> None: + self._text = value + + @property + def description(self): + return self._description + + @description.setter + def description(self, value) -> None: + self._description = value + + +# A Modbus item, consisting of address, name, +# format (temperature, status, ..), +# type (sensor, number, ..), +# device (System, Heatpump, ..) and +# optional result list from status items +# (number entities: status = limits? +class ModbusItem: + _address = None + _name = "empty" + _format = None + _type = TYPES.SENSOR + _resultlist = None + _device = None + + def __init__(self, address, name, format, type, device, resultlist=None): + self._address = address + self._name = name + self._format = format + self._type = type + self._device = device + self._resultlist = resultlist + + @property + def address(self): + return self._address + + @property + def name(self): + return self._name + + @property + def format(self): + return self._format + + @property + def type(self): + return self._type + + @property + def device(self): + return self._device + + @property + def resultlist(self): + return self._resultlist + + def getTextFromNumber(self, val): + if self._resultlist == None: + return None + for index, item in enumerate(self._resultlist): + if val == item.number: + return item.text + return "unbekannt <" + str(val) + ">" + + def getNumberFromText(self, val): + if self._resultlist == None: + return None + for index, item in enumerate(self._resultlist): + if val == item.text: + return item.number + return -1 diff --git a/custom_components/custom_components/weishaupt_modbus/kennfeld.py b/custom_components/custom_components/weishaupt_modbus/kennfeld.py new file mode 100644 index 0000000..ebeca48 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/kennfeld.py @@ -0,0 +1,165 @@ +# from scipy.interpolate import CubicSpline +from numpy.polynomial import Chebyshev +import numpy as np + +# import matplotlib.pyplot as plt +import json +from .const import CONST + + +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: + # 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, + } + 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"] + 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) + 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') + 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 + + return self.max_power[int(y)][int(x)] + + +# map = PowerMap() + +# plt.plot(t,np.transpose(map.max_power)) +# plt.ylabel('Max Power') +# plt.xlabel('°C') +# plt.show() + +# kennfeld = {'known_x': map.known_x, +# 'known_y': map.known_y, +# 'known_t': map.known_t} + +# with open("sample1.json", "w") as outfile: +# outfile.write(kennfeld) + + +# 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'] + +# print(map.known_x) +# print(map.known_y) +# print(map.known_t) diff --git a/custom_components/custom_components/weishaupt_modbus/manifest.json b/custom_components/custom_components/weishaupt_modbus/manifest.json new file mode 100644 index 0000000..c0531f7 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "weishaupt_wbb", + "name": "Weishaupt WBB", + "codeowners": ["@OStrama"], + "config_flow": true, + "documentation": "https://github.com/OStrama/weishaupt_modbus/", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/OStrama/weishaupt_modbus/issues", + "requirements": ["pymodbus"], + "version": "0.0.8" +} + diff --git a/custom_components/custom_components/weishaupt_modbus/modbusobject.py b/custom_components/custom_components/weishaupt_modbus/modbusobject.py new file mode 100644 index 0000000..02cceba --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/modbusobject.py @@ -0,0 +1,71 @@ +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from pymodbus.client import AsyncModbusTcpClient +from .const import FORMATS, TYPES + +# import logging +# logging.basicConfig() +# log = logging.getLogger() +# log.setLevel(logging.DEBUG) + + +# A Modbus object that contains a Modbus item and communicates with the Modbus +# it contains a ModbusClient for setting and getting Modbus register values +class ModbusObject: + _ModbusItem = None + _DataFormat = None + + _ip = None + _port = None + _ModbusClient = None + + def __init__(self, config_entry, modbus_item): + self._ModbusItem = modbus_item + + self._ip = config_entry.data[CONF_HOST] + self._port = config_entry.data[CONF_PORT] + self._ModbusClient = None + + async def connect(self): + try: + self._ModbusClient = AsyncModbusTcpClient(host=self._ip, port=self._port) + await self._ModbusClient.connect() + return self._ModbusClient.connected # noqa: TRY300 + except: # noqa: E722 + return None + + @property + async def value(self): + try: + await self.connect() + match self._ModbusItem.type: + case TYPES.SENSOR | TYPES.SENSOR_CALC: + # Sensor entities are read-only + return ( + await self._ModbusClient.read_input_registers( + self._ModbusItem.address, slave=1 + ) + ).registers[0] + case TYPES.SELECT | TYPES.NUMBER | TYPES.NUMBER_RO: + return ( + await self._ModbusClient.read_holding_registers( + self._ModbusItem.address, slave=1 + ) + ).registers[0] + except: # noqa: E722 + return 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 _: + await self.connect() + await self._ModbusClient.write_register( + self._ModbusItem.address, int(value), slave=1 + ) + except: # noqua: E722 + return None diff --git a/custom_components/custom_components/weishaupt_modbus/number.py b/custom_components/custom_components/weishaupt_modbus/number.py new file mode 100644 index 0000000..14106d6 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/number.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType + +# from time import gmtime, strftime +from .const import TYPES +from .hpconst import MODBUS_SYS_ITEMS +from .entities import BuildEntityList + + +async def async_setup_entry( + hass: HomeAssistant, + # config: ConfigType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the sensor platform.""" + Entries = [] + + async_add_entities( + BuildEntityList(Entries, config_entry, MODBUS_SYS_ITEMS, TYPES.NUMBER), + update_before_add=True, + ) diff --git a/custom_components/custom_components/weishaupt_modbus/select.py b/custom_components/custom_components/weishaupt_modbus/select.py new file mode 100644 index 0000000..da70ca8 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/select.py @@ -0,0 +1,21 @@ +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import TYPES +from .hpconst import MODBUS_SYS_ITEMS +from .entities import BuildEntityList + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Select entry setup.""" + Entries = [] + + async_add_entities( + BuildEntityList(Entries, config_entry, MODBUS_SYS_ITEMS, TYPES.SELECT), + update_before_add=True, + ) diff --git a/custom_components/custom_components/weishaupt_modbus/sensor.py b/custom_components/custom_components/weishaupt_modbus/sensor.py new file mode 100644 index 0000000..568e7d3 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/sensor.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import TYPES +from .hpconst import MODBUS_SYS_ITEMS +from .entities import BuildEntityList + + +async def async_setup_entry( + hass: HomeAssistant, + # config: ConfigType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the sensor platform.""" + 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, + ) diff --git a/custom_components/custom_components/weishaupt_modbus/strings.json b/custom_components/custom_components/weishaupt_modbus/strings.json new file mode 100644 index 0000000..85a72ff --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Account is already configured" + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Web scraping interval (default = 1800 sec)", + "api_scan_interval": "Api scan interval (default = 300 sec)", + "language": "Language (default = en)", + "mode": "Mode(default = api)" + } + } + } + } + } \ No newline at end of file diff --git a/custom_components/custom_components/weishaupt_modbus/switch.py b/custom_components/custom_components/weishaupt_modbus/switch.py new file mode 100644 index 0000000..d9daa7e --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/switch.py @@ -0,0 +1,93 @@ +"""Platform for Wattio integration testing.""" + +from __future__ import annotations + +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT # STATE_ON +from homeassistant.core import HomeAssistant + +# from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import wp +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Select entry setup.""" + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + async_add_entities( + [ + WW_SGReady(host, port), + ], + update_before_add=True, + ) + + +class WW_SGReady(SwitchEntity): + """Representation of Switch Sensor.""" + + # pylint: disable=too-many-instance-attributes + + def __init__(self, host, port) -> None: + """Initialize the sensor.""" + self._host = host + self._port = port + self._attr_state = "off" + + _attr_name = "SG-Ready" + _attr_unique_id = DOMAIN + _attr_name + _attr_should_poll = True + + @property + def is_on(self): + """Return is_on status.""" + if self._attr_state == "on": + return True + return False + + async def async_turn_on(self): + """Turn On method.""" + # self._attr_state = "on" + # whp = wp.heat_pump(self._host, self._port) + # whp.connect() + # whp.WW_SGReady = 1 + # self.async_write_ha_state() + + async def async_turn_off(self): + """Turn Off method.""" + # self._attr_state = "off" + # whp = wp.heat_pump(self._host, self._port) + # whp.connect() + # whp.WW_SGReady = 0 + # self.async_write_ha_state() + + async def async_update(self): + """Update switch.""" + whp = wp.heat_pump(self._host, self._port) + whp.connect() + if whp.WW_SGReady == 1: + self._attr_state = "on" + elif whp.WW_SGReady == 0: + self._attr_state = "off" + self._attr_state = whp.WW_SGReady + + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, "Warmwasser")}, + "name": "Wärmepumpe-Warmwasser", + "manufacturer": "Weishaupt", + } diff --git a/custom_components/custom_components/weishaupt_modbus/translations/en.json b/custom_components/custom_components/weishaupt_modbus/translations/en.json new file mode 100644 index 0000000..386cd56 --- /dev/null +++ b/custom_components/custom_components/weishaupt_modbus/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_scan_interval": "Api scan interval (default = 300 sec)", + "language": "Language (default = en)", + "mode": "Mode(default = api)", + "scan_interval": "Web scraping interval (default = 1800 sec)" + } + } + } + } +} \ No newline at end of file