diff --git a/.gitignore b/.gitignore index 15201ac..03b7136 100644 --- a/.gitignore +++ b/.gitignore @@ -1,131 +1,8 @@ # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py # pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py +.python-version # Environments .env @@ -133,39 +10,6 @@ celerybeat.pid env/ venv/ ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site # mypy .mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# PyPI configuration file -.pypirc diff --git a/README.md b/README.md index 386b626..19e4c7e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ -# hacs_froeling_lambdatronic_modbus -Home Assistant integration for fröling lambdatronic via Modbus (currently Modbus TCP only) +# Froeling Lambdatronic Modbus +Home Assistant integration for Fröling Lambdatronic heating systems via Modbus (currently supports Modbus TCP only). + +## :warning: Disclaimer :warning: +> **This integration is experimental and has not been tested over long periods.** +> It may contain missing or incorrect sensor data. +> Feel free to open an [issue](https://github.com/your-repo/issues) or contribute via a [pull request](https://github.com/your-repo/pulls). + +--- + +## :rocket: Features +With this integration, you can: +- Read real-time sensor data from your Fröling heating system. +- Monitor boiler performance and operational parameters. +- Configure heating system settings directly from Home Assistant. + +--- + +## :computer: Requirements +To communicate with the heating system, you need a Modbus-to-TCP device. +This integration has been tested with the **Waveshare RS232/RS485 to Ethernet Converter**, but other Serial-to-Ethernet adapters should work. + +### :wrench: Enabling Modbus RTU on the Boiler +To enable Modbus RTU on your Fröling boiler: + +1. Navigate to **Boiler Settings**. +2. Click the user icon and enter code `-7`. +3. Adjust the following settings: + - **Settings > General Settings > MODBUS Settings > Modbus Protokoll RTU** → `Set to 1` + - **Settings > General Settings > MODBUS Settings > Use Modbus Protokoll 2014** → `Yes` + - **Settings > General Settings > MODBUS Settings > Use COM2 as MODBUS Interface** → `Yes` + +--- + +## :hammer_and_wrench: Hardware Setup +I used a [Waveshare RS232/RS485 to Ethernet Converter](https://www.waveshare.com/rs232-485-to-eth.htm) and connected **RS232 to COM2** on the boiler. + +![Waveshare configuration](docs/image.png) + +Other Serial-to-Ethernet converters should work as well. + +--- + +## :package: Installation +1. Copy the integration files into your Home Assistant `custom_components` folder. +2. Restart Home Assistant. +3. Add the integration via the Home Assistant UI. + +--- + +## :orange_heart: Contributing +Contributions are welcome! + +1. **[Fork this repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo).** +2. Make changes within your fork. +3. **[Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).** + +I’ll do my best to review and merge contributions. \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/B1200522_ModBus Lambdatronic 3200_50-04_05-19_de.pdf b/custom_components/froeling_lambdatronic_modbus/B1200522_ModBus Lambdatronic 3200_50-04_05-19_de.pdf new file mode 100644 index 0000000..bcfc070 Binary files /dev/null and b/custom_components/froeling_lambdatronic_modbus/B1200522_ModBus Lambdatronic 3200_50-04_05-19_de.pdf differ diff --git a/custom_components/froeling_lambdatronic_modbus/__init__.py b/custom_components/froeling_lambdatronic_modbus/__init__.py new file mode 100644 index 0000000..48eae4f --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/__init__.py @@ -0,0 +1,26 @@ + + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.const import Platform + +DOMAIN = "froeling_modbus" + +async def async_setup(hass, config): + return True + +PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.BINARY_SENSOR] + +async def async_setup_entry(hass, entry): + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = entry.data + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + +async def async_unload_entry(hass, entry): + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, platform) + hass.data[DOMAIN].pop(entry.entry_id) + return True \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/binary_sensor.py b/custom_components/froeling_lambdatronic_modbus/binary_sensor.py new file mode 100644 index 0000000..9d86d9d --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/binary_sensor.py @@ -0,0 +1,124 @@ +from homeassistant.components.binary_sensor import BinarySensorEntity +from pymodbus.client.sync import ModbusTcpClient +import logging +from datetime import timedelta +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.translation import async_get_translations + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, config_entry, async_add_entities): + data = config_entry.data + translations = await async_get_translations(hass, hass.config.language, "entity") + + def create_binary_sensors(): + binary_sensors = [] + if data.get('hk01', False): + binary_sensors.extend([ + FroelingBinarySensor(hass, translations, data, "hk1_Pumpe_an_aus", 1030) + ]) + if data.get('hk02', False): + binary_sensors.extend([ + FroelingBinarySensor(hass, translations, data, "hk2_pumpe_an_aus", 1060) + ]) + if data.get('puffer01', False): + binary_sensors.extend([ + FroelingSensor(hass, translations, data, "puffer_1_pufferpumpe_an_aus", 32004), + ]) + if data.get('zirkulationspumpe', False): + binary_sensors.extend([ + FroelingSensor(hass, translations, data, "zirkulationspumpe_an_aus", 30711), + ]) + if data.get('boiler01', False): + binary_sensors.extend([ + FroelingSensor(hass, translations, data, "boiler_1_pumpe_an_aus", 31633) + ]) + return binary_sensors + + # Add initial binary sensors + binary_sensors = create_binary_sensors() + async_add_entities(binary_sensors) + update_interval = timedelta(seconds=data.get('update_interval', 60)) + for sensor in binary_sensors: + async_track_time_interval(hass, sensor.async_update, update_interval) + +class FroelingBinarySensor(BinarySensorEntity): + def __init__(self, hass, translations, data, entity_id, coil_address): + self._hass = hass + self._translations = translations + self._host = data['host'] + self._port = data['port'] + self._device_name = data['name'] + self._entity_id = entity_id + self._coil_address = coil_address + self._state = None + + @property + def unique_id(self): + return f"{self._device_name}_{self._entity_id}" + + @property + def name(self): + translated_name = self._translations.get(f"component.froeling_modbus.entity.binary_sensor.{self._entity_id}.name", self._entity_id) + return f"{self._device_name} {translated_name}" + + @property + def is_on(self): + return self._state + + async def async_update(self, _=None): + client = ModbusTcpClient(self._host, port=self._port) + if client.connect(): + try: + result = client.read_coils(self._coil_address, 1, unit=2) + if result.isError(): + _LOGGER.error("Error reading Modbus coil %s", self._coil_address) + self._state = None + else: + self._state = result.bits[0] + _LOGGER.debug("Reading Modbus coil: %s, state: %s", self._coil_address, self._state) + except Exception as e: + _LOGGER.error("Exception during Modbus communication: %s", e) + finally: + client.close() + +class FroelingSensor(BinarySensorEntity): + def __init__(self, hass, translations, data, entity_id, register): + self._hass = hass + self._translations = translations + self._host = data['host'] + self._port = data['port'] + self._device_name = data['name'] + self._entity_id = entity_id + self._register = register + self._state = None + + @property + def unique_id(self): + return f"{self._device_name}_{self._entity_id}" + + @property + def name(self): + translated_name = self._translations.get(f"component.froeling_modbus.entity.binary_sensor.{self._entity_id}.name", self._entity_id) + return f"{self._device_name} {translated_name}" + + @property + def is_on(self): + return self._state + + async def async_update(self, _=None): + client = ModbusTcpClient(self._host, port=self._port, retries=2, timeout=15) + if client.connect(): + try: + result = client.read_input_registers(self._register - 30001, 1, unit=2) + if result.isError(): + _LOGGER.error("Error reading Modbus input register %s", self._register - 30001) + self._state = None + else: + raw_value = result.registers[0] + self._state = raw_value > 0 + _LOGGER.debug("Reading Modbus input register: %s, state: %s", self._register - 30001, self._state) + except Exception as e: + _LOGGER.error("Exception during Modbus communication: %s", e) + finally: + client.close() \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/config_flow.py b/custom_components/froeling_lambdatronic_modbus/config_flow.py new file mode 100644 index 0000000..a99823e --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/config_flow.py @@ -0,0 +1,27 @@ +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.core import callback +from .const import DOMAIN + +@config_entries.HANDLERS.register(DOMAIN) +class FroelingModbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + async def async_step_user(self, user_input=None): + if user_input is not None: + return self.async_create_entry(title=user_input["name"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({ + vol.Required("name", default="Froeling"): str, + vol.Required("host"): str, + vol.Required("port", default=502): int, + vol.Required("update_interval", default=60): int, + vol.Optional("kessel", default=True): bool, + vol.Optional("boiler01", default=True): bool, + vol.Optional("hk01", default=True): bool, + vol.Optional("hk02", default=True): bool, + vol.Optional("austragung", default=True): bool, + vol.Optional("puffer01", default=True): bool, + vol.Optional("zirkulationspumpe", default=True): bool + }) + ) \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/const.py b/custom_components/froeling_lambdatronic_modbus/const.py new file mode 100644 index 0000000..dd028c1 --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/const.py @@ -0,0 +1 @@ +DOMAIN = "froeling_modbus" \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/manifest.json b/custom_components/froeling_lambdatronic_modbus/manifest.json new file mode 100644 index 0000000..1e6bb1a --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "froeling_lambdatronic_modbus", + "name": "Froeling Lambdatronic Modbus", + "version": "1.0.0", + "documentation": "https://github.com/GyroGearl00se/hacs_froeling_lambdatronic_modbus", + "issue_tracker": "https://github.com/GyroGearl00se/hacs_froeling_lambdatronic_modbus/issues", + "dependencies": [], + "codeowners": ["@GyroGearl00se"], + "requirements": ["pymodbus==2.5.3"], + "config_flow": true + } \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/number.py b/custom_components/froeling_lambdatronic_modbus/number.py new file mode 100644 index 0000000..19a7adf --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/number.py @@ -0,0 +1,131 @@ +from homeassistant.components.number import NumberEntity +from pymodbus.client.sync import ModbusTcpClient +import logging +from datetime import timedelta +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.translation import async_get_translations + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, config_entry, async_add_entities): + data = config_entry.data + translations = await async_get_translations(hass, hass.config.language, "entity") + + def create_numbers(): + numbers = [] + + if data.get('kessel', False): + numbers.extend([ + FroelingNumber(hass, translations, data, "Kessel_Solltemperatur", 40001, "°C", 2, 0, 70, 90), + FroelingNumber(hass, translations, data, "Bei_welcher_RL_Temperatur_an_der_Zirkulationsleitung_soll_die_Pumpe_ausschalten", 40601, "°C", 2, 0, 20, 120) + ]) + if data.get('hk01', False): + numbers.extend([ + FroelingNumber(hass, translations, data, "HK1_Vorlauf_Temperatur_10C_Aussentemperatur", 41032, "°C", 2, 0, 10, 110), + FroelingNumber(hass, translations, data, "HK1_Vorlauf_Temperatur_minus_10C_Aussentemperatur", 41033, "°C", 2, 0, 10, 110), + FroelingNumber(hass, translations, data, "HK1_Heizkreispumpe_ausschalten_wenn_Vorlauf_Soll_kleiner_ist_als", 41040, "°C", 2, 0, 10, 30), + FroelingNumber(hass, translations, data, "HK1_Absenkung_der_Vorlauftemperatur_im_Absenkbetrieb", 41034, "°C", 2, 0, 0, 70), + FroelingNumber(hass, translations, data, "HK1_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Heizbetrieb_einschaltet", 41037, "°C", 2, 0, -20, 50), + FroelingNumber(hass, translations, data, "HK1_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Absenkbetrieb_einschaltet", 41038, "°C", 2, 0, -20, 50), + FroelingNumber(hass, translations, data, "HK1_Frostschutztemperatur", 41039, "°C", 2, 0, 10, 20), + FroelingNumber(hass, translations, data, "HK1_Temp_am_Puffer_oben_ab_der_der_Ueberhitzungsschutz_aktiv_wird", 41048, "°C", 1, 0, 60, 120) + ]) + if data.get('hk02', False): + numbers.extend([ + FroelingNumber(hass, translations, data, "HK2_Vorlauf_Temperatur_10C_Aussentemperatur", 41062, "°C", 2, 0, 10, 110), + FroelingNumber(hass, translations, data, "HK2_Vorlauf_Temperatur_minus_10C_Aussentemperatur", 41063, "°C", 2, 0, 10, 110), + FroelingNumber(hass, translations, data, "HK2_Heizkreispumpe_ausschalten_wenn_Vorlauf_Soll_kleiner_ist_als", 41070, "°C", 2, 0, 10, 30), + FroelingNumber(hass, translations, data, "HK2_Absenkung_der_Vorlauftemperatur_im_Absenkbetrieb", 41064, "°C", 2, 0, 0, 70), + FroelingNumber(hass, translations, data, "HK2_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Heizbetrieb_einschaltet", 41067, "°C", 2, 0, -20, 50), + FroelingNumber(hass, translations, data, "HK2_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Absenkbetrieb_einschaltet", 41068, "°C", 2, 0, -20, 50), + FroelingNumber(hass, translations, data, "HK2_Frostschutztemperatur", 41069, "°C", 2, 0, -10, 20), + FroelingNumber(hass, translations, data, "HK2_Temp_am_Puffer_oben_ab_der_der_Ueberhitzungsschutz_aktiv_wird", 41079, "°C", 1, 0, 60, 120) + ]) + if data.get('boiler01', False): + numbers.extend([ + FroelingNumber(hass, translations, data, "Boiler_1_Gewuenschte_Boilertemperatur", 41632, "°C", 2, 0, 10, 100), + FroelingNumber(hass, translations, data, "Boiler_1_Nachladen_wenn_Boilertemperatur_unter", 41633, "°C", 2, 0, 1, 90) + ]) + if data.get('austragung', False): + numbers.extend([ + FroelingNumber(hass, translations, data, "Pelletlager_Restbestand", 40320, "t", 10, 1, 0, 100) + ]) + + return numbers + + numbers = create_numbers() + async_add_entities(numbers) + update_interval = timedelta(seconds=data.get('update_interval', 60)) + for number in numbers: + async_track_time_interval(hass, number.async_update, update_interval) + +class FroelingNumber(NumberEntity): + def __init__(self, hass, translations, data, entity_id, register, unit, scaling_factor, decimal_places=0, min_value=0, max_value=0): + self._hass = hass + self._translations = translations + self._host = data['host'] + self._port = data['port'] + self._device_name = data['name'] + self._entity_id = entity_id + self._register = register + self._unit = unit + self._scaling_factor = scaling_factor + self._decimal_places = decimal_places + self._min_value = min_value + self._max_value = max_value + self._value = None + + @property + def unique_id(self): + return f"{self._device_name}_{self._entity_id}" + + @property + def name(self): + translated_name = self._translations.get(f"component.froeling_modbus.entity.number.{self._entity_id}.name", self._entity_id) + return f"{self._device_name} {translated_name}" + + @property + def native_value(self): + return self._value + + @property + def native_unit_of_measurement(self): + return self._unit + + @property + def native_min_value(self): + return self._min_value + + @property + def native_max_value(self): + return self._max_value + + async def async_set_native_value(self, value): + client = ModbusTcpClient(self._host, port=self._port) + if client.connect(): + try: + scaled_value = int(value * self._scaling_factor) + client.write_register(self._register - 40001, scaled_value, unit=2) + self._value = value + except Exception as e: + _LOGGER.error("Exception during Modbus communication: %s", e) + finally: + client.close() + + async def async_update(self, _=None): + client = ModbusTcpClient(self._host, port=self._port) + if client.connect(): + try: + result = client.read_holding_registers(self._register - 40001, 1, unit=2) + if result.isError(): + _LOGGER.error("Error reading Modbus holding register %s", self._register - 40001) + self._value = None + else: + raw_value = result.registers[0] + _LOGGER.debug("Error reading Modbus holding register %s", self._register - 40001) + self._value = round(raw_value / self._scaling_factor, self._decimal_places) + _LOGGER.debug("processed Modbus holding register %s: raw_value=%s, _value=%s", self._register - 40001, raw_value, self._value) + except Exception as e: + _LOGGER.error("Exception during Modbus communication: %s", e) + finally: + client.close() diff --git a/custom_components/froeling_lambdatronic_modbus/sensor.py b/custom_components/froeling_lambdatronic_modbus/sensor.py new file mode 100644 index 0000000..7230c65 --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/sensor.py @@ -0,0 +1,289 @@ +from homeassistant.components.sensor import SensorEntity +from pymodbus.client.sync import ModbusTcpClient +import logging +from datetime import timedelta +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.translation import async_get_translations + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, config_entry, async_add_entities): + data = config_entry.data + + translations = await async_get_translations(hass, hass.config.language, "entity") + def create_text_sensors(): + text_sensors = [] + text_sensors.extend([ + FroelingTextSensor(hass, translations, data, "Anlagenzustand", 34001, ANLAGENZUSTAND_MAPPING), + FroelingTextSensor(hass, translations, data, "Kesselzustand", 34002, KESSELZUSTAND_MAPPING) + ]) + return text_sensors + + def create_sensors(): + sensors = [] + sensors.extend([ + FroelingSensor(hass, translations, data, "Aussentemperatur", 31001, "°C", 2, 0, device_class="temperature"), + ]) + if data.get('kessel', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "Kesseltemperatur", 30001, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Abgastemperatur", 30002, "°C", 1, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Verbleibende_Heizstunden_bis_zur_Asche_entleeren_Warnung", 30087, "h", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Saugzug_Ansteuerung", 30013, "%", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Saugzugdrehzahl", 30007, "Upm", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Sauerstoffregler", 30017, "%", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Restsauerstoffgehalt", 30004, "%", 10, 1, device_class="none"), + FroelingSensor(hass, translations, data, "Ruecklauffuehler", 30010, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Primaerluft", 30012, "%", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Sekundaerluft", 30014, "%", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Betriebsstunden", 30021, "h", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Stunden_seit_letzter_Wartung", 30056, "h", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Betriebsstunden_in_der_Feuererhaltung", 30025, "h", 1, 0, device_class="none") + ]) + if data.get('hk01', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "HK01_Vorlauf_Isttemperatur", 31031, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "HK01_Vorlauf_Solltemperatur", 31032, "°C", 2, 0, device_class="temperature") + ]) + if data.get('hk02', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "HK02_Vorlauf_Isttemperatur", 31061, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "HK02_Vorlauf_Solltemperatur", 31062, "°C", 2, 0, device_class="temperature"), + ]) + if data.get('puffer01', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "Puffer_1_Temperatur_oben", 32001, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Puffer_1_Temperatur_mitte", 32002, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Puffer_1_Temperatur_unten", 32003, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Puffer_1_Pufferpumpen_Ansteuerung", 32004, "%", 1, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Puffer_1_Ladezustand", 32007, "%", 1, 0, device_class="none") + ]) + if data.get('boiler01', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "Boiler_1_Temperatur_oben", 31631, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Boiler_1_Pumpe_Ansteuerung", 31633, "%", 1, 0, device_class="none") + ]) + if data.get('austragung', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "Fuellstand_im_Pelletsbehaelter", 30022, "%", 207, 1, device_class="none"), + FroelingSensor(hass, translations, data, "Resetierbarer_kg_Zaehler", 30082, "kg", 1, 0, device_class="weight"), + FroelingSensor(hass, translations, data, "Resetierbarer_t_Zaehler", 30083, "t", 1, 0, device_class="weight"), + FroelingSensor(hass, translations, data, "Pelletverbrauch_Gesamt", 30084, "t", 10, 0, device_class="weight"), + ]) + if data.get('zirkulationspumpe', False): + sensors.extend([ + FroelingSensor(hass, translations, data, "Ruecklauftemperatur_an_der_Zirkulations_Leitung", 30712, "°C", 2, 0, device_class="temperature"), + FroelingSensor(hass, translations, data, "Stoemungsschalter_an_der_Brauchwasser_Leitung", 30601, "", 2, 0, device_class="none"), + FroelingSensor(hass, translations, data, "Drehzahl_der_Zirkulations_Pumpe", 30711, "%", 1, 0, device_class="none"), + ]) + return sensors + + text_sensors = create_text_sensors() + async_add_entities(text_sensors) + + sensors = create_sensors() + async_add_entities(sensors) + + update_interval = timedelta(seconds=data.get('update_interval', 60)) + for sensor in sensors: + async_track_time_interval(hass, sensor.async_update, update_interval) + for sensor in text_sensors: + async_track_time_interval(hass, sensor.async_update_text_sensor, update_interval) + +class FroelingSensor(SensorEntity): + def __init__(self, hass, translations, data, entity_id, register, unit, scaling_factor, decimal_places=0, device_class=None): + self._hass = hass + self._translations = translations + self._host = data['host'] + self._port = data['port'] + self._device_name = data['name'] + self._entity_id = entity_id + self._register = register + self._unit = unit + self._scaling_factor = scaling_factor + self._decimal_places = decimal_places + self._device_class = device_class + self._state = None + + @property + def unique_id(self): + return f"{self._device_name}_{self._entity_id}" + + @property + def name(self): + translated_name = self._translations.get(f"component.froeling_modbus.entity.sensor.{self._entity_id}.name", self._entity_id) + return f"{self._device_name} {translated_name}" + + @property + def state(self): + return self._state + + @property + def unit_of_measurement(self): + return self._unit + + @property + def device_class(self): + return self._device_class + + async def async_update(self, _=None): + client = ModbusTcpClient(self._host, port=self._port, retries=2, timeout=15) + if client.connect(): + try: + result = client.read_input_registers(self._register - 30001, 1, unit=2) + if result.isError(): + _LOGGER.error("Error reading Modbus input register %s", self._register - 30001) + self._state = None + else: + raw_value = result.registers[0] + scaled_value = raw_value / self._scaling_factor + if self._decimal_places == 0: + self._state = int(scaled_value) # Convert to integer if decimal_places is 0 + else: + self._state = round(scaled_value, self._decimal_places) + _LOGGER.debug("Reading Modbus input register: %s, state: %s", self._register - 30001, self._state) + except Exception as e: + _LOGGER.error("Exception during Modbus communication: %s", e) + finally: + client.close() + +ANLAGENZUSTAND_MAPPING = { + 0: "Dauerlast", + 1: "Brauchwasser", + 2: "Automatik", + 3: "Scheitholzbetr", + 4: "Reinigen", + 5: "Ausgeschaltet", + 6: "Extraheizen", + 7: "Kaminkehrer", + 8: "Reinigen" +} + +KESSELZUSTAND_MAPPING = { + 0: "STÖRUNG", + 1: "Kessel Aus", + 2: "Anheizen", + 3: "Heizen", + 4: "Feuererhaltung", + 5: "Feuer Aus", + 6: "Tür offen", + 7: "Vorbereitung", + 8: "Vorwärmen", + 9: "Zünden", + 10: "Abstellen Warten", + 11: "Abstellen Warten1", + 12: "Abstellen Einschub1", + 13: "Abstellen Warten2", + 14: "Abstellen Einschub2", + 15: "Abreinigen", + 16: "2h warten", + 17: "Saugen / Heizen", + 18: "Fehlzündung", + 19: "Betriebsbereit", + 20: "Rost schließen", + 21: "Stoker leeren", + 22: "Vorheizen", + 23: "Saugen", + 24: "RSE schließen", + 25: "RSE öffnen", + 26: "Rost kippen", + 27: "Vorwärmen-Zünden", + 28: "Resteinschub", + 29: "Stoker auffüllen", + 30: "Lambdasonde aufheizen", + 31: "Gebläsenachlauf I", + 32: "Gebläsenachlauf II", + 33: "Abgestellt", + 34: "Nachzünden", + 35: "Zünden Warten", + 36: "FB: RSE schließen", + 37: "FB: Kessel belüften", + 38: "FB: Zünden", + 39: "FB: min. Einschub", + 40: "RSE schließen", + 41: "STÖRUNG: STB/NA", + 42: "STÖRUNG: Kipprost", + 43: "STÖRUNG: FR-Überdr.", + 44: "STÖRUNG: Türkont.", + 45: "STÖRUNG: Saugzug", + 46: "STÖRUNG: Umfeld", + 47: "FEHLER: STB/NA", + 48: "FEHLER: Kipprost", + 49: "FEHLER: FR-Überdr.", + 50: "FEHLER: Türkont.", + 51: "FEHLER: Saugzug", + 52: "FEHLER: Umfeld", + 53: "FEHLER: Stoker", + 54: "STÖRUNG: Stoker", + 55: "FB: Stoker leeren", + 56: "Vorbelüften", + 57: "STÖRUNG: Hackgut", + 58: "FEHLER: Hackgut", + 59: "NB: Tür offen", + 60: "NB: Anheizen", + 61: "NB: Heizen", + 62: "FEHLER: STB/NA", + 63: "FEHLER: Allgemein", + 64: "NB: Feuer Aus", + 65: "Selbsttest aktiv", + 66: "Fehlerbeh. 20min", + 67: "FEHLER: Fallschacht", + 68: "STÖRUNG: Fallschacht", + 69: "Reinigen möglich", + 70: "Heizen - Reinigen", + 71: "SH Anheizen", + 72: "SH Heizen", + 73: "SH Heiz/Abstell", + 74: "STÖRUNG sicher", + 75: "AGR Nachlauf", + 76: "AGR reinigen", + 77: "Zündung AUS", + 78: "Filter reinigen", + 79: "Anheizassistent", + 80: "SH Zünden", + 81: "SH Störung", + 82: "Sensorcheck" +} + +class FroelingTextSensor(SensorEntity): + def __init__(self, hass, translations, data, entity_id, register, mapping): + self._hass = hass + self._translations = translations + self._host = data['host'] + self._port = data['port'] + self._device_name = data['name'] + self._entity_id = entity_id + self._register = register + self._mapping = mapping + self._state = None + + @property + def unique_id(self): + return f"{self._device_name}_{self._entity_id}" + + @property + def name(self): + translated_name = self._translations.get(f"component.froeling_modbus.entity.sensor.{self._entity_id}.name", self._entity_id) + return f"{self._device_name} {translated_name}" + + @property + def state(self): + return self._state + + async def async_update_text_sensor(self, _=None): + client = ModbusTcpClient(self._host, port=self._port, retries=2, timeout=15) + if client.connect(): + try: + result = client.read_input_registers(self._register - 30001, 1, unit=2) + if result.isError(): + _LOGGER.error("Error reading Modbus input register %s", self._register - 30001) + self._state = None + else: + raw_value = result.registers[0] + self._state = self._mapping.get(raw_value, "Unknown") + _LOGGER.debug("Reading Modbus input register: %s, state: %s", self._register - 30001, self._state) + except Exception as e: + _LOGGER.error("Exception during Modbus communication: %s", e) + finally: + client.close() + diff --git a/custom_components/froeling_lambdatronic_modbus/translations/de.json b/custom_components/froeling_lambdatronic_modbus/translations/de.json new file mode 100644 index 0000000..28cc381 --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/translations/de.json @@ -0,0 +1,211 @@ +{ + "config": { + "step": { + "user": { + "title": "Froeling Modbus konfigurieren", + "description": "Bitte geben Sie die Verbindungsdetails für Ihr Froeling Modbus-Gerät ein.", + "data": { + "name": "Eindeutiger Name (Standard: Froeling)", + "host": "Hostname/IP", + "port": "Port (Standard: 502)", + "update_interval": "Update intervall (Standard: 60 Sekunden)", + "kessel": "Kessel", + "boiler01": "Boiler 01", + "hk01": "Heizkreis 01", + "hk02": "Heizkreis 02", + "austragung": "Austragung", + "puffer01": "Puffer 01", + "zirkulationspumpe": "Zirkulationspumpe" + } + } + } + }, + "entity": { + "sensor": { + "Anlagenzustand": { + "name": "Anlagenzustand" + }, + "Kesselzustand": { + "name": "Kesselzustand" + }, + "Aussentemperatur": { + "name": "Außentemperatur" + }, + "Kesseltemperatur": { + "name": "Kesseltemperatur" + }, + "Abgastemperatur": { + "name": "Abgastemperatur" + }, + "Verbleibende_Heizstunden_bis_zur_Asche_entleeren_Warnung": { + "name": "Verbleibende Heizstunden bis zur Asche entleeren Warnung" + }, + "Saugzug_Ansteuerung": { + "name": "Saugzug - Ansteuerung" + }, + "Saugzugdrehzahl": { + "name": "Saugzugdrehzahl" + }, + "Sauerstoffregler": { + "name": "Sauerstoffregler" + }, + "Restsauerstoffgehalt": { + "name": "Restsauerstoffgehalt" + }, + "Ruecklauffuehler": { + "name": "Rücklauffühler" + }, + "Primaerluft": { + "name": "Primärluft" + }, + "Sekundaerluft": { + "name": "Sekundärluft" + }, + "Betriebsstunden": { + "name": "Betriebsstunden" + }, + "Stunden_seit_letzter_Wartung": { + "name": "Stunden seit letzter Wartung" + }, + "Betriebsstunden_in_der_Feuererhaltung": { + "name": "Betriebsstunden in der Feuererhaltung" + }, + "HK01_Vorlauf_Isttemperatur": { + "name": "HK01 - Vorlauf-Isttemperatur" + }, + "HK01_Vorlauf_Solltemperatur": { + "name": "HK01 - Vorlauf-Solltemperatur" + }, + "HK02_Vorlauf_Isttemperatur": { + "name": "HK02 - Vorlauf-Isttemperatur" + }, + "HK02_Vorlauf_Solltemperatur": { + "name": "HK02 - Vorlauf-Solltemperatur" + }, + "Puffer_1_Temperatur_oben": { + "name": "Puffer 1 Temperatur oben" + }, + "Puffer_1_Temperatur_mitte": { + "name": "Puffer 1 Temperatur mitte" + }, + "Puffer_1_Temperatur_unten": { + "name": "Puffer 1 Temperatur unten" + }, + "Puffer_1_Pufferpumpen_Ansteuerung": { + "name": "Puffer 1 Pufferpumpen Ansteuerung" + }, + "Puffer_1_Ladezustand": { + "name": "Puffer 1 Ladezustand" + }, + "Boiler_1_Temperatur_oben": { + "name": "Boiler 1 Temperatur oben" + }, + "Boiler_1_Pumpe_Ansteuerung": { + "name": "Boiler 1 Pumpe Ansteuerung" + }, + "Fuellstand_im_Pelletsbehaelter": { + "name": "Füllstand_im_Pelletsbehälter" + }, + "Resetierbarer_kg_Zaehler": { + "name": "Resetierbarer Kg Zähler" + }, + "Resetierbarer_t_Zaehler": { + "name": "Resetierbarer t Zähler" + }, + "Pelletverbrauch_Gesamt": { + "name": "Pelletverbrauch Gesamt" + }, + "Ruecklauftemperatur_an_der_Zirkulations_Leitung": { + "name": "Rücklauftemperatur an der Zirkulations Leitung" + }, + "Stoemungsschalter_an_der_Brauchwasser_Leitung": { + "name": "Strömungsschalter an der Brauchwasser Leitung" + }, + "Drehzahl_der_Zirkulations_Pumpe": { + "name": "Drehzahl der Zirkulations Pumpe" + } + }, + "number": { + "Kessel_Solltemperatur": { + "name": "Kessel Solltemperatur" + }, + "Bei_welcher_RL_Temperatur_an_der_Zirkulationsleitung_soll_die_Pumpe_ausschalten": { + "name": "Bei welcher RL-Temperatur an der Zirkulationsleitung soll die Pumpe ausschalten" + }, + "HK1_Vorlauf_Temperatur_10C_Aussentemperatur": { + "name": "HK1 Vorlauf Temperatur bei 10°C Außentemperatur" + }, + "HK1_Vorlauf_Temperatur_minus_10C_Aussentemperatur": { + "name": "HK1 Vorlauf Temperatur bei -10°C Außentemperatur" + }, + "HK1_Heizkreispumpe_ausschalten_wenn_Vorlauf_Soll_kleiner_ist_als": { + "name": "HK1 Heizkreispumpe ausschalten wenn Vorlauf Soll kleiner ist als" + }, + "HK1_Absenkung_der_Vorlauftemperatur_im_Absenkbetrieb": { + "name": "HK1 Absenkung der Vorlauftemperatur im Absenkbetrieb" + }, + "HK1_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Heizbetrieb_einschaltet": { + "name": "HK1 Außentemperatur unter der die Heizkreispumpe im Heizbetrieb einschaltet" + }, + "HK1_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Absenkbetrieb_einschaltet": { + "name": "HK1 Außentemperatur unter der die Heizkreispumpe im Absenkbetrieb einschaltet" + }, + "HK1_Frostschutztemperatur": { + "name": "HK1 Frostschutztemperatur" + }, + "HK1_Temp_am_Puffer_oben_ab_der_der_Ueberhitzungsschutz_aktiv_wird": { + "name": "HK1 Temperatur am Puffer oben ab der der Überhitzungsschutz aktiv wird" + }, + "HK2_Vorlauf_Temperatur_10C_Aussentemperatur": { + "name": "HK2 Vorlauf Temperatur bei 10°C Außentemperatur" + }, + "HK2_Vorlauf_Temperatur_minus_10C_Aussentemperatur": { + "name": "HK2 Vorlauf Temperatur bei -10°C Außentemperatur" + }, + "HK2_Heizkreispumpe_ausschalten_wenn_Vorlauf_Soll_kleiner_ist_als": { + "name": "HK2 Heizkreispumpe ausschalten wenn Vorlauf Soll kleiner ist als" + }, + "HK2_Absenkung_der_Vorlauftemperatur_im_Absenkbetrieb": { + "name": "HK2 Absenkung der Vorlauftemperatur im Absenkbetrieb" + }, + "HK2_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Heizbetrieb_einschaltet": { + "name": "HK2 Außentemperatur unter der die Heizkreispumpe im Heizbetrieb einschaltet" + }, + "HK2_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Absenkbetrieb_einschaltet": { + "name": "HK2 Außentemperatur unter der die Heizkreispumpe im Absenkbetrieb einschaltet" + }, + "HK2_Frostschutztemperatur": { + "name": "HK2 Frostschutztemperatur" + }, + "HK2_Temp_am_Puffer_oben_ab_der_der_Ueberhitzungsschutz_aktiv_wird": { + "name": "HK2 Temperatur am Puffer oben ab der der Überhitzungsschutz aktiv wird" + }, + "Boiler_1_Gewuenschte_Boilertemperatur": { + "name": "Boiler 1 Gewünschte Boilertemperatur" + }, + "Boiler_1_Nachladen_wenn_Boilertemperatur_unter": { + "name": "Boiler 1 Nachladen wenn Boilertemperatur unter" + }, + "Pelletlager_Restbestand": { + "name": "Pelletlager Restbestand" + } + }, + "binary_sensor": { + "hk1_pumpe_an_aus": { + "name": "HK01 Pumpe AN/AUS" + }, + "hk2_pumpe_an_aus": { + "name": "HK02 Pumpe AN/AUS" + }, + "puffer_1_pufferpumpe_an_aus": { + "name": "Puffer 1 Pumpe AN/AUS" + }, + "zirkulationspumpe_an_aus": { + "name": "Zirkulationspumpe AN/AUS" + }, + "boiler_1_pumpe_an_aus": { + "name": "Boiler 1 Pumpe AN/AUS" + } + } + } +} \ No newline at end of file diff --git a/custom_components/froeling_lambdatronic_modbus/translations/en.json b/custom_components/froeling_lambdatronic_modbus/translations/en.json new file mode 100644 index 0000000..d0e430e --- /dev/null +++ b/custom_components/froeling_lambdatronic_modbus/translations/en.json @@ -0,0 +1,211 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Froeling Modbus", + "description": "Please enter the connection details for your Froeling Modbus device.", + "data": { + "name": "Unique Name (Default: Froeling)", + "host": "Hostname/IP", + "port": "Port (Default: 502)", + "update_interval": "Update interval (Default: 60 seconds)", + "kessel": "Boiler", + "boiler01": "DHW Boiler 01 (Domestic Hot Water)", + "hk01": "Heating Circuit 01", + "hk02": "Heating Circuit 02", + "austragung": "Feed System", + "puffer01": "Buffer 01", + "zirkulationspumpe": "Circulation Pump" + } + } + } + }, + "entity": { + "sensor": { + "Anlagenzustand": { + "name": "System State" + }, + "Kesselzustand": { + "name": "Boiler State" + }, + "Aussentemperatur": { + "name": "Outside Temperature" + }, + "Kesseltemperatur": { + "name": "Boiler Temperature" + }, + "Abgastemperatur": { + "name": "Exhaust Temperature" + }, + "Verbleibende_Heizstunden_bis_zur_Asche_entleeren_Warnung": { + "name": "Remaining Heating Hours Until Ash Emptying Warning" + }, + "Saugzug_Ansteuerung": { + "name": "Induced Draft Control" + }, + "Saugzugdrehzahl": { + "name": "Induced Draft Speed" + }, + "Sauerstoffregler": { + "name": "Oxygen Controller" + }, + "Restsauerstoffgehalt": { + "name": "Residual Oxygen Content" + }, + "Ruecklauffuehler": { + "name": "Return Sensor" + }, + "Primaerluft": { + "name": "Primary Air" + }, + "Sekundaerluft": { + "name": "Secondary Air" + }, + "Betriebsstunden": { + "name": "Operating Hours" + }, + "Stunden_seit_letzter_Wartung": { + "name": "Hours Since Last Maintenance" + }, + "Betriebsstunden_in_der_Feuererhaltung": { + "name": "Operating Hours in Fire Maintenance" + }, + "HK01_Vorlauf_Isttemperatur": { + "name": "HK01 Flow Actual Temperature" + }, + "HK01_Vorlauf_Solltemperatur": { + "name": "HK01 Flow Target Temperature" + }, + "HK02_Vorlauf_Isttemperatur": { + "name": "HK02 Flow Actual Temperature" + }, + "HK02_Vorlauf_Solltemperatur": { + "name": "HK02 Flow Target Temperature" + }, + "Puffer_1_Temperatur_oben": { + "name": "Buffer 1 Top Temperature" + }, + "Puffer_1_Temperatur_mitte": { + "name": "Buffer 1 Middle Temperature" + }, + "Puffer_1_Temperatur_unten": { + "name": "Buffer 1 Bottom Temperature" + }, + "Puffer_1_Pufferpumpen_Ansteuerung": { + "name": "Buffer 1 Pump Control" + }, + "Puffer_1_Ladezustand": { + "name": "Buffer 1 Charge State" + }, + "Boiler_1_Temperatur_oben": { + "name": "Boiler 1 Top Temperature" + }, + "Boiler_1_Pumpe_Ansteuerung": { + "name": "Boiler 1 Pump Control" + }, + "Fuellstand_im_Pelletsbehaelter": { + "name": "Pellet Container Level" + }, + "Resetierbarer_kg_Zaehler": { + "name": "Resettable kg Counter" + }, + "Resetierbarer_t_Zaehler": { + "name": "Resettable t Counter" + }, + "Pelletverbrauch_Gesamt": { + "name": "Total Pellet Consumption" + }, + "Ruecklauftemperatur_an_der_Zirkulations_Leitung": { + "name": "Return Temperature at the Circulation Line" + }, + "Stoemungsschalter_an_der_Brauchwasser_Leitung": { + "name": "Flow Switch at the Domestic Water Line" + }, + "Drehzahl_der_Zirkulations_Pumpe": { + "name": "Speed of the Circulation Pump" + } + }, + "number": { + "Kessel_Solltemperatur": { + "name": "Boiler Target Temperature" + }, + "Bei_welcher_RL_Temperatur_an_der_Zirkulationsleitung_soll_die_Pumpe_ausschalten": { + "name": "At Which Return Line Temperature Should the Circulation Pump Turn Off" + }, + "HK1_Vorlauf_Temperatur_10C_Aussentemperatur": { + "name": "HK1 Flow Temperature at 10°C Outside Temperature" + }, + "HK1_Vorlauf_Temperatur_minus_10C_Aussentemperatur": { + "name": "HK1 Flow Temperature at -10°C Outside Temperature" + }, + "HK1_Heizkreispumpe_ausschalten_wenn_Vorlauf_Soll_kleiner_ist_als": { + "name": "HK1 Heating Circuit Pump Off When Flow Target is Less Than" + }, + "HK1_Absenkung_der_Vorlauftemperatur_im_Absenkbetrieb": { + "name": "HK1 Reduction of Flow Temperature in Setback Mode" + }, + "HK1_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Heizbetrieb_einschaltet": { + "name": "HK1 Outside Temperature Below Which Heating Circuit Pump Turns On in Heating Mode" + }, + "HK1_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Absenkbetrieb_einschaltet": { + "name": "HK1 Outside Temperature Below Which Heating Circuit Pump Turns On in Setback Mode" + }, + "HK1_Frostschutztemperatur": { + "name": "HK1 Frost Protection Temperature" + }, + "HK1_Temp_am_Puffer_oben_ab_der_der_Ueberhitzungsschutz_aktiv_wird": { + "name": "HK1 Temperature at Buffer Top Where Overheat Protection Activates" + }, + "HK2_Vorlauf_Temperatur_10C_Aussentemperatur": { + "name": "HK2 Flow Temperature at 10°C Outside Temperature" + }, + "HK2_Vorlauf_Temperatur_minus_10C_Aussentemperatur": { + "name": "HK2 Flow Temperature at -10°C Outside Temperature" + }, + "HK2_Heizkreispumpe_ausschalten_wenn_Vorlauf_Soll_kleiner_ist_als": { + "name": "HK2 Heating Circuit Pump Off When Flow Target is Less Than" + }, + "HK2_Absenkung_der_Vorlauftemperatur_im_Absenkbetrieb": { + "name": "HK2 Reduction of Flow Temperature in Setback Mode" + }, + "HK2_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Heizbetrieb_einschaltet": { + "name": "HK2 Outside Temperature Below Which Heating Circuit Pump Turns On in Heating Mode" + }, + "HK2_Aussentemperatur_unter_der_die_Heizkreispumpe_im_Absenkbetrieb_einschaltet": { + "name": "HK2 Outside Temperature Below Which Heating Circuit Pump Turns On in Setback Mode" + }, + "HK2_Frostschutztemperatur": { + "name": "HK2 Frost Protection Temperature" + }, + "HK2_Temp_am_Puffer_oben_ab_der_der_Ueberhitzungsschutz_aktiv_wird": { + "name": "HK2 Temperature at Buffer Top Where Overheat Protection Activates" + }, + "Boiler_1_Gewuenschte_Boilertemperatur": { + "name": "Boiler 1 Desired Temperature" + }, + "Boiler_1_Nachladen_wenn_Boilertemperatur_unter": { + "name": "Boiler 1 Recharge When Temperature is Below" + }, + "Pelletlager_Restbestand": { + "name": "Pellet Storage Remaining Stock" + } + }, + "binary_sensor": { + "hk1_pumpe_an_aus": { + "name": "HK01 Pump ON/OFF" + }, + "hk2_pumpe_an_aus": { + "name": "HK02 Pump ON/OFF" + }, + "puffer_1_pufferpumpe_an_aus": { + "name": "Buffer 1 Pump ON/OFF" + }, + "zirkulationspumpe_an_aus": { + "name": "Circulation Pump ON/OFF" + }, + "boiler_1_pumpe_an_aus": { + "name": "Boiler 1 Pump ON/OFF" + } + } + } +} \ No newline at end of file diff --git a/docs/image.png b/docs/image.png new file mode 100644 index 0000000..70f2cf5 Binary files /dev/null and b/docs/image.png differ diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..681855c --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "Froeling Lambdatronic Modbus", + "iot_class": "local_polling" + } \ No newline at end of file