diff --git a/CODEOWNERS b/CODEOWNERS
index 1865229aed998..6416012bcefd6 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -244,6 +244,7 @@
/bundles/org.openhab.binding.modbus/ @ssalonen
/bundles/org.openhab.binding.modbus.e3dc/ @weymann
/bundles/org.openhab.binding.modbus.helioseasycontrols/ @bern77
+/bundles/org.openhab.binding.modbus.lambda/ @chilobo
/bundles/org.openhab.binding.modbus.kermi/ @KaaNee
/bundles/org.openhab.binding.modbus.sbc/ @fwolter
/bundles/org.openhab.binding.modbus.stiebeleltron/ @pail23
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 1ce231440bb18..1b165f120f860 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1211,6 +1211,11 @@
org.openhab.binding.modbus.kermi
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.modbus.lambda
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.modbus.sbc
diff --git a/bundles/org.openhab.binding.modbus.lambda/NOTICE b/bundles/org.openhab.binding.modbus.lambda/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.modbus.lambda/README.md b/bundles/org.openhab.binding.modbus.lambda/README.md
new file mode 100644
index 0000000000000..a69b37b776251
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/README.md
@@ -0,0 +1,314 @@
+# Lambda Heat Pump
+
+This extension adds support for the Lambda Heat Pump modbus protocol as provided by
+
+
+A Lambda Heat Pump has to be reachable within your network.
+If you plan to use the E-Manager part to hand over your PV excess to the heat pump ask Lambda support to configure it to
+E-Meter Kommunikationsart: ModBus Client
+E-Meter Messpunkt: E-Eintrag
+
+Other configurations of the E-Manager are not supported (yet).
+
+## Supported Things
+
+This bundle adds the following thing types to the Modbus binding.
+Note, that the things will show up under the Modbus binding.
+
+| Thing | ThingTypeID | Description |
+| --------------- | --------------------| --------------------------------------- |
+| General | general | General sections Ambient and E-Manager |
+| Heat Pump | heat-pump | Heat Pump section |
+| Boiler | boiler | Boiler section |
+| Buffer | buffer | Buffer section |
+| Solar | solar | Solar section |
+| Heating Circuit | heating-circuit | Heating Circuit section |
+
+A Modbus Bridge has to be installed before installing the above mentioned things.
+The binding supports installations with more than one Heat Pump, Boiler, Buffer, Heating Circuit.
+For each of these parts you have to provide the Subindex of your thing in the configurations section, usually using the User Interface.
+So if you have two Heating Circuits use 0 for the first and 1 for the second Heating Circuit.
+Handling of General System Settings (Base Adress 200) is not supported (yet). Solar functions (Base Address 4000) are now supported.
+Some of the registers noted RW in the manual are read only in the binding.
+
+## Discovery
+
+This extension does not support autodiscovery. The things need to be added manually.
+
+## Thing Configuration
+
+You first need to set up a TCP Modbus bridge according to the Modbus documentation.
+A typical modbus bridge configuration would look like this:
+
+```java
+Bridge modbus:tcp:bridge [ host="10.0.0.2", port=502, id=1 ]
+```
+
+You then add the things of the Lambda Heat Pump system as part of the modbus binding.
+Things in this extension will use the selected bridge to connect to the device.
+
+The following parameters are valid for all things:
+
+| Parameter | Type | Required | Default if omitted | Description |
+| --------- | ------- | -------- | ------------------ | -------------------------------------------------------------------------- |
+| refresh | integer | no | 30 | Poll interval in seconds. Increase this if you encounter connection errors |
+| maxTries | integer | no | 3 | Number of retries when before giving up reading from this thing. |
+
+Heat Pump, Boiler, Buffer and Heating Circuit things use another parameter Subindex to add one or more things of this type:
+
+| Parameter | Type | Required | Default if omitted | Description |
+| --------- | ------- | -------- | ------------------ | -------------------------------------------------------------------------- |
+| Subindex | integer | yes | 0 | Subindex for things of the same thing type, starting with 0 |
+
+| Thing Type | Range |
+| ---------------- | ------- |
+| Heatpump | 0..2 |
+| Boiler | 0..4 |
+| Buffer | 0..4 |
+| Solar | 0..2 |
+| Heating Circuit | 0..11 |
+
+## Channels
+
+Channels within the things are grouped into channel groups.
+
+### General:
+
+### Ambient Group
+
+| Channel ID | Item Type | Read only | Description |
+| -------------------------------- | ------------------ | --------- | ------------------------------------------------------------------------ |
+| ambient-error-number | Number | true | Ambient Error Number (0 = No error) |
+| ambient-operating-state | Number | true | Ambient Operating State (0 = OFF, 1 = AUTOMATIC, 2 = MANUAL, 3 = ERROR) |
+| actual-ambient-temperature | Number:Temperature | false | Actual Ambient Temperature |
+| average-ambient-temperature | Number:Temperature | true | Arithmetic average temperature of the last 60 minutes |
+| calculated-ambient-temperature | Number:Temperature | true | Temperature for calculations in heat distribution modules |
+
+### E-Manager Group
+
+This group contains parameters signaling the PV excess to the heat pump.
+
+| Channel ID | Item Type | Read only | Description |
+| -------------------------------- | ------------------ | --------- | -------------------------------------------------------------------------------------- |
+| emanager-error-number | Number | true | E-Manager Error Number (0 = No error) |
+| emanager-operating-state | Number | true | E-Manager Operating State (0 = OFF, 1 = AUTOMATIC, 2 = MANUAL, 3 = ERROR 4 = OFFLINE |
+| actual-power | Number:Power | false | Actual excess power 0 W .. 65535 W |
+| actual-power-signed | Number:Power | false | Actual excess power -32768 W .. 32767 W |
+| actual-power-consumption | Number:Power | true | Power consumption of heat-pump (only valid when Betriebsart: Automatik, 0 W otherwise) |
+| power-consumption-setpoint | Number:Power | false | Power consumption setpoint for heat pump 1 |
+
+### Heat Pump Group
+
+This group contains general operational information about the heat pump itself.
+
+| Channel ID | Item Type | Read only | Description |
+| -------------------------------| --------------------------| --------- | ----------------------------------------------------------------------- |
+| heat-pump-error-state | Number | true | Error state (0 = NONE, 1 = MESSAGE, 2 = WARNING, 3 = ALARM, 4 = FAULT) |
+| heat-pump-error-number | Number | true | Error number: scrolling through all active error numbers (1..99) |
+| heat-pump-state | Number | true | State: See Modbus description manual, link above |
+| heat-pump-operating-state | Number | true | Operating State: See Modbus description manual, link above |
+| heat-pump-t-flow | Number:Temperature | true | Flow line termperature |
+| heat-pump-t-return | Number:Temperature | true | Return line temperature |
+| heat-pump-vol-sink | Number:VolumetricFlowRate | true | Volume flow heat sink |
+| heat-pump-t-eqin | Number:Temperature | true | Energy source inlet temperature |
+| heat-pump-t-eqout | Number:Temperature | true | Energy source outlet temperature |
+| heat-pump-vol-source | Number:VolumetricFlowRate | true | Volume flow energy source |
+| heat-pump-compressor-rating | Number | true | Compressor unit rating |
+| heat-pump-qp-heating | Number:Power | true | Actual heating capacity |
+| heat-pump-fi-power-consumption | Number:Power | true | Frequency inverter actual power consumption |
+| heat-pump-cop | Number | true | Coefficient of performance |
+| heat-pump-request-password | Number | false | Password register to release modbus request registers |
+| heat-pump-request-type | Number: | false | Request Type |
+| heat-pump-request-t-flow | Number:Temperature | false | Requested flow line termperature |
+| heat-pump-request-t-return | Number:Temperature | false | Requested return line temperature |
+| heat-pump-request-heat-sink | Number:Temperature | false | Requested temperature difference between flow and return line |
+| heat-pump-relais-state | Number:Temperature | true | Heatpump Relais State for 2nd heating stage |
+| heat-pump-vdae | Number:Energy | true | Accumulated electrical energy consumption of compressor unit |
+| heat-pump-vdaq | Number:Energy | true | Accumulated thermal energy output of compressor unit |
+
+### Boiler Group
+
+This group contains information about the boiler for the water for domestic use / tap water / washwater.
+
+| Channel ID | Item Type | Read only | Description |
+| ------------------------------------- | ------------------ | --------- | ------------------------------------------------------------------- |
+| boiler-error-number | Number | true | Boiler Error Number (0 = No error) |
+| boiler-operating-state | Number | true | Boiler Operating State: See Modbus description manual, link above |
+| boiler-actual-high-temperature | Number:Temperature | true | Actual temperature boiler high sensor |
+| boiler-actual-low-temperature | Number:Temperature | true | Actual temperature boiler low sensor |
+| maximum-boiler-temperature | Number:Temperature | false | Setting for maximum boiler temperature (min = 25.0°C; max = 65.0°C) |
+
+### Buffer Group
+
+This group contains information about the buffer for the heating circuit.
+
+| Channel ID | Item Type | Read only | Description |
+| ------------------------------------------------| ------------------ | --------- | ---------------------------------------------------------------------|
+| buffer-error-number | Number | true | Buffer Error Number (0 = No error) |
+| buffer-operating-state | Number | true | Buffer Operating State: See Modbus description manual, link above |
+| buffer-actual-high-temperature | Number:Temperature | true | Actual temperature buffer high sensor |
+| buffer-actual-low-temperature | Number:Temperature | true | Actual temperature buffer low sensor |
+| buffer-actual-modbus-temperature | Number:Temperature | false | Actual temperature set via modbus |
+| buffer-request-type | Number | false | Request Type: See Modbus description manual, link above |
+| buffer-request-flow-line-temperature | Number:Temperature | false | Requested flow line temperature |
+| buffer-request-return-line-temperature | Number:Temperature | false | Requested return line temperature |
+| buffer-request-heat-sink-temperature-difference | Number:Temperature | false | Requested temperature difference between flow line and return line |
+| buffer-request-heating-capacity | Number:Power | false | Requested capacity |
+| maximum-buffer-temperature | Number:Temperature | false | Setting for maximum buffer temperature ) |
+### Solar Group
+
+This group contains information about the solar thermic component.
+
+| Channel ID | Item Type | Read only | Description |
+| ------------------------------ | ------------------ | --------- | ------------------------------------------------------------------- |
+| solar-error-number | Number | true | Solar Error Number (0 = No error) |
+| solar-operating-state | Number | true | Solar Operating State: See Modbus description manual, link above |
+| solar-collector-temperature | Number:Temperature | true | Temperature of the solar collector |
+| solar-storage-temperature | Number:Temperature | true | Temperature of the solar storage |
+| solar-pump-speed | Number | true | Speed of the solar pump |
+| solar-heat-quantity | Number:Energy | true | Heat quantity produced by solar |
+| solar-power-output | Number:Power | true | Current power output of solar |
+| solar-operating-hours | Number | true | Operating hours of solar component |
+
+### Heating Circuit Group
+
+This group contains general operational information about the heating circuit.
+
+| Channel ID | Item Type | Read only | Description |
+| ---------------------------------------------- | -------------------| --------- | ----------------------------------------------------------- |
+| heating-circuit-error-number | Number | true | Error Number (0 = No error) |
+| heating-circuit-operating-state | Number | true | Operating State: See Modbus description manual, link above |
+| heating-circuit-flow-line-temperature | Number:Temperature | true | Actual temperature flow line sensor |
+| heating-circuit-return-line-temperature | Number:Temperature | true | Actual temperature return line sensor |
+| heating-circuit-room-device-temperature | Number:Temperature | false | Actual temperature room device sensor |
+| heating-circuit-setpoint-flow-line-temperature | Number:Temperature | false | Setpoint temperature flow line |
+| heating-circuit-operating-mode | Number | false | Operating Mode: See Modbus description manual, link above |
+| heating-circuit-target-temperature-flow-line | Number:Temperature | false | Setpoin temperature flow line (min = 15.0°C; max = 65.0°C) |
+| heating-circuit-offset-flow-line-temperature | Number:Temperature | false | Setting for flow line temperature setpoint offset |
+| heating-circuit-room-heating-temperature | Number:Temperature | false | Setting for heating mode room setpoint temperature |
+| heating-circuit-room-cooling-temperature | Number:Temperature | false | Setting for cooling mode room setpoint temperature |
+
+## Full Example
+
+### `demo.things` Example
+
+```java
+Bridge modbus:tcp:Bridge "Lambda Modbus TCP Bridge" [ host="192.168.223.83", port=502, id=1, enableDiscovery=false ] {
+ Thing general lambdageneral "Lambda General" (modbus:tcp:Bridge) [ refresh=60, subindex=0 ]
+ Thing heat-pump lambdaheat-pump "Lambda Heatpump" (modbus:tcp:Bridge) [ refresh=60, subindex=0 ]
+ Thing boiler lambdaboiler "Lambda Boiler" (modbus:tcp:Bridge) [ refresh=60, subindex=0 ]
+ Thing buffer lambdabuffer "Lambda Buffer" (modbus:tcp:Bridge) [ refresh=60, subindex=0 ]
+ Thing solar lambdasolar "Lambda Solar" (modbus:tcp:Bridge) [ refresh=60, subindex=0 ]
+}
+```
+
+### Items Lambda Boiler
+
+```java
+Number lambdaboiler_errornumber "Boiler Error Number" (lambdaboiler) { channel="modbus:boiler:Lambda_Bridge:lambdaboiler:boiler-group#boiler-error-number" }
+Number lambdaboiler_operatingstate "Boiler Operating State" (lambdaboiler) { channel="modbus:boiler:Lambda_Bridge:lambdaboiler:boiler-group#boiler-operating-state" }
+Number:Temperature lambdaboiler_actualhightemperature "Boiler Actual High Temperature" (lambdaboiler) { channel="modbus:boiler:Lambda_Bridge:lambdaboiler:boiler-group#boiler-actual-high-temperature" }
+Number:Temperature lambdaboiler_actuallowtemperature "Boiler Actual Low Temperature" (lambdaboiler) { channel="modbus:boiler:Lambda_Bridge:lambdaboiler:boiler-group#boiler-actual-low-temperature" }
+Number:Temperature lambdaboiler_maximumboilertemperature "Maximum Boiler Temperature" (lambdaboiler) { channel="modbus:boiler:Lambda_Bridge:lambdaboiler:boiler-group#maximum-boiler-temperature" }
+```
+
+### Items Lambda Buffer
+
+```java
+Number lambdabuffer_errornumber "Buffer Error Number" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-error-number" }
+Number lambdabuffer_operatingstate "Buffer Operating State" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-operating-state" }
+Number:Temperature lambdabuffer_actualhightemperature "Buffer Actual High Temperature" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-actual-high-temperature" }
+Number:Temperature lambdabuffer_actuallowtemperature "Buffer Actual Low Temperature" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-actual-low-temperature" }
+Number:Temperature lambdabuffer_actualmodbustemperature "Actual Modbus Temperature" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-actual-modbus-temperature" }
+Number lambdabuffer_requesttype "Request Type" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-request-type" }
+Number:Temperature lambdabuffer_requestflowlinetemperature "Request Flow Line Temperature" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-request-flow-line-temperature" }
+Number:Temperature lambdabuffer_requestreturnlinetemperature "Request Return Line Temperature" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-request-return-line-temperature" }
+Number:Temperature lambdabuffer_requestheatsinktemperature "Requested Heat Sink Temperature Difference" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-request-heat-sink-temperature" }
+Number:Power lambdabuffer_requestheatingcapacity "Requested Heating Capacity" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#buffer-request-heating-capacity" }
+Number:Temperature lambdabuffer_maximumbuffertemperature "Maximum Buffer Temperature" (lambdabuffer) { channel="modbus:buffer:Lambda_Bridge:lambdabuffer:buffer-group#maximum-buffer-temperature" }
+ ```
+
+### Items Lambda General
+
+```java
+Number lambdaambient_operatingstate "Ambient Operating State" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:ambient-group#ambient-operating-state" }
+Number lambdaambient_errornumber "Ambient Error Number" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:ambient-group#ambient-error-number" }
+Number:Temperature lambdaambient_actualambienttemperature "Ambient Actual Temperature" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:ambient-group#actual-ambient-temperature" }
+Number:Temperature lambdaambient_averageambienttemperature "Ambient Average Temperature" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:ambient-group#average-ambient-temperature" }
+Number:Temperature lambdaambient_calculatedambienttemperature "Ambient Calculated Temperature" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:ambient-group#calculated-ambient-temperature" }
+Number lambdaemanager_operatingstate "EManager Operating State" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:emanager-group#emanager-operating-state" }
+Number lambdaemanager_errornumber "EManager Error Number" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:emanager-group#emanager-error-number" }
+Number:Power lambdaemanager_actualpower "EManager Actual Power" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:emanager-group#actual-power" }
+Number:Power lambdaemanager_actualpowerconsumption "EManager Actual Power Consumption" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:emanager-group#actual-power-consumption" }
+Number:Power lambdaemanager_powerconsumptionsetpoint "EManager Power Consumption Setpoint" (lambdageneral) { channel="modbus:general:Lambda_Bridge:lambdageneral:emanager-group#power-consumption-setpoint" }
+
+```
+
+### Items Heatingcircuit
+
+```java
+Number lambdaheatingcircuit_errornumber "Heating Circuit Error Number" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-error-number" }
+Number lambdaheatingcircuit_operatingstate "Heating Circuit Operating State" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-operating-state" }
+Number:Temperature lambdaheatingcircuit_flowlinetemperature "Heating Circuit Flow Line Temperature" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-flow-line-temperature" }
+Number:Temperature lambdaheatingcircuit_returnlinetemperature "Heating Circuit Return Line Temperature" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-return-line-temperature" }
+Number:Temperature lambdaheatingcircuit_roomdevicetemperature "Heating Circuit Room Device Temperature" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-room-device-temperature" }
+Number:Temperature lambdaheatingcircuit_setpointflowlinetemperature "Heating Circuit Setpoint Flow Line Temperature" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-setpoint-flow-line-temperature" }
+Number lambdaheatingcircuit_operatingmode "Heating Circuit Operating Mode" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-operating-mode" }
+Number:Temperature lambdaheatingcircuit_offsetflowlinetemperature "Heating Circuit Offset Flow Line Temperature"(lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-offset-flow-line-temperature" }
+Number:Temperature lambdaheatingcircuit_roomheatingtemperature "Heating Circuit Room Heating Temperature" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-room-heating-temperature" }
+Number:Temperature lambdaheatingcircuit_roomcoolingtemperature "Heating Circuit Room Cooling Temperature" (lambdaheatingcircuit) { channel="modbus:heating-circuit:Lambda_Bridge:lambdaheatingcircuit:heating-circuit-group#heating-circuit-room-cooling-temperature" }
+```
+
+### Items Lambda Solar
+
+```java
+Number lambdasolar_errornumber "Solar Error Number" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-error-number" }
+Number lambdasolar_operatingstate "Solar Operating State" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-operating-state" }
+Number:Temperature lambdasolar_collectortemperature "Solar Collector Temperature" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-collector-temperature" }
+Number:Temperature lambdasolar_storagetemperature "Solar Storage Temperature" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-storage-temperature" }
+Number lambdasolar_pumpspeed "Solar Pump Speed" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-pump-speed" }
+Number:Energy lambdasolar_heatquantity "Solar Heat Quantity" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-heat-quantity" }
+Number:Power lambdasolar_poweroutput "Solar Power Output" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-power-output" }
+Number lambdasolar_operatinghours "Solar Operating Hours" (lambdasolar) { channel="modbus:solar:Lambda_Bridge:lambdasolar:solar-group#solar-operating-hours" }
+```
+
+### Items Lambda Heatpump
+
+```java
+Number lambdaheatpump_errorstate "Heatpump Error State" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-error-state" }
+Number lambdaheatpump_errornumber "Heatpump Error Number" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-error-number" }
+Number lambdaheatpump_state "Heatpump State" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-state" }
+Number lambdaheatpump_operatingstate "Heatpump Operating State" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-operating-state" }
+Number:Temperature lambdaheatpump_tflow "Heatpump Flow Line Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-t-flow" }
+Number:Temperature lambdaheatpump_treturn "Heatpump Return Line Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-t-return" }
+Number:VolumetricFlowRate lambdaheatpump_volsink "Heatpump Volume Flow Heat Sink" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-vol-sink" }
+Number:Temperature lambdaheatpump_teqin "Heatpump Energy Source Inlet Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-t-eqin" }
+Number:Temperature lambdaheatpump_teqout "Heatpump Energy Source Outlet Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-t-eqout" }
+Number:VolumetricFlowRate lambdaheatpump_volsource "Heatpump Volume Flow Energy Source" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-vol-source" }
+Number lambdaheatpump_compressorrating "Heatpump Compressor Rating" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-compressor-rating" }
+Number:Power lambdaheatpump_qpheating "Heatpump Actual Heating Capacity" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-qp-heating" }
+Number:Power lambdaheatpump_fipowerconsumption "Heatpump Frequency inverter Actual Power Consumption" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-fi-power-consumption" }
+Number lambdaheatpump_cop "Heatpump COP" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-cop" }
+Number lambdaheatpump_requestpassword "Heatpump Request Password" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-request-password" }
+Number lambdaheatpump_requesttype "Heatpump Request Type" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-request-type" }
+Number lambdaheatpump_requesttflow "Heatpump Requested Flow Line Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-request-t-flow" }
+Number lambdaheatpump_requesttreturn "Heatpump Requested Return Line Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-request-t-return" }
+Number lambdaheatpump_requestheatsink "Heatpump Requested Heat Sink Temperature" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-request-heat-sink" }
+Number lambdaheatpump_relaisstate "Heatpump Relais State" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-relais-state" }
+Number:Energy lambdaheatpump_vdae "Heatpump Accumulated Electrical Energy consumption" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-vdae" }
+Number:Energy lambdaheatpump_vdaq "Heatpump Accumulated Thermical Energy consumption" (lambdaheatpump) { channel="modbus:heat-pump:Lambda_Bridge:lambdaheatpumpheat-pump-group#heat-pump-vdaq" }
+```
+
+### Example: (DSL) Send Power value the E-Manager of the Lambda Heat Pump
+
+'''
+// Sending Value to Heatpump
+// Script has to send a value about every 30 seconds, for example with cron settings.
+// Calculate power_to_heat-pump using your data provided by the PV system.
+// var int power_to_heat-pump = ((lambdaemanager_actualpowerconsumption.state as Number) - (PW_Battery.state as Number) - (PW_Grid.state as Number)).intValue
+
+var int power_to_heat-pump = 1000
+
+ lambdaemanager_actualpower.sendCommand(power_to_heat-pump)
+'''
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/pom.xml b/bundles/org.openhab.binding.modbus.lambda/pom.xml
new file mode 100644
index 0000000000000..0bd8326f8fc8e
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ 4.0.0
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 5.1.0-SNAPSHOT
+
+
+ org.openhab.binding.modbus.lambda
+
+ openHAB Add-ons :: Bundles :: Lambda Binding
+
+
+
+ org.openhab.addons.bundles
+ org.openhab.binding.modbus
+ ${project.version}
+ provided
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/BoilerConfiguration.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/BoilerConfiguration.java
new file mode 100644
index 0000000000000..268554251444d
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/BoilerConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BoilerConfiguration} class contains fields mapping
+ * thing configuration parameters.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class BoilerConfiguration extends GeneralConfiguration {
+ /**
+ * Subindex to calculate the base adress of the modbus registers
+ */
+ private int subindex = 0;
+
+ public int getSubindex() {
+ return subindex;
+ }
+
+ public void setSubindex(int subindex) {
+ this.subindex = subindex;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/BufferConfiguration.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/BufferConfiguration.java
new file mode 100644
index 0000000000000..305acef9c7a6e
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/BufferConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BufferConfiguration} class contains fields mapping
+ * thing configuration parameters.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class BufferConfiguration extends GeneralConfiguration {
+ /**
+ * Subindex to calculate the base adress of the modbus registers
+ */
+ private int subindex = 0;
+
+ public int getSubindex() {
+ return subindex;
+ }
+
+ public void setSubindex(int subindex) {
+ this.subindex = subindex;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/GeneralConfiguration.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/GeneralConfiguration.java
new file mode 100644
index 0000000000000..59cdafb853212
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/GeneralConfiguration.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GeneralConfiguration} class contains fields mapping
+ * thing configuration parameters.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class GeneralConfiguration {
+ /**
+ * Refresh interval in seconds
+ */
+ private int refresh = 30;
+
+ private int maxTries = 3;// backwards compatibility and tests
+
+ /**
+ * Gets refresh period in milliseconds
+ */
+ public long getRefreshMillis() {
+ return refresh * 1000;
+ }
+
+ public int getMaxTries() {
+ return maxTries;
+ }
+
+ public void setMaxTries(int maxTries) {
+ this.maxTries = maxTries;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/HeatingCircuitConfiguration.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/HeatingCircuitConfiguration.java
new file mode 100644
index 0000000000000..a6b03a27a2bdb
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/HeatingCircuitConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HeatingCircuitConfiguration} class contains fields mapping
+ * thing configuration parameters.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class HeatingCircuitConfiguration extends GeneralConfiguration {
+ /**
+ * Subindex to calculate the base adress of the modbus registers
+ */
+ private int subindex = 0;
+
+ public int getSubindex() {
+ return subindex;
+ }
+
+ public void setSubindex(int subindex) {
+ this.subindex = subindex;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/HeatpumpConfiguration.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/HeatpumpConfiguration.java
new file mode 100644
index 0000000000000..e384e2ea27d2e
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/HeatpumpConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link HeatpumpConfiguration} class contains fields mapping
+ * thing configuration parameters.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class HeatpumpConfiguration extends GeneralConfiguration {
+ /**
+ * Subindex to calculate the base adress of the modbus registers
+ */
+ private int subindex = 0;
+
+ public int getSubindex() {
+ return subindex;
+ }
+
+ public void setSubindex(int subindex) {
+ this.subindex = subindex;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/LambdaBindingConstants.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/LambdaBindingConstants.java
new file mode 100644
index 0000000000000..601d31e90c82c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/LambdaBindingConstants.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.ModbusBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link LambdaBindingConstants} class defines common
+ * constants, which are used across the whole binding.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class LambdaBindingConstants {
+
+ private static final String BINDING_ID = ModbusBindingConstants.BINDING_ID;
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_GENERAL = new ThingTypeUID(BINDING_ID, "general");
+ public static final ThingTypeUID THING_TYPE_BOILER = new ThingTypeUID(BINDING_ID, "boiler");
+ public static final ThingTypeUID THING_TYPE_BUFFER = new ThingTypeUID(BINDING_ID, "buffer");
+ public static final ThingTypeUID THING_TYPE_HEAT_PUMP = new ThingTypeUID(BINDING_ID, "heat-pump");
+ public static final ThingTypeUID THING_TYPE_SOLAR = new ThingTypeUID(BINDING_ID, "solar");
+ public static final ThingTypeUID THING_TYPE_HEATING_CIRCUIT = new ThingTypeUID(BINDING_ID, "heating-circuit");
+
+ // Channel group ids
+ public static final String GROUP_GENERAL_AMBIENT = "ambient-group";
+ public static final String GROUP_GENERAL_EMANAGER = "emanager-group";
+ public static final String GROUP_HEAT_PUMP = "heat-pump-group";
+ public static final String GROUP_BOILER = "boiler-group";
+ public static final String GROUP_BOILER_REG50 = "boiler-reg50-group";
+ public static final String GROUP_BUFFER = "buffer-group";
+ public static final String GROUP_BUFFER_REG50 = "buffer-reg50-group";
+ public static final String GROUP_SOLAR = "solar-group";
+ public static final String GROUP_SOLAR_REG50 = "solar-reg50-group";
+ public static final String GROUP_HEATING_CIRCUIT = "heating-circuit-group";
+ public static final String GROUP_HEATING_CIRCUIT_REG50 = "heating-circuit-reg50-group";
+
+ // List of all Channel ids in device information group
+ // General Ambient
+ public static final String CHANNEL_AMBIENT_ERROR_NUMBER = "ambient-error-number";
+ public static final String CHANNEL_AMBIENT_OPERATING_STATE = "ambient-operating-state";
+ public static final String CHANNEL_ACTUAL_AMBIENT_TEMPERATURE = "actual-ambient-temperature";
+ public static final String CHANNEL_AVERAGE_AMBIENT_TEMPERATURE = "average-ambient-temperature";
+ public static final String CHANNEL_CALCULATED_AMBIENT_TEMPERATURE = "calculated-ambient-temperature";
+
+ // General E-manager
+ public static final String CHANNEL_EMANAGER_ERROR_NUMBER = "emanager-error-number";
+ public static final String CHANNEL_EMANAGER_OPERATING_STATE = "emanager-operating-state";
+ public static final String CHANNEL_EMANAGER_ACTUAL_POWER = "emanager-actual-power";
+ public static final String CHANNEL_EMANAGER_ACTUAL_POWER_SIGNED = "emanager-actual-power-signed";
+ public static final String CHANNEL_EMANAGER_ACTUAL_POWER_CONSUMPTION = "emanager-actual-power-consumption";
+ public static final String CHANNEL_POWER_CONSUMPTION_SETPOINT = "emanager-power-consumption-setpoint";
+
+ // Heatpump
+ public static final String CHANNEL_HEAT_PUMP_ERROR_STATE = "heat-pump-error-state";
+ public static final String CHANNEL_HEAT_PUMP_ERROR_NUMBER = "heat-pump-error-number";
+ public static final String CHANNEL_HEAT_PUMP_STATE = "heat-pump-state";
+ public static final String CHANNEL_HEAT_PUMP_OPERATING_STATE = "heat-pump-operating-state";
+ public static final String CHANNEL_HEAT_PUMP_T_FLOW = "heat-pump-t-flow";
+ public static final String CHANNEL_HEAT_PUMP_T_RETURN = "heat-pump-t-return";
+ public static final String CHANNEL_HEAT_PUMP_VOL_SINK = "heat-pump-vol-sink";
+ public static final String CHANNEL_HEAT_PUMP_T_EQIN = "heat-pump-t-eqin";
+ public static final String CHANNEL_HEAT_PUMP_T_EQOUT = "heat-pump-t-eqout";
+ public static final String CHANNEL_HEAT_PUMP_VOL_SOURCE = "heat-pump-vol-source";
+ public static final String CHANNEL_HEAT_PUMP_COMPRESSOR_RATING = "heat-pump-compressor-rating";
+ public static final String CHANNEL_HEAT_PUMP_QP_HEATING = "heat-pump-qp-heating";
+ public static final String CHANNEL_HEAT_PUMP_FI_POWER_CONSUMPTION = "heat-pump-fi-power-consumption";
+ public static final String CHANNEL_HEAT_PUMP_COP = "heat-pump-cop";
+ public static final String CHANNEL_HEAT_PUMP_REQUEST_PASSWORD = "heat-pump-request-password";
+ public static final String CHANNEL_HEAT_PUMP_REQUEST_TYPE = "heat-pump-request-type";
+ public static final String CHANNEL_HEAT_PUMP_REQUEST_T_FLOW = "heat-pump-request-t-flow";
+ public static final String CHANNEL_HEAT_PUMP_REQUEST_T_RETURN = "heat-pump-request-t-return";
+ public static final String CHANNEL_HEAT_PUMP_REQUEST_HEAT_SINK = "heat-pump-request-heat-sink";
+ public static final String CHANNEL_HEAT_PUMP_RELAIS_STATE = "heat-pump-relais-state";
+ public static final String CHANNEL_HEAT_PUMP_VDAE = "heat-pump-vdae";
+ public static final String CHANNEL_HEAT_PUMP_VDAQ = "heat-pump-vdaq";
+
+ // Boiler
+ public static final String CHANNEL_BOILER_ERROR_NUMBER = "boiler-error-number";
+ public static final String CHANNEL_BOILER_OPERATING_STATE = "boiler-operating-state";
+ public static final String CHANNEL_BOILER_ACTUAL_HIGH_TEMPERATURE = "boiler-actual-high-temperature";
+ public static final String CHANNEL_BOILER_ACTUAL_LOW_TEMPERATURE = "boiler-actual-low-temperature";
+ public static final String CHANNEL_BOILER_MAXIMUM_BOILER_TEMPERATURE = "maximum-boiler-temperature";
+
+ // Buffer
+ public static final String CHANNEL_BUFFER_ERROR_NUMBER = "buffer-error-number";
+ public static final String CHANNEL_BUFFER_OPERATING_STATE = "buffer-operating-state";
+ public static final String CHANNEL_BUFFER_ACTUAL_HIGH_TEMPERATURE = "buffer-actual-high-temperature";
+ public static final String CHANNEL_BUFFER_ACTUAL_LOW_TEMPERATURE = "buffer-actual-low-temperature";
+ public static final String CHANNEL_BUFFER_ACTUAL_MODBUS_TEMPERATURE = "buffer-actual-modbus-temperature";
+ public static final String CHANNEL_BUFFER_REQUEST_TYPE = "buffer-request-type";
+ public static final String CHANNEL_BUFFER_REQUEST_FLOW_LINE_TEMPERATURE = "buffer-request-flow-line-temperature";
+ public static final String CHANNEL_BUFFER_REQUEST_RETURN_LINE_TEMPERATURE = "buffer-request-return-line-temperature";
+ public static final String CHANNEL_BUFFER_REQUEST_HEAT_SINK_TEMPERATURE = "buffer-request-heat-sink-temperature-difference";
+ public static final String CHANNEL_BUFFER_REQUEST_HEATING_CAPACITY = "buffer-request-heating-capacity";
+ public static final String CHANNEL_BUFFER_MAXIMUM_BUFFER_TEMPERATURE = "maximum-buffer-temperature";
+
+ // Solar
+ public static final String CHANNEL_SOLAR_ERROR_NUMBER = "solar-error-number";
+ public static final String CHANNEL_SOLAR_OPERATING_STATE = "solar-operating-state";
+ public static final String CHANNEL_SOLAR_COLLECTOR_TEMPERATURE = "solar-collector-temperature";
+ public static final String CHANNEL_SOLAR_BUFFER1_TEMPERATURE = "solar-buffer1-temperature";
+ public static final String CHANNEL_SOLAR_BUFFER2_TEMPERATURE = "solar-buffer2-temperature";
+ public static final String CHANNEL_SOLAR_MAXIMUM_BUFFER_TEMPERATURE = "solar-maximum-buffer-temperature";
+ public static final String CHANNEL_SOLAR_BUFFER_CHANGEOVER_TEMPERATURE = "solar-buffer-changeover-temperature";
+
+ // Heating Circuit
+ public static final String CHANNEL_HEATING_CIRCUIT_ERROR_NUMBER = "heating-circuit-error-number";
+ public static final String CHANNEL_HEATING_CIRCUIT_OPERATING_STATE = "heating-circuit-operating-state";
+ public static final String CHANNEL_HEATING_CIRCUIT_FLOW_LINE_TEMPERATURE = "heating-circuit-flow-line-temperature";
+ public static final String CHANNEL_HEATING_CIRCUIT_RETURN_LINE_TEMPERATURE = "heating-circuit-return-line-temperature";
+ public static final String CHANNEL_HEATING_CIRCUIT_ROOM_DEVICE_TEMPERATURE = "heating-circuit-room-device-temperature";
+ public static final String CHANNEL_HEATING_CIRCUIT_SETPOINT_FLOW_LINE_TEMPERATURE = "heating-circuit-setpoint-flow-line-temperature";
+ public static final String CHANNEL_HEATING_CIRCUIT_OPERATING_MODE = "heating-circuit-operating-mode";
+ public static final String CHANNEL_HEATING_CIRCUIT_TARGET_TEMPERATURE_FLOW_LINE = "heating-circuit-target-temperature-flow-line";
+ public static final String CHANNEL_HEATING_CIRCUIT_OFFSET_FLOW_LINE_TEMPERATURE = "heating-circuit-offset-flow-line-temperature";
+ public static final String CHANNEL_HEATING_CIRCUIT_ROOM_HEATING_TEMPERATURE = "heating-circuit-room-heating-temperature";
+ public static final String CHANNEL_HEATING_CIRCUIT_ROOM_COOLING_TEMPERATURE = "heating-circuit-room-cooling-temperature";
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/LambdaHandlerFactory.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/LambdaHandlerFactory.java
new file mode 100644
index 0000000000000..d2b571edcd013
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/LambdaHandlerFactory.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.lambda.internal.handler.BoilerHandler;
+import org.openhab.binding.modbus.lambda.internal.handler.BufferHandler;
+import org.openhab.binding.modbus.lambda.internal.handler.GeneralHandler;
+import org.openhab.binding.modbus.lambda.internal.handler.HeatingCircuitHandler;
+import org.openhab.binding.modbus.lambda.internal.handler.HeatpumpHandler;
+import org.openhab.binding.modbus.lambda.internal.handler.SolarHandler;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LambdaHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.lambda", service = ThingHandlerFactory.class)
+public class LambdaHandlerFactory extends BaseThingHandlerFactory {
+
+ private final Logger logger = LoggerFactory.getLogger(LambdaHandlerFactory.class);
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GENERAL, THING_TYPE_HEAT_PUMP,
+ THING_TYPE_BOILER, THING_TYPE_BUFFER, THING_TYPE_HEATING_CIRCUIT, THING_TYPE_SOLAR);
+
+ private static final Map> HANDLER_FACTORY_MAP = Map.of(
+ THING_TYPE_HEAT_PUMP, HeatpumpHandler::new, THING_TYPE_GENERAL, GeneralHandler::new, THING_TYPE_BUFFER,
+ BufferHandler::new, THING_TYPE_BOILER, BoilerHandler::new, THING_TYPE_HEATING_CIRCUIT,
+ HeatingCircuitHandler::new, THING_TYPE_SOLAR, SolarHandler::new);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ logger.debug("LambdaHandlerFactory thingTypeUID: {}", thingTypeUID);
+
+ Function factory = HANDLER_FACTORY_MAP.get(thingTypeUID);
+ return factory != null ? factory.apply(thing) : null;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/SolarConfiguration.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/SolarConfiguration.java
new file mode 100644
index 0000000000000..79b841f40e0f7
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/SolarConfiguration.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link SolarConfiguration} class contains fields mapping
+ * thing configuration parameters for the solar handler.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+
+@NonNullByDefault
+public class SolarConfiguration extends GeneralConfiguration {
+ /**
+ * Subindex for multiple solar components
+ */
+ private int subindex = 0;
+
+ public int getSubindex() {
+ return subindex;
+ }
+
+ public void setSubindex(int subindex) {
+ this.subindex = subindex;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/AmbientBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/AmbientBlock.java
new file mode 100644
index 0000000000000..24dc715e846b7
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/AmbientBlock.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the Ambient Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class AmbientBlock {
+ public int ambientErrorNumber;
+ public int ambientOperatingState;
+ public int actualAmbientTemperature;
+ public int averageAmbientTemperature;
+ public int calculatedAmbientTemperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BoilerBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BoilerBlock.java
new file mode 100644
index 0000000000000..eceea63f428c5
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BoilerBlock.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the Boiler Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class BoilerBlock {
+ public int boilerErrorNumber;
+ public int boilerOperatingState;
+ public int boilerActualHighTemperature;
+ public int boilerActualLowTemperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BoilerReg50Block.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BoilerReg50Block.java
new file mode 100644
index 0000000000000..08f6e8b043ed2
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BoilerReg50Block.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the BoilerReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class BoilerReg50Block {
+
+ public int boilerMaximumBoilerTemperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BufferBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BufferBlock.java
new file mode 100644
index 0000000000000..847d6c599eaa5
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BufferBlock.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the Buffer Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class BufferBlock {
+ public int bufferErrorNumber;
+ public int bufferOperatingState;
+ public int bufferActualHighTemperature;
+ public int bufferActualLowTemperature;
+ public int bufferActualModbusTemperature;
+ public int bufferRequestType;
+ public int bufferrequestFlowLineTemperature;
+ public int bufferrequestReturnLineTemperature;
+ public int bufferrequestHeatSinkTemperature;
+ public int bufferrequestHeatingCapacity;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BufferReg50Block.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BufferReg50Block.java
new file mode 100644
index 0000000000000..ed741ba3a8448
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/BufferReg50Block.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the BufferReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class BufferReg50Block {
+
+ public int bufferMaximumBufferTemperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/EManagerBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/EManagerBlock.java
new file mode 100644
index 0000000000000..f8fa19bad6c5d
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/EManagerBlock.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the EManager Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class EManagerBlock {
+ public int emanagerErrorNumber;
+ public int emanagerOperatingState;
+ public int emanagerActualPower;
+ public int emanagerActualPowerSigned;
+ public int emanagerActualPowerConsumption;
+ public int emanagerPowerConsumptionSetpoint;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatingCircuitBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatingCircuitBlock.java
new file mode 100644
index 0000000000000..33079ed119a90
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatingCircuitBlock.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the HeatingCircuit Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class HeatingCircuitBlock {
+ public int heatingcircuitErrorNumber;
+ public int heatingcircuitOperatingState;
+ public int heatingcircuitFlowLineTemperature;
+ public int heatingcircuitReturnLineTemperature;
+ public int heatingcircuitRoomDeviceTemperature;
+ public int heatingcircuitSetpointFlowLineTemperature;
+ public int heatingcircuitOperatingMode;
+ public int heatingcircuitTargetTemperatureFlowLine;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatingCircuitReg50Block.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatingCircuitReg50Block.java
new file mode 100644
index 0000000000000..eb672c8439e72
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatingCircuitReg50Block.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the HeatingCircuitReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+
+public class HeatingCircuitReg50Block {
+ public int heatingcircuitOffsetFlowLineTemperature;
+ public int heatingcircuitRoomHeatingTemperature;
+ public int heatingcircuitRoomCoolingTemperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatpumpBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatpumpBlock.java
new file mode 100644
index 0000000000000..283a3f78f1706
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/HeatpumpBlock.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the Heatpump Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+public class HeatpumpBlock {
+ public int heatpumpErrorState;
+ public int heatpumpErrorNumber;
+ public int heatpumpState;
+ public int heatpumpOperatingState;
+ public int heatpumpTFlow;
+ public int heatpumpTReturn;
+ public int heatpumpVolSink;
+ public int heatpumpTEQin;
+ public int heatpumpTEQout;
+ public int heatpumpVolSource;
+ public int heatpumpCompressorRating;
+ public int heatpumpQpHeating;
+ public int heatpumpFIPowerConsumption;
+ public int heatpumpCOP;
+ public int heatpumpRequestPassword;
+ public int heatpumpRequestType;
+ public int heatpumpRequestTFlow;
+ public int heatpumpRequestTReturn;
+ public int heatpumpRequestHeatSink;
+ public int heatpumpRelaisState;
+ public long heatpumpVdAE;
+ public long heatpumpVdAQ;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/SolarBlock.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/SolarBlock.java
new file mode 100644
index 0000000000000..e5d2dfbddc518
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/SolarBlock.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Data transfer object for solar thermic component data
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus to add handler
+ */
+public class SolarBlock {
+ public int solarErrorNumber;
+ public int solarOperatingState;
+ public int solarCollectorTemperature;
+ public int solarBuffer1Temperature;
+ public int solarBuffer2Temperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/SolarReg50Block.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/SolarReg50Block.java
new file mode 100644
index 0000000000000..3d8c4f985994b
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/dto/SolarReg50Block.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.dto;
+
+/**
+ * Dto class for the SolarReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+
+public class SolarReg50Block {
+ public int solarMaximumBufferTemperature;
+ public int solarBufferChangeoverTemperature;
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/BoilerHandler.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/BoilerHandler.java
new file mode 100644
index 0000000000000..c4edbae9b6ce5
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/BoilerHandler.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.library.unit.Units.*;
+
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
+import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
+import org.openhab.binding.modbus.lambda.internal.BoilerConfiguration;
+import org.openhab.binding.modbus.lambda.internal.dto.BoilerBlock;
+import org.openhab.binding.modbus.lambda.internal.dto.BoilerReg50Block;
+import org.openhab.binding.modbus.lambda.internal.parser.BoilerBlockParser;
+import org.openhab.binding.modbus.lambda.internal.parser.BoilerReg50BlockParser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
+import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
+import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
+import org.openhab.core.io.transport.modbus.PollTask;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BoilerHandler} is responsible for handling commands,
+ * which are sent to one of the channels and for polling the modbus.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class BoilerHandler extends BaseThingHandler {
+
+ public abstract class AbstractBasePoller {
+
+ private final Logger logger = LoggerFactory.getLogger(BoilerHandler.class);
+
+ private volatile @Nullable PollTask pollTask;
+
+ public synchronized void unregisterPollTask() {
+ PollTask task = pollTask;
+ if (task == null) {
+ return;
+ }
+
+ ModbusCommunicationInterface mycomms = BoilerHandler.this.comms;
+ if (mycomms != null) {
+ mycomms.unregisterRegularPoll(task);
+ }
+ pollTask = null;
+ }
+
+ /**
+ * Register poll task This is where we set up our regular poller
+ */
+ public synchronized void registerPollTask(int address, int length, ModbusReadFunctionCode readFunctionCode) {
+ logger.debug("Setting up regular polling Address: {}", address);
+
+ ModbusCommunicationInterface mycomms = BoilerHandler.this.comms;
+ BoilerConfiguration myconfig = Objects.requireNonNull(BoilerHandler.this.config);
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("Boiler: registerPollTask called without proper configuration");
+ }
+
+ ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), readFunctionCode, address,
+ length, myconfig.getMaxTries());
+
+ long refreshMillis = myconfig.getRefreshMillis();
+
+ pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
+ result.getRegisters().ifPresent(this::handlePolledData);
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }, BoilerHandler.this::handleReadError);
+ }
+
+ public synchronized void poll() {
+ PollTask task = pollTask;
+ ModbusCommunicationInterface mycomms = BoilerHandler.this.comms;
+ if (task != null && mycomms != null) {
+ mycomms.submitOneTimePoll(task.getRequest(), task.getResultCallback(), task.getFailureCallback());
+ }
+ }
+
+ protected abstract void handlePolledData(ModbusRegisterArray registers);
+ }
+
+ /**
+ * Logger instance
+ */
+ private final Logger logger = LoggerFactory.getLogger(BoilerHandler.class);
+
+ /**
+ * Configuration instance
+ */
+ protected @Nullable BoilerConfiguration config = null;
+
+ /**
+ * Parsers used to convert incoming raw messages into state blocks
+ */
+
+ private final BoilerBlockParser boilerBlockParser = new BoilerBlockParser();
+ private final BoilerReg50BlockParser boilerReg50BlockParser = new BoilerReg50BlockParser();
+
+ /**
+ * These are the tasks used to poll the device
+ */
+
+ private volatile @Nullable AbstractBasePoller boilerPoller = null;
+ private volatile @Nullable AbstractBasePoller boilerReg50Poller = null;
+
+ /**
+ * Communication interface to the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+ /**
+ * This is the slave id, we store this once initialization is complete
+ */
+ private volatile int slaveId;
+
+ /**
+ * Instances of this handler should get a reference to the modbus manager
+ *
+ * @param thing the thing to handle
+ */
+ public BoilerHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * @param address address of the value to be written on the modbus
+ *
+ * @param shortValue value to be written on the modbus
+ */
+ protected void writeInt16(int address, short shortValue) {
+ BoilerConfiguration myconfig = Objects.requireNonNull(BoilerHandler.this.config);
+ ModbusCommunicationInterface mycomms = BoilerHandler.this.comms;
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+ // big endian byte ordering
+ byte hi = (byte) (shortValue >> 8);
+ byte lo = (byte) shortValue;
+ ModbusRegisterArray data = new ModbusRegisterArray(hi, lo);
+
+ ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data,
+ true, myconfig.getMaxTries());
+
+ mycomms.submitOneTimeWrite(request, result -> {
+ if (hasConfigurationError()) {
+ return;
+ }
+ BoilerHandler.this.updateStatus(ThingStatus.ONLINE);
+ }, failure -> {
+ BoilerHandler.this.handleWriteError(failure);
+ });
+ }
+
+ private short getScaledInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(CELSIUS);
+ if (c != null) {
+ return (short) (c.doubleValue() * 10);
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return (short) (c.doubleValue() * 10);
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ /**
+ * Handle incoming commands.
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ String groupId = channelUID.getGroupId();
+ if (groupId != null) {
+ AbstractBasePoller poller;
+ switch (groupId) {
+ case GROUP_BOILER:
+ poller = boilerPoller;
+ break;
+ case GROUP_BOILER_REG50:
+ poller = boilerReg50Poller;
+ break;
+ default:
+ poller = null;
+ break;
+ }
+ if (poller != null) {
+ logger.trace("Boiler: Polling initiated");
+ poller.poll();
+ }
+ }
+ } else {
+ try {
+ if (GROUP_BOILER_REG50.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_BOILER_MAXIMUM_BOILER_TEMPERATURE:
+ writeInt16(reg50baseadress, getScaledInt16Value(command));
+ break;
+ }
+ }
+ }
+
+ catch (LambdaException error) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String cls = error.getClass().getName();
+ String msg = error.getMessage();
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with: %s: %s", cls, msg));
+ }
+ }
+ }
+
+ /**
+ * Initialization: Load the config object of the block Connect to the slave
+ * bridge Start the periodic polling
+ */
+ @Override
+ public void initialize() {
+ config = getConfigAs(BoilerConfiguration.class);
+ startUp();
+ }
+
+ /**
+ * Adresses for the polling registers, used for reading and writing
+ */
+ private int baseadress;
+ private int reg50baseadress;
+
+ /**
+ * This method starts the operation of this handler Connect to the slave bridge
+ * Start the periodic polling
+ */
+ private void startUp() {
+ if (comms != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return;
+ }
+
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ logger.debug("Boiler: Error setting up SlaveId");
+ }
+
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface of boiler is null, Thing & Bridge are offline");
+ return;
+ }
+
+ if (config == null) {
+ logger.debug("CommunicationInterface config of boiler is null, Thing & Bridge are offline");
+ return;
+ }
+
+ BoilerConfiguration myconfig = Objects.requireNonNull(BoilerHandler.this.config);
+
+ baseadress = 2000 + 100 * myconfig.getSubindex();
+ reg50baseadress = baseadress + 50;
+
+ if (boilerPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledBoilerData(registers);
+ }
+ };
+ poller.registerPollTask(baseadress, 4, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ boilerPoller = poller;
+ }
+
+ if (boilerReg50Poller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledBoilerReg50Data(registers);
+ }
+ };
+
+ poller.registerPollTask(reg50baseadress, 1, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ boilerReg50Poller = poller;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ /**
+ * Dispose the binding correctly
+ */
+ @Override
+ public void dispose() {
+ tearDown();
+ }
+
+ /**
+ * Unregister the poll tasks and release the endpoint reference
+ */
+ private void tearDown() {
+ AbstractBasePoller poller = boilerPoller;
+
+ if (poller != null) {
+ poller.unregisterPollTask();
+ boilerPoller = null;
+ }
+ poller = boilerReg50Poller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ boilerReg50Poller = null;
+ }
+
+ comms = null;
+ }
+
+ /**
+ * Returns the current slave id from the bridge
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * Get the endpoint handler from the bridge this handler is connected to Checks
+ * that we're connected to the right type of bridge
+ *
+ * @return the endpoint handler or null if the bridge does not exist
+ */
+ private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.debug("Bridge is null");
+ return null;
+ }
+ if (bridge.getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Bridge is not online");
+ return null;
+ }
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.debug("Bridge handler is null");
+ return null;
+ }
+
+ if (handler instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ throw new IllegalStateException("Unexpected bridge handler: " + handler.toString());
+ }
+ }
+
+ protected State getScaled(Number value, Unit> unit, Double pow) {
+ double factor = Math.pow(10, pow);
+ return QuantityType.valueOf(value.doubleValue() * factor, unit);
+ }
+
+ protected State getUnscaled(Number value, Unit> unit) {
+ return QuantityType.valueOf(value.doubleValue(), unit);
+ }
+
+ /**
+ * Returns high value * 1000 + low value
+ *
+ * @param high the high value
+ *
+ * @param low the low value
+ *
+ * @return the scaled value as a DecimalType
+ */
+ protected State getEnergyQuantity(int high, int low) {
+ double value = high * 1000 + low;
+ return QuantityType.valueOf(value, KILOWATT_HOUR);
+ }
+
+ /**
+ * These methods are called each time new data has been polled from the modbus
+ * slave The register array is first parsed, then each of the channels are
+ * updated to the new values
+ *
+ * @param registers byte array read from the modbus slave
+ */
+
+ protected void handlePolledBoilerData(ModbusRegisterArray registers) {
+ BoilerBlock block = boilerBlockParser.parse(registers);
+
+ // Boiler group
+ updateState(channelUID(GROUP_BOILER, CHANNEL_BOILER_ERROR_NUMBER), new DecimalType(block.boilerErrorNumber));
+ updateState(channelUID(GROUP_BOILER, CHANNEL_BOILER_OPERATING_STATE),
+ new DecimalType(block.boilerOperatingState));
+ updateState(channelUID(GROUP_BOILER, CHANNEL_BOILER_ACTUAL_HIGH_TEMPERATURE),
+ getScaled(block.boilerActualHighTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BOILER, CHANNEL_BOILER_ACTUAL_LOW_TEMPERATURE),
+ getScaled(block.boilerActualLowTemperature, CELSIUS, -1.0));
+ resetCommunicationError();
+ }
+
+ protected void handlePolledBoilerReg50Data(ModbusRegisterArray registers) {
+ BoilerReg50Block block = boilerReg50BlockParser.parse(registers);
+
+ // BoilerReg50 groupaximumBoilerTemperature: {}",
+ updateState(channelUID(GROUP_BOILER_REG50, CHANNEL_BOILER_MAXIMUM_BOILER_TEMPERATURE),
+ getScaled(block.boilerMaximumBoilerTemperature, CELSIUS, -1.0));
+ resetCommunicationError();
+ }
+
+ /**
+ * @param bridgeStatusInfo
+ */
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ startUp();
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ tearDown();
+ }
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleReadError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with read: %s: %s", cls, msg));
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleWriteError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with write: %s: %s", cls, msg));
+ }
+
+ /**
+ * Returns true, if we're in a CONFIGURATION_ERROR state
+ *
+ * @return
+ */
+ protected boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ /**
+ * Reset communication status to ONLINE if we're in an OFFLINE state
+ */
+ protected void resetCommunicationError() {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
+ && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ /**
+ * Returns the channel UID for the specified group and channel id
+ *
+ * @param string the channel group
+ *
+ * @param string the channel id in that group
+ *
+ * @return the globally unique channel uid
+ */
+ ChannelUID channelUID(String group, String id) {
+ return new ChannelUID(getThing().getUID(), group, id);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/BufferHandler.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/BufferHandler.java
new file mode 100644
index 0000000000000..e4b727bd2ad32
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/BufferHandler.java
@@ -0,0 +1,582 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.library.unit.Units.*;
+
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
+import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
+import org.openhab.binding.modbus.lambda.internal.BufferConfiguration;
+import org.openhab.binding.modbus.lambda.internal.dto.BufferBlock;
+import org.openhab.binding.modbus.lambda.internal.dto.BufferReg50Block;
+import org.openhab.binding.modbus.lambda.internal.parser.BufferBlockParser;
+import org.openhab.binding.modbus.lambda.internal.parser.BufferReg50BlockParser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
+import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
+import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
+import org.openhab.core.io.transport.modbus.PollTask;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BufferHandler} is responsible for handling commands,
+ * which are sent to one of the channels and for polling the modbus.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class BufferHandler extends BaseThingHandler {
+
+ public abstract class AbstractBasePoller {
+
+ private volatile @Nullable PollTask pollTask;
+
+ public synchronized void unregisterPollTask() {
+ PollTask task = pollTask;
+ if (task == null) {
+ return;
+ }
+
+ ModbusCommunicationInterface mycomms = BufferHandler.this.comms;
+ if (mycomms != null) {
+ mycomms.unregisterRegularPoll(task);
+ }
+ pollTask = null;
+ }
+
+ /**
+ * Register poll task This is where we set up our regular poller
+ */
+ public synchronized void registerPollTask(int address, int length, ModbusReadFunctionCode readFunctionCode) {
+ ModbusCommunicationInterface mycomms = BufferHandler.this.comms;
+ BufferConfiguration myconfig = Objects.requireNonNull(BufferHandler.this.config);
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+
+ ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), readFunctionCode, address,
+ length, myconfig.getMaxTries());
+
+ long refreshMillis = myconfig.getRefreshMillis();
+
+ pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
+ result.getRegisters().ifPresent(this::handlePolledData);
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }, BufferHandler.this::handleReadError);
+ }
+
+ public synchronized void poll() {
+ PollTask task = pollTask;
+ ModbusCommunicationInterface mycomms = BufferHandler.this.comms;
+ if (task != null && mycomms != null) {
+ mycomms.submitOneTimePoll(task.getRequest(), task.getResultCallback(), task.getFailureCallback());
+ }
+ }
+
+ protected abstract void handlePolledData(ModbusRegisterArray registers);
+ }
+
+ /**
+ * Logger instance
+ */
+ private final Logger logger = LoggerFactory.getLogger(BufferHandler.class);
+
+ /**
+ * Configuration instance
+ */
+ protected @Nullable BufferConfiguration config = null;
+
+ /**
+ * Parsers used to convert incoming raw messages into state blocks
+ */
+
+ private final BufferBlockParser bufferBlockParser = new BufferBlockParser();
+ private final BufferReg50BlockParser bufferReg50BlockParser = new BufferReg50BlockParser();
+
+ /**
+ * These are the tasks used to poll the device
+ */
+ private volatile @Nullable AbstractBasePoller bufferPoller = null;
+ private volatile @Nullable AbstractBasePoller bufferReg50Poller = null;
+ /**
+ * Communication interface to the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+ /**
+ * This is the slave id, we store this once initialization is complete
+ */
+ private volatile int slaveId;
+
+ /**
+ * Instances of this handler should get a reference to the modbus manager
+ *
+ * @param thing the thing to handle
+ */
+ public BufferHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * @param address address of the value to be written on the modbus
+ *
+ * @param shortValue value to be written on the modbus
+ */
+ protected void writeInt16(int address, short shortValue) {
+ BufferConfiguration myconfig = Objects.requireNonNull(BufferHandler.this.config);
+ ModbusCommunicationInterface mycomms = BufferHandler.this.comms;
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+ // big endian byte ordering
+ byte hi = (byte) (shortValue >> 8);
+ byte lo = (byte) shortValue;
+ ModbusRegisterArray data = new ModbusRegisterArray(hi, lo);
+
+ ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data,
+ true, myconfig.getMaxTries());
+
+ mycomms.submitOneTimeWrite(request, result -> {
+ if (hasConfigurationError()) {
+ return;
+ }
+ BufferHandler.this.updateStatus(ThingStatus.ONLINE);
+ }, failure -> {
+ BufferHandler.this.handleWriteError(failure);
+ });
+ }
+
+ /**
+ * @param command get the value of this command.
+ *
+ * @return short the value of the command as short
+ */
+ private short getInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(WATT);
+ if (c != null) {
+ return c.shortValue();
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return c.shortValue();
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ private short getScaledInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(CELSIUS);
+ if (c != null) {
+ return (short) (c.doubleValue() * 10);
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return (short) (c.doubleValue() * 10);
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ /**
+ * Handle incoming commands.
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ String groupId = channelUID.getGroupId();
+ if (groupId != null) {
+ AbstractBasePoller poller;
+ switch (groupId) {
+ case GROUP_BUFFER:
+ poller = bufferPoller;
+ break;
+ case GROUP_BUFFER_REG50:
+ poller = bufferReg50Poller;
+ break;
+ default:
+ poller = null;
+ break;
+ }
+ if (poller != null) {
+ logger.trace("Buffer: Polling initiated");
+ poller.poll();
+ }
+ }
+ } else {
+ try {
+ if (GROUP_BUFFER.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_BUFFER_ACTUAL_MODBUS_TEMPERATURE:
+ writeInt16(baseadress + 4, getScaledInt16Value(command));
+ break;
+ case CHANNEL_BUFFER_REQUEST_TYPE:
+ writeInt16(baseadress + 5, getInt16Value(command));
+ break;
+ case CHANNEL_BUFFER_REQUEST_FLOW_LINE_TEMPERATURE:
+ writeInt16(baseadress + 6, getScaledInt16Value(command));
+ break;
+ case CHANNEL_BUFFER_REQUEST_RETURN_LINE_TEMPERATURE:
+ writeInt16(baseadress + 7, getScaledInt16Value(command));
+ break;
+ case CHANNEL_BUFFER_REQUEST_HEAT_SINK_TEMPERATURE:
+ writeInt16(baseadress + 8, getScaledInt16Value(command));
+ break;
+ case CHANNEL_BUFFER_REQUEST_HEATING_CAPACITY:
+ writeInt16(baseadress + 9, getScaledInt16Value(command));
+ break;
+ }
+ }
+
+ if (GROUP_BUFFER_REG50.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_BUFFER_MAXIMUM_BUFFER_TEMPERATURE:
+ writeInt16(reg50baseadress, getScaledInt16Value(command));
+ break;
+ }
+ }
+ }
+
+ catch (LambdaException error) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String cls = error.getClass().getName();
+ String msg = error.getMessage();
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with: %s: %s", cls, msg));
+ }
+ }
+ }
+
+ /**
+ * Initialization: Load the config object of the block Connect to the slave
+ * bridge Start the periodic polling
+ */
+ @Override
+ public void initialize() {
+ config = getConfigAs(BufferConfiguration.class);
+
+ startUp();
+ }
+
+ /**
+ * Adresses for the polling registers, used for reading and writing
+ */
+ private int baseadress;
+ private int reg50baseadress;
+
+ /**
+ * This method starts the operation of this handler Connect to the slave bridge
+ * Start the periodic polling1
+ */
+ private void startUp() {
+ if (comms != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return;
+ }
+
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ logger.debug("Buffer: Error setting up SlaveId");
+ }
+
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface of buffer is null, Thing & Bridge are offline");
+ return;
+ }
+
+ if (config == null) {
+ logger.debug("Invalid comms/config/manager ref for Buffer handler");
+ return;
+ }
+
+ BufferConfiguration myconfig = Objects.requireNonNull(BufferHandler.this.config);
+
+ baseadress = 3000 + 100 * myconfig.getSubindex();
+ reg50baseadress = baseadress + 50;
+
+ if (bufferPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledBufferData(registers);
+ }
+ };
+
+ poller.registerPollTask(baseadress, 10, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ bufferPoller = poller;
+ }
+ if (bufferReg50Poller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledBufferReg50Data(registers);
+ }
+ };
+
+ poller.registerPollTask(reg50baseadress, 1, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ bufferReg50Poller = poller;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ /**
+ * Dispose the binding correctly
+ */
+ @Override
+ public void dispose() {
+ tearDown();
+ }
+
+ /**
+ * Unregister the poll tasks and release the endpoint reference
+ */
+ private void tearDown() {
+ AbstractBasePoller poller = bufferPoller;
+
+ if (poller != null) {
+ poller.unregisterPollTask();
+ bufferPoller = null;
+ }
+ poller = bufferReg50Poller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ bufferReg50Poller = null;
+ }
+
+ comms = null;
+ }
+
+ /**
+ * Returns the current slave id from the bridge
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * Get the endpoint handler from the bridge this handler is connected to Checks
+ * that we're connected to the right type of bridge
+ *
+ * @return the endpoint handler or null if the bridge does not exist
+ */
+ private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.debug("Bridge is null");
+ return null;
+ }
+
+ if (bridge.getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Bridge is not online");
+ return null;
+ }
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.debug("Bridge handler is null");
+ return null;
+ }
+
+ if (handler instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ throw new IllegalStateException("Unexpected bridge handler: " + handler.toString());
+ }
+ }
+
+ protected State getScaled(Number value, Unit> unit, Double pow) {
+ double factor = Math.pow(10, pow);
+ return QuantityType.valueOf(value.doubleValue() * factor, unit);
+ }
+
+ protected State getUnscaled(Number value, Unit> unit) {
+ return QuantityType.valueOf(value.doubleValue(), unit);
+ }
+
+ /**
+ * Returns high value * 1000 + low value
+ *
+ * @param high the high value
+ *
+ * @param low the low value
+ *
+ * @return the scaled value as a DecimalType
+ */
+ protected State getEnergyQuantity(int high, int low) {
+ double value = high * 1000 + low;
+ return QuantityType.valueOf(value, KILOWATT_HOUR);
+ }
+
+ /**
+ * These methods are called each time new data has been polled from the modbus
+ * slave The register array is first parsed, then each of the channels are
+ * updated to the new values
+ *
+ * @param registers byte array read from the modbus slave
+ */
+
+ protected void handlePolledBufferData(ModbusRegisterArray registers) {
+ BufferBlock block = bufferBlockParser.parse(registers);
+
+ // Buffer group
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_ERROR_NUMBER), new DecimalType(block.bufferErrorNumber));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_OPERATING_STATE),
+ new DecimalType(block.bufferOperatingState));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_ACTUAL_HIGH_TEMPERATURE),
+ getScaled(block.bufferActualHighTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_ACTUAL_LOW_TEMPERATURE),
+ getScaled(block.bufferActualLowTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_ACTUAL_MODBUS_TEMPERATURE),
+ getScaled(block.bufferActualModbusTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_REQUEST_TYPE), new DecimalType(block.bufferRequestType));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_REQUEST_FLOW_LINE_TEMPERATURE),
+ getScaled(block.bufferrequestFlowLineTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_REQUEST_RETURN_LINE_TEMPERATURE),
+ getScaled(block.bufferrequestReturnLineTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_REQUEST_HEAT_SINK_TEMPERATURE),
+ getScaled(block.bufferrequestHeatSinkTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_BUFFER, CHANNEL_BUFFER_REQUEST_HEATING_CAPACITY),
+ getScaled(block.bufferrequestHeatingCapacity, WATT, 2.0));
+
+ resetCommunicationError();
+ }
+
+ protected void handlePolledBufferReg50Data(ModbusRegisterArray registers) {
+ BufferReg50Block block = bufferReg50BlockParser.parse(registers);
+
+ // BufferReg50 group
+ updateState(channelUID(GROUP_BUFFER_REG50, CHANNEL_BUFFER_MAXIMUM_BUFFER_TEMPERATURE),
+ getScaled(block.bufferMaximumBufferTemperature, CELSIUS, -1.0));
+ resetCommunicationError();
+ }
+
+ /**
+ * @param bridgeStatusInfo
+ */
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ startUp();
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ tearDown();
+ }
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleReadError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with read: %s: %s", cls, msg));
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleWriteError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with write: %s: %s", cls, msg));
+ }
+
+ /**
+ * Returns true, if we're in a CONFIGURATION_ERROR state
+ *
+ * @return
+ */
+ protected boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ /**
+ * Reset communication status to ONLINE if we're in an OFFLINE state
+ */
+ protected void resetCommunicationError() {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
+ && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ /**
+ * Returns the channel UID for the specified group and channel id
+ *
+ * @param string the channel group
+ *
+ * @param string the channel id in that group
+ *
+ * @return the globally unique channel uid
+ */
+ ChannelUID channelUID(String group, String id) {
+ return new ChannelUID(getThing().getUID(), group, id);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/GeneralHandler.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/GeneralHandler.java
new file mode 100644
index 0000000000000..0fb66df1e96c6
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/GeneralHandler.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.library.unit.Units.*;
+
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
+import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
+import org.openhab.binding.modbus.lambda.internal.GeneralConfiguration;
+import org.openhab.binding.modbus.lambda.internal.dto.AmbientBlock;
+import org.openhab.binding.modbus.lambda.internal.dto.EManagerBlock;
+import org.openhab.binding.modbus.lambda.internal.parser.AmbientBlockParser;
+import org.openhab.binding.modbus.lambda.internal.parser.EManagerBlockParser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
+import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
+import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
+import org.openhab.core.io.transport.modbus.PollTask;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link GeneralHandler} is responsible for handling commands,
+ * which are sent to one of the channels and for polling the modbus.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class GeneralHandler extends BaseThingHandler {
+
+ public abstract class AbstractBasePoller {
+
+ private volatile @Nullable PollTask pollTask;
+
+ public synchronized void unregisterPollTask() {
+ PollTask task = pollTask;
+ if (task == null) {
+ return;
+ }
+
+ ModbusCommunicationInterface mycomms = GeneralHandler.this.comms;
+ if (mycomms != null) {
+ mycomms.unregisterRegularPoll(task);
+ }
+ pollTask = null;
+ }
+
+ /**
+ * Register poll task This is where we set up our regular poller
+ */
+ public synchronized void registerPollTask(int address, int length, ModbusReadFunctionCode readFunctionCode) {
+ ModbusCommunicationInterface mycomms = GeneralHandler.this.comms;
+ GeneralConfiguration myconfig = Objects.requireNonNull(GeneralHandler.this.config);
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+
+ ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), readFunctionCode, address,
+ length, myconfig.getMaxTries());
+
+ long refreshMillis = myconfig.getRefreshMillis();
+
+ pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
+ result.getRegisters().ifPresent(this::handlePolledData);
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }, GeneralHandler.this::handleReadError);
+ }
+
+ public synchronized void poll() {
+ PollTask task = pollTask;
+ ModbusCommunicationInterface mycomms = GeneralHandler.this.comms;
+ if (task != null && mycomms != null) {
+ mycomms.submitOneTimePoll(task.getRequest(), task.getResultCallback(), task.getFailureCallback());
+ }
+ }
+
+ protected abstract void handlePolledData(ModbusRegisterArray registers);
+ }
+
+ /**
+ * Logger instance
+ */
+ private final Logger logger = LoggerFactory.getLogger(GeneralHandler.class);
+
+ /**
+ * Configuration instance
+ */
+ protected @Nullable GeneralConfiguration config = null;
+ /**
+ * Parsers used to convert incoming raw messages into state blocks
+ */
+ private final AmbientBlockParser ambientBlockParser = new AmbientBlockParser();
+ private final EManagerBlockParser emanagerBlockParser = new EManagerBlockParser();
+
+ /**
+ * These are the tasks used to poll the device
+ */
+ private volatile @Nullable AbstractBasePoller ambientPoller = null;
+ private volatile @Nullable AbstractBasePoller emanagerPoller = null;
+
+ /**
+ * Communication interface to the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+ /**
+ * This is the slave id, we store this once initialization is complete
+ */
+ private volatile int slaveId;
+
+ /**
+ * Instances of this handler should get a reference to the modbus manager
+ *
+ * @param thing the thing to handle
+ */
+ public GeneralHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * @param address address of the value to be written on the modbus
+ *
+ * @param shortValue value to be written on the modbus
+ */
+ protected void writeInt16(int address, short shortValue) {
+ GeneralConfiguration myconfig = Objects.requireNonNull(GeneralHandler.this.config);
+ ModbusCommunicationInterface mycomms = GeneralHandler.this.comms;
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+ // big endian byte ordering
+ byte hi = (byte) (shortValue >> 8);
+ byte lo = (byte) shortValue;
+ ModbusRegisterArray data = new ModbusRegisterArray(hi, lo);
+
+ ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data,
+ true, myconfig.getMaxTries());
+
+ mycomms.submitOneTimeWrite(request, result -> {
+ if (hasConfigurationError()) {
+ return;
+ }
+ GeneralHandler.this.updateStatus(ThingStatus.ONLINE);
+ }, failure -> {
+ GeneralHandler.this.handleWriteError(failure);
+ });
+ }
+
+ /**
+ * @param command get the value of this command.
+ *
+ * @return short the value of the command as short
+ */
+ private short getInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(WATT);
+ if (c != null) {
+ return c.shortValue();
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return c.shortValue();
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ /**
+ * Handle incoming commands.
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ String groupId = channelUID.getGroupId();
+ if (groupId != null) {
+ AbstractBasePoller poller;
+ switch (groupId) {
+ case GROUP_GENERAL_AMBIENT:
+ poller = ambientPoller;
+ break;
+ case GROUP_GENERAL_EMANAGER:
+ poller = emanagerPoller;
+ break;
+ default:
+ poller = null;
+ break;
+ }
+ if (poller != null) {
+ logger.trace("General: Polling initiated");
+ poller.poll();
+ }
+ }
+ } else {
+ try {
+ if (GROUP_GENERAL_AMBIENT.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_ACTUAL_AMBIENT_TEMPERATURE:
+ writeInt16(2, getInt16Value(command));
+ break;
+ }
+ }
+
+ if (GROUP_GENERAL_EMANAGER.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_EMANAGER_ACTUAL_POWER:
+ writeInt16(102, getInt16Value(command));
+ break;
+ case CHANNEL_EMANAGER_ACTUAL_POWER_SIGNED:
+ writeInt16(102, getInt16Value(command));
+ break;
+ }
+ }
+ }
+
+ catch (LambdaException error) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String cls = error.getClass().getName();
+ String msg = error.getMessage();
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with: %s: %s", cls, msg));
+ }
+ }
+ }
+
+ /**
+ * Initialization: Load the config object of the block Connect to the slave
+ * bridge Start the periodic polling
+ */
+ @Override
+ public void initialize() {
+ config = getConfigAs(GeneralConfiguration.class);
+
+ logger.debug("Initializing thing with properties: {}", thing.getProperties());
+
+ startUp();
+ }
+
+ /**
+ * This method starts the operation of this handler Connect to the slave bridge
+ * Start the periodic polling1
+ */
+ private void startUp() {
+ if (comms != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return;
+ }
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ logger.debug("General: Error setting up SlaveId");
+ }
+
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface of general section is null, Thing & Bridge are offline");
+ return;
+ }
+
+ if (config == null) {
+ logger.debug("Invalid comms/config/manager ref for lambda general handler");
+ return;
+ }
+
+ if (ambientPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledAmbientData(registers);
+ }
+ };
+ poller.registerPollTask(0, 5, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ ambientPoller = poller;
+ }
+
+ if (emanagerPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledEManagerData(registers);
+ }
+ };
+ poller.registerPollTask(100, 5, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ emanagerPoller = poller;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ /**
+ * Dispose the binding correctly
+ */
+ @Override
+ public void dispose() {
+ tearDown();
+ }
+
+ /**
+ * Unregister the poll tasks and release the endpoint reference
+ */
+ private void tearDown() {
+ AbstractBasePoller poller = ambientPoller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ ambientPoller = null;
+ }
+
+ poller = emanagerPoller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ emanagerPoller = null;
+ }
+
+ comms = null;
+ }
+
+ /**
+ * Returns the current slave id from the bridge
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * Get the endpoint handler from the bridge this handler is connected to Checks
+ * that we're connected to the right type of bridge
+ *
+ * @return the endpoint handler or null if the bridge does not exist
+ */
+ private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.debug("Bridge is null");
+ return null;
+ }
+ if (bridge.getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Bridge is not online");
+ return null;
+ }
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.debug("Bridge handler is null");
+ return null;
+ }
+
+ if (handler instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ throw new IllegalStateException("Unexpected bridge handler: " + handler.toString());
+ }
+ }
+
+ protected State getScaled(Number value, Unit> unit, Double pow) {
+ double factor = Math.pow(10, pow);
+ return QuantityType.valueOf(value.doubleValue() * factor, unit);
+ }
+
+ protected State getUnscaled(Number value, Unit> unit) {
+ return QuantityType.valueOf(value.doubleValue(), unit);
+ }
+
+ /**
+ * Returns high value * 1000 + low value
+ *
+ * @param high the high value
+ *
+ * @param low the low value
+ *
+ * @return the scaled value as a DecimalType
+ */
+ protected State getEnergyQuantity(int high, int low) {
+ double value = high * 1000 + low;
+ return QuantityType.valueOf(value, KILOWATT_HOUR);
+ }
+
+ /**
+ * These methods are called each time new data has been polled from the modbus
+ * slave The register array is first parsed, then each of the channels are
+ * updated to the new values
+ *
+ * @param registers byte array read from the modbus slave
+ */
+ protected void handlePolledAmbientData(ModbusRegisterArray registers) {
+ // Ambient group
+ AmbientBlock block = ambientBlockParser.parse(registers);
+
+ updateState(channelUID(GROUP_GENERAL_AMBIENT, CHANNEL_AMBIENT_ERROR_NUMBER),
+ new DecimalType(block.ambientErrorNumber));
+ updateState(channelUID(GROUP_GENERAL_AMBIENT, CHANNEL_AMBIENT_OPERATING_STATE),
+ new DecimalType(block.ambientOperatingState));
+ updateState(channelUID(GROUP_GENERAL_AMBIENT, CHANNEL_ACTUAL_AMBIENT_TEMPERATURE),
+ getScaled(block.actualAmbientTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_GENERAL_AMBIENT, CHANNEL_AVERAGE_AMBIENT_TEMPERATURE),
+ getScaled(block.averageAmbientTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_GENERAL_AMBIENT, CHANNEL_CALCULATED_AMBIENT_TEMPERATURE),
+ getScaled(block.calculatedAmbientTemperature, CELSIUS, -1.0));
+ resetCommunicationError();
+ }
+
+ protected void handlePolledEManagerData(ModbusRegisterArray registers) {
+ // EManager group
+ EManagerBlock block = emanagerBlockParser.parse(registers);
+ updateState(channelUID(GROUP_GENERAL_EMANAGER, CHANNEL_EMANAGER_ERROR_NUMBER),
+ new DecimalType(block.emanagerErrorNumber));
+ updateState(channelUID(GROUP_GENERAL_EMANAGER, CHANNEL_EMANAGER_OPERATING_STATE),
+ new DecimalType(block.emanagerOperatingState));
+ updateState(channelUID(GROUP_GENERAL_EMANAGER, CHANNEL_EMANAGER_ACTUAL_POWER_CONSUMPTION),
+ getUnscaled(block.emanagerActualPowerConsumption, WATT));
+ updateState(channelUID(GROUP_GENERAL_EMANAGER, CHANNEL_EMANAGER_ACTUAL_POWER),
+ getUnscaled(block.emanagerActualPower, WATT));
+ updateState(channelUID(GROUP_GENERAL_EMANAGER, CHANNEL_EMANAGER_ACTUAL_POWER_SIGNED),
+ getUnscaled(block.emanagerActualPowerSigned, WATT));
+ updateState(channelUID(GROUP_GENERAL_EMANAGER, CHANNEL_POWER_CONSUMPTION_SETPOINT),
+ getUnscaled(block.emanagerPowerConsumptionSetpoint, WATT));
+ resetCommunicationError();
+ }
+
+ /**
+ * @param bridgeStatusInfo
+ */
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ startUp();
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ tearDown();
+ }
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleReadError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with read: %s: %s", cls, msg));
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleWriteError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with write: %s: %s", cls, msg));
+ }
+
+ /**
+ * Returns true, if we're in a CONFIGURATION_ERROR state
+ *
+ * @return
+ */
+ protected boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ /**
+ * Reset communication status to ONLINE if we're in an OFFLINE state
+ */
+ protected void resetCommunicationError() {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
+ && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ /**
+ * Returns the channel UID for the specified group and channel id
+ *
+ * @param string the channel group
+ *
+ * @param string the channel id in that group
+ *
+ * @return the globally unique channel uid
+ */
+ ChannelUID channelUID(String group, String id) {
+ return new ChannelUID(getThing().getUID(), group, id);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/HeatingCircuitHandler.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/HeatingCircuitHandler.java
new file mode 100644
index 0000000000000..33891eec9e690
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/HeatingCircuitHandler.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.library.unit.Units.*;
+
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
+import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
+import org.openhab.binding.modbus.lambda.internal.HeatingCircuitConfiguration;
+import org.openhab.binding.modbus.lambda.internal.dto.HeatingCircuitBlock;
+import org.openhab.binding.modbus.lambda.internal.dto.HeatingCircuitReg50Block;
+import org.openhab.binding.modbus.lambda.internal.parser.HeatingCircuitBlockParser;
+import org.openhab.binding.modbus.lambda.internal.parser.HeatingCircuitReg50BlockParser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
+import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
+import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
+import org.openhab.core.io.transport.modbus.PollTask;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HeatingCircuitHandler} is responsible for handling commands,
+ * which are sent to one of the channels and for polling the modbus.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class HeatingCircuitHandler extends BaseThingHandler {
+
+ public abstract class AbstractBasePoller {
+
+ private final Logger logger = LoggerFactory.getLogger(HeatingCircuitHandler.class);
+
+ private volatile @Nullable PollTask pollTask;
+
+ public synchronized void unregisterPollTask() {
+ PollTask task = pollTask;
+ if (task == null) {
+ return;
+ }
+
+ ModbusCommunicationInterface mycomms = HeatingCircuitHandler.this.comms;
+ if (mycomms != null) {
+ mycomms.unregisterRegularPoll(task);
+ }
+ pollTask = null;
+ }
+
+ /**
+ * Register poll task This is where we set up our regular poller
+ */
+ public synchronized void registerPollTask(int address, int length, ModbusReadFunctionCode readFunctionCode) {
+ logger.debug("Setting up regular polling Address: {}", address);
+
+ ModbusCommunicationInterface mycomms = HeatingCircuitHandler.this.comms;
+ HeatingCircuitConfiguration myconfig = Objects.requireNonNull(HeatingCircuitHandler.this.config);
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("HeatingCircuit: registerPollTask called without proper configuration");
+ }
+
+ ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), readFunctionCode, address,
+ length, myconfig.getMaxTries());
+
+ long refreshMillis = myconfig.getRefreshMillis();
+
+ pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
+ result.getRegisters().ifPresent(this::handlePolledData);
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }, HeatingCircuitHandler.this::handleReadError);
+ }
+
+ public synchronized void poll() {
+ PollTask task = pollTask;
+ ModbusCommunicationInterface mycomms = HeatingCircuitHandler.this.comms;
+ if (task != null && mycomms != null) {
+ mycomms.submitOneTimePoll(task.getRequest(), task.getResultCallback(), task.getFailureCallback());
+ }
+ }
+
+ protected abstract void handlePolledData(ModbusRegisterArray registers);
+ }
+
+ /**
+ * Logger instance
+ */
+ private final Logger logger = LoggerFactory.getLogger(HeatingCircuitHandler.class);
+
+ /**
+ * Configuration instance
+ */
+ protected @Nullable HeatingCircuitConfiguration config = null;
+
+ /**
+ * Parsers used to convert incoming raw messages into state blocks
+ */
+
+ private final HeatingCircuitBlockParser heatingcircuitBlockParser = new HeatingCircuitBlockParser();
+ private final HeatingCircuitReg50BlockParser heatingcircuitReg50BlockParser = new HeatingCircuitReg50BlockParser();
+
+ /**
+ * These are the tasks used to poll the device
+ */
+
+ private volatile @Nullable AbstractBasePoller heatingcircuitPoller = null;
+ private volatile @Nullable AbstractBasePoller heatingcircuitReg50Poller = null;
+ /**
+ * Communication interface to the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+ /**
+ * This is the slave id, we store this once initialization is complete
+ */
+ private volatile int slaveId;
+
+ /**
+ * Instances of this handler should get a reference to the modbus manager
+ *
+ * @param thing the thing to handle
+ */
+ public HeatingCircuitHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * @param address address of the value to be written on the modbus
+ *
+ * @param shortValue value to be written on the modbus
+ */
+ protected void writeInt16(int address, short shortValue) {
+ HeatingCircuitConfiguration myconfig = Objects.requireNonNull(HeatingCircuitHandler.this.config);
+ ModbusCommunicationInterface mycomms = HeatingCircuitHandler.this.comms;
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+ // big endian byte ordering
+ byte hi = (byte) (shortValue >> 8);
+ byte lo = (byte) shortValue;
+ ModbusRegisterArray data = new ModbusRegisterArray(hi, lo);
+
+ ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data,
+ true, myconfig.getMaxTries());
+
+ mycomms.submitOneTimeWrite(request, result -> {
+ if (hasConfigurationError()) {
+ return;
+ }
+ HeatingCircuitHandler.this.updateStatus(ThingStatus.ONLINE);
+ }, failure -> {
+ HeatingCircuitHandler.this.handleWriteError(failure);
+ });
+ }
+
+ /**
+ * @param command get the value of this command.
+ *
+ * @return short the value of the command as short
+ */
+ private short getInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType> quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(WATT);
+ if (c != null) {
+ return c.shortValue();
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return c.shortValue();
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ private short getScaledInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType> quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(CELSIUS);
+ if (c != null) {
+ return (short) (c.doubleValue() * 10);
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return (short) (c.doubleValue() * 10);
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ /**
+ * Handle incoming commands.
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ String groupId = channelUID.getGroupId();
+ if (groupId != null) {
+ AbstractBasePoller poller;
+ switch (groupId) {
+ case GROUP_HEATING_CIRCUIT:
+ poller = heatingcircuitPoller;
+ break;
+ case GROUP_HEATING_CIRCUIT_REG50:
+ poller = heatingcircuitReg50Poller;
+ break;
+ default:
+ poller = null;
+ break;
+ }
+ if (poller != null) {
+ logger.trace("HeatingCircuit: Polling initiated");
+ poller.poll();
+ }
+ }
+ } else {
+ try {
+ if (GROUP_HEATING_CIRCUIT.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_HEATING_CIRCUIT_ROOM_DEVICE_TEMPERATURE:
+ writeInt16(baseadress + 4, getScaledInt16Value(command));
+ break;
+ case CHANNEL_HEATING_CIRCUIT_SETPOINT_FLOW_LINE_TEMPERATURE:
+ writeInt16(baseadress + 5, getScaledInt16Value(command));
+ break;
+ case CHANNEL_HEATING_CIRCUIT_OPERATING_MODE:
+ writeInt16(baseadress + 6, getInt16Value(command));
+ break;
+
+ }
+ }
+
+ if (GROUP_HEATING_CIRCUIT_REG50.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_HEATING_CIRCUIT_OFFSET_FLOW_LINE_TEMPERATURE:
+ writeInt16(reg50baseadress, getScaledInt16Value(command));
+ break;
+ case CHANNEL_HEATING_CIRCUIT_ROOM_HEATING_TEMPERATURE:
+ writeInt16(reg50baseadress + 1, getScaledInt16Value(command));
+ break;
+ case CHANNEL_HEATING_CIRCUIT_ROOM_COOLING_TEMPERATURE:
+ writeInt16(reg50baseadress + 2, getScaledInt16Value(command));
+ break;
+
+ }
+
+ }
+
+ } catch (LambdaException error) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String cls = error.getClass().getName();
+ String msg = error.getMessage();
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with: %s: %s", cls, msg));
+ }
+ }
+ }
+
+ /**
+ * Initialization: Load the config object of the block Connect to the slave
+ * bridge Start the periodic polling
+ */
+ @Override
+ public void initialize() {
+ config = getConfigAs(HeatingCircuitConfiguration.class);
+
+ logger.debug("Initializing thing with properties: {}", thing.getProperties());
+
+ startUp();
+ }
+
+ /**
+ * Adresses for the polling registers, used for reading and writing
+ */
+ private int baseadress;
+ private int reg50baseadress;
+
+ /**
+ * This method starts the operation of this handler Connect to the slave bridge
+ * Start the periodic polling1
+ */
+ private void startUp() {
+ if (comms != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return;
+ }
+
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ logger.debug("HeatingCircuit: Error setting up SlaveId");
+
+ }
+
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface of Heating Circuit is null, Thing & Bridge are offline");
+ return;
+ }
+
+ if (config == null) {
+ logger.debug("Invalid comms/config/manager ref for lambda heatingcircuit handler");
+ return;
+ }
+
+ HeatingCircuitConfiguration myconfig = Objects.requireNonNull(HeatingCircuitHandler.this.config);
+
+ baseadress = 5000 + 100 * myconfig.getSubindex();
+ reg50baseadress = baseadress + 50;
+
+ if (heatingcircuitPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledHeatingData(registers);
+ }
+ };
+ // neu: poller.registerPollTask(baseadress, 8, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ poller.registerPollTask(baseadress, 7, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ heatingcircuitPoller = poller;
+ }
+ if (heatingcircuitReg50Poller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledHeatingReg50Data(registers);
+ }
+ };
+
+ poller.registerPollTask(reg50baseadress, 3, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ heatingcircuitReg50Poller = poller;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ /**
+ * Dispose the binding correctly
+ */
+ @Override
+ public void dispose() {
+ tearDown();
+ }
+
+ /**
+ * Unregister the poll tasks and release the endpoint reference
+ */
+ private void tearDown() {
+ AbstractBasePoller poller = heatingcircuitPoller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ heatingcircuitPoller = null;
+ }
+
+ poller = heatingcircuitReg50Poller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ heatingcircuitReg50Poller = null;
+ }
+ comms = null;
+ }
+
+ /**
+ * Returns the current slave id from the bridge
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * Get the endpoint handler from the bridge this handler is connected to Checks
+ * that we're connected to the right type of bridge
+ *
+ * @return the endpoint handler or null if the bridge does not exist
+ */
+ private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.debug("Bridge is null");
+ return null;
+ }
+ if (bridge.getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Bridge is not online");
+ return null;
+ }
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.debug("Bridge handler is null");
+ return null;
+ }
+
+ if (handler instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ throw new IllegalStateException("Unexpected bridge handler: " + handler.toString());
+ }
+ }
+
+ protected State getScaled(Number value, Unit> unit, Double pow) {
+ double factor = Math.pow(10, pow);
+ return QuantityType.valueOf(value.doubleValue() * factor, unit);
+ }
+
+ protected State getUnscaled(Number value, Unit> unit) {
+ return QuantityType.valueOf(value.doubleValue(), unit);
+ }
+
+ /**
+ * Returns high value * 1000 + low value
+ *
+ * @param high the high value
+ *
+ * @param low the low value
+ *
+ * @return the scaled value as a DecimalType
+ */
+ protected State getEnergyQuantity(int high, int low) {
+ double value = high * 1000 + low;
+ return QuantityType.valueOf(value, KILOWATT_HOUR);
+ }
+
+ /**
+ * These methods are called each time new data has been polled from the modbus
+ * slave The register array is first parsed, then each of the channels are
+ * updated to the new values
+ *
+ * @param registers byte array read from the modbus slave
+ */
+
+ protected void handlePolledHeatingData(ModbusRegisterArray registers) {
+ HeatingCircuitBlock block = heatingcircuitBlockParser.parse(registers);
+
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_ERROR_NUMBER),
+ new DecimalType(block.heatingcircuitErrorNumber));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_OPERATING_STATE),
+ new DecimalType(block.heatingcircuitOperatingState));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_FLOW_LINE_TEMPERATURE),
+ getScaled(block.heatingcircuitFlowLineTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_RETURN_LINE_TEMPERATURE),
+ getScaled(block.heatingcircuitReturnLineTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_ROOM_DEVICE_TEMPERATURE),
+ getScaled(block.heatingcircuitRoomDeviceTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_SETPOINT_FLOW_LINE_TEMPERATURE),
+ getScaled(block.heatingcircuitSetpointFlowLineTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_OPERATING_MODE),
+ new DecimalType(block.heatingcircuitOperatingMode));
+ // neu: updateState(channelUID(GROUP_HEATING_CIRCUIT, CHANNEL_HEATING_CIRCUIT_TARGET_TEMPERATURE_FLOW_LINE),
+ // neu: getScaled(block.heatingcircuitTargetTemperatureFlowLine, CELSIUS, -1.0));
+
+ resetCommunicationError();
+ }
+
+ protected void handlePolledHeatingReg50Data(ModbusRegisterArray registers) {
+ HeatingCircuitReg50Block block = heatingcircuitReg50BlockParser.parse(registers);
+
+ // HeatingCircuit1Settting group
+
+ updateState(channelUID(GROUP_HEATING_CIRCUIT_REG50, CHANNEL_HEATING_CIRCUIT_OFFSET_FLOW_LINE_TEMPERATURE),
+ getScaled(block.heatingcircuitOffsetFlowLineTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT_REG50, CHANNEL_HEATING_CIRCUIT_ROOM_HEATING_TEMPERATURE),
+ getScaled(block.heatingcircuitRoomHeatingTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEATING_CIRCUIT_REG50, CHANNEL_HEATING_CIRCUIT_ROOM_COOLING_TEMPERATURE),
+ getScaled(block.heatingcircuitRoomCoolingTemperature, CELSIUS, -1.0));
+ resetCommunicationError();
+ }
+
+ /**
+ * @param bridgeStatusInfo
+ */
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ startUp();
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ tearDown();
+ }
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleReadError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with read: %s: %s", cls, msg));
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleWriteError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with write: %s: %s", cls, msg));
+ }
+
+ /**
+ * Returns true, if we're in a CONFIGURATION_ERROR state
+ *
+ * @return
+ */
+ protected boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ /**
+ * Reset communication status to ONLINE if we're in an OFFLINE state
+ */
+ protected void resetCommunicationError() {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
+ && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ /**
+ * Returns the channel UID for the specified group and channel id
+ *
+ * @param string the channel group
+ *
+ * @param string the channel id in that group
+ *
+ * @return the globally unique channel uid
+ */
+ ChannelUID channelUID(String group, String id) {
+ return new ChannelUID(getThing().getUID(), group, id);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/HeatpumpHandler.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/HeatpumpHandler.java
new file mode 100644
index 0000000000000..2ed4b8ace72ec
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/HeatpumpHandler.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.library.unit.Units.*;
+
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
+import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
+import org.openhab.binding.modbus.lambda.internal.HeatpumpConfiguration;
+import org.openhab.binding.modbus.lambda.internal.dto.HeatpumpBlock;
+import org.openhab.binding.modbus.lambda.internal.parser.HeatpumpBlockParser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
+import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
+import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
+import org.openhab.core.io.transport.modbus.PollTask;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HeatpumpHandler} is responsible for handling commands,
+ * which are sent to one of the channels and for polling the modbus.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class HeatpumpHandler extends BaseThingHandler {
+
+ public abstract class AbstractBasePoller {
+
+ private volatile @Nullable PollTask pollTask;
+
+ public synchronized void unregisterPollTask() {
+ PollTask task = pollTask;
+ if (task == null) {
+ return;
+ }
+
+ ModbusCommunicationInterface mycomms = HeatpumpHandler.this.comms;
+ if (mycomms != null) {
+ mycomms.unregisterRegularPoll(task);
+ }
+ pollTask = null;
+ }
+
+ /**
+ * Register poll task This is where we set up our regular poller
+ */
+ public synchronized void registerPollTask(int address, int length, ModbusReadFunctionCode readFunctionCode) {
+ ModbusCommunicationInterface mycomms = HeatpumpHandler.this.comms;
+ HeatpumpConfiguration myconfig = Objects.requireNonNull(HeatpumpHandler.this.config);
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("Heatpump: registerPollTask called without proper configuration");
+ }
+
+ ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), readFunctionCode, address,
+ length, myconfig.getMaxTries());
+
+ long refreshMillis = myconfig.getRefreshMillis();
+
+ pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
+ result.getRegisters().ifPresent(this::handlePolledData);
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }, HeatpumpHandler.this::handleReadError);
+ }
+
+ public synchronized void poll() {
+ PollTask task = pollTask;
+ ModbusCommunicationInterface mycomms = HeatpumpHandler.this.comms;
+ if (task != null && mycomms != null) {
+ mycomms.submitOneTimePoll(task.getRequest(), task.getResultCallback(), task.getFailureCallback());
+ }
+ }
+
+ protected abstract void handlePolledData(ModbusRegisterArray registers);
+ }
+
+ /**
+ * Logger instance
+ */
+ private final Logger logger = LoggerFactory.getLogger(HeatpumpHandler.class);
+
+ /**
+ * Configuration instance
+ */
+ protected @Nullable HeatpumpConfiguration config = null;
+ /**
+ * Parsers used to convert incoming raw messages into state blocks
+ */
+
+ private final HeatpumpBlockParser heatpumpBlockParser = new HeatpumpBlockParser();
+ /**
+ * These are the tasks used to poll the device
+ */
+ private volatile @Nullable AbstractBasePoller heatpumpPoller = null;
+ /**
+ * Communication interface to the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+ /**
+ * This is the slave id, we store this once initialization is complete
+ */
+ private volatile int slaveId;
+
+ /**
+ * Instances of this handler should get a reference to the modbus manager
+ *
+ * @param thing the thing to handle
+ */
+ public HeatpumpHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * @param address address of the value to be written on the modbus
+ *
+ * @param shortValue value to be written on the modbus
+ */
+ protected void writeInt16(int address, short shortValue) {
+ HeatpumpConfiguration myconfig = Objects.requireNonNull(HeatpumpHandler.this.config);
+ ModbusCommunicationInterface mycomms = HeatpumpHandler.this.comms;
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+ // big endian byte ordering
+ byte hi = (byte) (shortValue >> 8);
+ byte lo = (byte) shortValue;
+ ModbusRegisterArray data = new ModbusRegisterArray(hi, lo);
+
+ ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data,
+ true, myconfig.getMaxTries());
+
+ mycomms.submitOneTimeWrite(request, result -> {
+ if (hasConfigurationError()) {
+ return;
+ }
+ HeatpumpHandler.this.updateStatus(ThingStatus.ONLINE);
+ }, failure -> {
+ HeatpumpHandler.this.handleWriteError(failure);
+ });
+ }
+
+ private short getScaledInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType> quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(CELSIUS);
+ if (c != null) {
+ return (short) (c.doubleValue() * 10);
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return (short) (c.doubleValue() * 10);
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ /**
+ * Handle incoming commands.
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ String groupId = channelUID.getGroupId();
+ if (groupId != null) {
+ AbstractBasePoller poller;
+ switch (groupId) {
+ case GROUP_HEAT_PUMP:
+ poller = heatpumpPoller;
+ break;
+ default:
+ poller = null;
+ break;
+ }
+ if (poller != null) {
+ logger.trace("Heatpump: Polling initiated");
+ poller.poll();
+ }
+ }
+ } else {
+ try {
+ if (GROUP_HEAT_PUMP.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_HEAT_PUMP_REQUEST_T_FLOW:
+ writeInt16(baseadress + 16, getScaledInt16Value(command));
+ break;
+
+ case CHANNEL_HEAT_PUMP_REQUEST_T_RETURN:
+ writeInt16(baseadress + 17, getScaledInt16Value(command));
+ break;
+
+ case CHANNEL_HEAT_PUMP_REQUEST_HEAT_SINK:
+ writeInt16(baseadress + 18, getScaledInt16Value(command));
+ break;
+ }
+ }
+ } catch (LambdaException error) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String cls = error.getClass().getName();
+ String msg = error.getMessage();
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with: %s: %s", cls, msg));
+ }
+ }
+ }
+
+ /**
+ * Initialization: Load the config object of the block Connect to the slave
+ * bridge Start the periodic polling
+ */
+ @Override
+ public void initialize() {
+ config = getConfigAs(HeatpumpConfiguration.class);
+
+ logger.debug("Initializing thing with properties: {}", thing.getProperties());
+
+ startUp();
+ }
+
+ /**
+ * Adresses for the polling registers, used for reading and writing
+ */
+ private int baseadress;
+
+ /**
+ * This method starts the operation of this handler Connect to the slave bridge
+ * Start the periodic polling1
+ */
+ private void startUp() {
+ if (comms != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return;
+ }
+
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ // this will be handled below as endpoint remains null
+ }
+
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface of Heat Pump is null, Thing & Bridge are offline");
+ return;
+ }
+
+ if (config == null) {
+ logger.debug("Invalid comms/config/manager ref for lambda heat-pump handler");
+ return;
+ }
+
+ HeatpumpConfiguration myconfig = Objects.requireNonNull(HeatpumpHandler.this.config);
+
+ baseadress = 1000 + 100 * myconfig.getSubindex();
+
+ if (heatpumpPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledHeatpumpData(registers);
+ }
+ };
+
+ poller.registerPollTask(baseadress, 24, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ heatpumpPoller = poller;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ /**
+ * Dispose the binding correctly
+ */
+ @Override
+ public void dispose() {
+ tearDown();
+ }
+
+ /**
+ * Unregister the poll tasks and release the endpoint reference
+ */
+ private void tearDown() {
+ AbstractBasePoller poller = heatpumpPoller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ heatpumpPoller = null;
+ }
+
+ comms = null;
+ }
+
+ /**
+ * Returns the current slave id from the bridge
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * Get the endpoint handler from the bridge this handler is connected to Checks
+ * that we're connected to the right type of bridge
+ *
+ * @return the endpoint handler or null if the bridge does not exist
+ */
+ private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.debug("Bridge is null");
+ return null;
+ }
+ if (bridge.getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Bridge is not online");
+ return null;
+ }
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.debug("Bridge handler is null");
+ return null;
+ }
+ if (handler instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ throw new IllegalStateException("Unexpected bridge handler: " + handler.toString());
+ }
+ }
+
+ protected State getScaled(Number value, Unit> unit, Double pow) {
+ double factor = Math.pow(10, pow);
+ return QuantityType.valueOf(value.doubleValue() * factor, unit);
+ }
+
+ protected State getUnscaled(Number value, Unit> unit) {
+ return QuantityType.valueOf(value.doubleValue(), unit);
+ }
+
+ /**
+ * Returns high value * 1000 + low value
+ *
+ * @param high the high value
+ *
+ * @param low the low value
+ *
+ * @return the scaled value as a DecimalType
+ */
+ protected State getEnergyQuantity(int high, int low) {
+ double value = high * 1000 + low;
+ return QuantityType.valueOf(value, KILOWATT_HOUR);
+ }
+
+ /**
+ * These methods are called each time new data has been polled from the modbus
+ * slave The register array is first parsed, then each of the channels are
+ * updated to the new values
+ *
+ * @param registers byte array read from the modbus slave
+ */
+
+ protected void handlePolledHeatpumpData(ModbusRegisterArray registers) {
+ HeatpumpBlock block = heatpumpBlockParser.parse(registers);
+
+ // Heatpump group
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_ERROR_STATE),
+ new DecimalType(block.heatpumpErrorState));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_ERROR_NUMBER),
+ new DecimalType(block.heatpumpErrorNumber));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_OPERATING_STATE),
+ new DecimalType(block.heatpumpOperatingState));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_STATE), new DecimalType(block.heatpumpState));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_T_FLOW),
+ getScaled(block.heatpumpTFlow, CELSIUS, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_T_RETURN),
+ getScaled(block.heatpumpTReturn, CELSIUS, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_VOL_SINK),
+ getScaled(block.heatpumpVolSink, LITRE_PER_MINUTE, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_T_EQIN),
+ getScaled(block.heatpumpTEQin, CELSIUS, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_T_EQOUT),
+ getScaled(block.heatpumpTEQout, CELSIUS, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_VOL_SOURCE),
+ getScaled(block.heatpumpVolSource, LITRE_PER_MINUTE, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_COMPRESSOR_RATING),
+ getScaled(block.heatpumpCompressorRating, PERCENT, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_QP_HEATING),
+ getScaled(block.heatpumpQpHeating, WATT, 2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_FI_POWER_CONSUMPTION),
+ getUnscaled(block.heatpumpFIPowerConsumption, WATT));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_COP), getScaled(block.heatpumpCOP, PERCENT, -2.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_REQUEST_PASSWORD),
+ new DecimalType(block.heatpumpRequestPassword));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_REQUEST_TYPE),
+ new DecimalType(block.heatpumpRequestType));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_REQUEST_T_FLOW),
+ getScaled(block.heatpumpRequestTFlow, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_REQUEST_T_RETURN),
+ getScaled(block.heatpumpRequestTReturn, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_REQUEST_HEAT_SINK),
+ getScaled(block.heatpumpRequestHeatSink, KELVIN, -1.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_RELAIS_STATE),
+ new DecimalType(block.heatpumpRelaisState));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_VDAE),
+ getScaled(block.heatpumpVdAE, KILOWATT_HOUR, -3.0));
+ updateState(channelUID(GROUP_HEAT_PUMP, CHANNEL_HEAT_PUMP_VDAQ),
+ getScaled(block.heatpumpVdAQ, KILOWATT_HOUR, -3.0));
+
+ resetCommunicationError();
+ }
+
+ /**
+ * @param bridgeStatusInfo
+ */
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ startUp();
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ tearDown();
+ }
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleReadError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with read: %s: %s", cls, msg));
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleWriteError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with write: %s: %s", cls, msg));
+ }
+
+ /**
+ * Returns true, if we're in a CONFIGURATION_ERROR state
+ *
+ * @return
+ */
+ protected boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ /**
+ * Reset communication status to ONLINE if we're in an OFFLINE state
+ */
+ protected void resetCommunicationError() {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
+ && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ /**
+ * Returns the channel UID for the specified group and channel id
+ *
+ * @param string the channel group
+ *
+ * @param string the channel id in that group
+ *
+ * @return the globally unique channel uid
+ */
+ ChannelUID channelUID(String group, String id) {
+ return new ChannelUID(getThing().getUID(), group, id);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/LambdaException.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/LambdaException.java
new file mode 100644
index 0000000000000..1e5b2692e1c3a
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/LambdaException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Thrown when the lambda handler sees an error.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+
+@NonNullByDefault
+public class LambdaException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public LambdaException() {
+ }
+
+ public LambdaException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/SolarHandler.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/SolarHandler.java
new file mode 100644
index 0000000000000..e016b9a791860
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/handler/SolarHandler.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.handler;
+
+import static org.openhab.binding.modbus.lambda.internal.LambdaBindingConstants.*;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
+import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
+import org.openhab.binding.modbus.lambda.internal.SolarConfiguration;
+import org.openhab.binding.modbus.lambda.internal.dto.SolarBlock;
+import org.openhab.binding.modbus.lambda.internal.dto.SolarReg50Block;
+import org.openhab.binding.modbus.lambda.internal.parser.SolarBlockParser;
+import org.openhab.binding.modbus.lambda.internal.parser.SolarReg50BlockParser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
+import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
+import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
+import org.openhab.core.io.transport.modbus.ModbusWriteRequestBlueprint;
+import org.openhab.core.io.transport.modbus.PollTask;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SolarHandler} is responsible for handling commands,
+ * which are sent to one of the channels and for polling the modbus
+ * for solar thermic component data.
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ */
+@NonNullByDefault
+public class SolarHandler extends BaseThingHandler {
+
+ public abstract class AbstractBasePoller {
+
+ private volatile @Nullable PollTask pollTask;
+
+ public synchronized void unregisterPollTask() {
+ PollTask task = pollTask;
+ if (task == null) {
+ return;
+ }
+
+ ModbusCommunicationInterface mycomms = SolarHandler.this.comms;
+ if (mycomms != null) {
+ mycomms.unregisterRegularPoll(task);
+ }
+ pollTask = null;
+ }
+
+ public synchronized void registerPollTask(int address, int length, ModbusReadFunctionCode readFunctionCode) {
+ ModbusCommunicationInterface mycomms = SolarHandler.this.comms;
+ SolarConfiguration myconfig = Objects.requireNonNull(SolarHandler.this.config);
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("Solar: registerPollTask called without proper configuration");
+ }
+
+ ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(), readFunctionCode, address,
+ length, myconfig.getMaxTries());
+
+ long refreshMillis = myconfig.getRefreshMillis();
+
+ pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
+ result.getRegisters().ifPresent(this::handlePolledData);
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }, SolarHandler.this::handleReadError);
+ }
+
+ public synchronized void poll() {
+ PollTask task = pollTask;
+ ModbusCommunicationInterface mycomms = SolarHandler.this.comms;
+ if (task != null && mycomms != null) {
+ mycomms.submitOneTimePoll(task.getRequest(), task.getResultCallback(), task.getFailureCallback());
+ }
+ }
+
+ protected abstract void handlePolledData(ModbusRegisterArray registers);
+ }
+
+ private final Logger logger = LoggerFactory.getLogger(SolarHandler.class);
+
+ protected @Nullable SolarConfiguration config = null;
+
+ private final SolarBlockParser solarBlockParser = new SolarBlockParser();
+ private final SolarReg50BlockParser solarReg50BlockParser = new SolarReg50BlockParser();
+
+ private volatile @Nullable AbstractBasePoller solarPoller = null;
+ private volatile @Nullable AbstractBasePoller solarReg50Poller = null;
+
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+
+ private volatile int slaveId;
+
+ public SolarHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * @param command get the value of this command.
+ *
+ * @return short the value of the command as short
+ */
+
+ /**
+ * @param address address of the value to be written on the modbus
+ *
+ * @param shortValue value to be written on the modbus
+ */
+ protected void writeInt16(int address, short shortValue) {
+ SolarConfiguration myconfig = Objects.requireNonNull(SolarHandler.this.config);
+ ModbusCommunicationInterface mycomms = SolarHandler.this.comms;
+
+ if (myconfig == null || mycomms == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+ // big endian byte ordering
+ byte hi = (byte) (shortValue >> 8);
+ byte lo = (byte) shortValue;
+ ModbusRegisterArray data = new ModbusRegisterArray(hi, lo);
+
+ ModbusWriteRegisterRequestBlueprint request = new ModbusWriteRegisterRequestBlueprint(slaveId, address, data,
+ true, myconfig.getMaxTries());
+
+ mycomms.submitOneTimeWrite(request, result -> {
+ if (hasConfigurationError()) {
+ return;
+ }
+ SolarHandler.this.updateStatus(ThingStatus.ONLINE);
+ }, failure -> {
+ SolarHandler.this.handleWriteError(failure);
+ });
+ }
+
+ private short getScaledInt16Value(Command command) throws LambdaException {
+ if (command instanceof QuantityType> quantityCommand) {
+ QuantityType> c = quantityCommand.toUnit(CELSIUS);
+ if (c != null) {
+ return (short) (c.doubleValue() * 10);
+ } else {
+ throw new LambdaException("Unsupported unit");
+ }
+ }
+ if (command instanceof DecimalType c) {
+ return (short) (c.doubleValue() * 10);
+ }
+ throw new LambdaException("Unsupported command type");
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ String groupId = channelUID.getGroupId();
+ if (groupId != null) {
+ AbstractBasePoller poller;
+ switch (groupId) {
+ case GROUP_SOLAR:
+ poller = solarPoller;
+ break;
+ case GROUP_SOLAR_REG50:
+ poller = solarReg50Poller;
+ break;
+ default:
+ poller = null;
+ break;
+ }
+ if (poller != null) {
+ logger.trace("Solar: Polling initiated");
+ poller.poll();
+ }
+ }
+ } else {
+ try {
+ if (GROUP_SOLAR_REG50.equals(channelUID.getGroupId())) {
+ switch (channelUID.getIdWithoutGroup()) {
+ case CHANNEL_SOLAR_MAXIMUM_BUFFER_TEMPERATURE:
+ writeInt16(reg50baseadress, getScaledInt16Value(command));
+ break;
+ case CHANNEL_SOLAR_BUFFER_CHANGEOVER_TEMPERATURE:
+ writeInt16(reg50baseadress + 1, getScaledInt16Value(command));
+ break;
+ }
+ }
+ } catch (LambdaException error) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String cls = error.getClass().getName();
+ String msg = error.getMessage();
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with: %s: %s", cls, msg));
+ }
+
+ }
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(SolarConfiguration.class);
+
+ logger.debug("Initializing solar thing with properties: {}", thing.getProperties());
+
+ startUp();
+ }
+
+ private int baseadress;
+ private int reg50baseadress;
+
+ private void startUp() {
+ if (comms != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return;
+ }
+
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ // this will be handled below as endpoint remains null
+ }
+
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface of Solar is null, Thing & Bridge are offline");
+ return;
+ }
+
+ if (config == null) {
+ logger.debug("Invalid comms/config/manager ref for lambda solar handler");
+ return;
+ }
+
+ SolarConfiguration myconfig = Objects.requireNonNull(SolarHandler.this.config);
+
+ // Base address for solar is 4000 as mentioned in README.md
+ baseadress = 4000 + 100 * myconfig.getSubindex();
+ reg50baseadress = baseadress + 50;
+
+ if (solarPoller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledSolarData(registers);
+ }
+ };
+
+ poller.registerPollTask(baseadress, 5, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ solarPoller = poller;
+ }
+ if (solarReg50Poller == null) {
+ AbstractBasePoller poller = new AbstractBasePoller() {
+ @Override
+ protected void handlePolledData(ModbusRegisterArray registers) {
+ handlePolledSolarReg50Data(registers);
+ }
+ };
+
+ poller.registerPollTask(reg50baseadress, 2, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS);
+ solarReg50Poller = poller;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ @Override
+ public void dispose() {
+ tearDown();
+ }
+
+ private void tearDown() {
+ AbstractBasePoller poller = solarPoller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ solarPoller = null;
+ }
+
+ poller = solarReg50Poller;
+ if (poller != null) {
+ poller.unregisterPollTask();
+ solarReg50Poller = null;
+ }
+
+ comms = null;
+ }
+
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ logger.debug("Bridge is null");
+ return null;
+ }
+ if (bridge.getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Bridge is not online");
+ return null;
+ }
+
+ ThingHandler handler = bridge.getHandler();
+ if (handler == null) {
+ logger.debug("Bridge handler is null");
+ return null;
+ }
+ if (handler instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ throw new IllegalStateException("Unexpected bridge handler: " + handler.toString());
+ }
+ }
+
+ protected State getScaled(Number value, Unit> unit, Double pow) {
+ double factor = Math.pow(10, pow);
+ return QuantityType.valueOf(value.doubleValue() * factor, unit);
+ }
+
+ protected void handlePolledSolarData(ModbusRegisterArray registers) {
+ SolarBlock block = solarBlockParser.parse(registers);
+
+ // Update solar channels
+ updateState(channelUID(GROUP_SOLAR, CHANNEL_SOLAR_ERROR_NUMBER), new DecimalType(block.solarErrorNumber));
+ updateState(channelUID(GROUP_SOLAR, CHANNEL_SOLAR_OPERATING_STATE), new DecimalType(block.solarOperatingState));
+ updateState(channelUID(GROUP_SOLAR, CHANNEL_SOLAR_COLLECTOR_TEMPERATURE),
+ getScaled(block.solarCollectorTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_SOLAR, CHANNEL_SOLAR_BUFFER1_TEMPERATURE),
+ getScaled(block.solarBuffer1Temperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_SOLAR, CHANNEL_SOLAR_BUFFER2_TEMPERATURE),
+ getScaled(block.solarBuffer2Temperature, CELSIUS, -1.0));
+
+ resetCommunicationError();
+ }
+
+ protected void handlePolledSolarReg50Data(ModbusRegisterArray registers) {
+ SolarReg50Block block = solarReg50BlockParser.parse(registers);
+
+ updateState(channelUID(GROUP_SOLAR_REG50, CHANNEL_SOLAR_MAXIMUM_BUFFER_TEMPERATURE),
+ getScaled(block.solarMaximumBufferTemperature, CELSIUS, -1.0));
+ updateState(channelUID(GROUP_SOLAR_REG50, CHANNEL_SOLAR_BUFFER_CHANGEOVER_TEMPERATURE),
+ getScaled(block.solarBufferChangeoverTemperature, CELSIUS, -1.0));
+
+ resetCommunicationError();
+ }
+
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ super.bridgeStatusChanged(bridgeStatusInfo);
+
+ if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+ startUp();
+ } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
+ tearDown();
+ }
+ }
+
+ protected void handleReadError(AsyncModbusFailure failure) {
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with read: %s: %s", cls, msg));
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleWriteError(AsyncModbusFailure failure) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = failure.getCause().getMessage();
+ String cls = failure.getCause().getClass().getName();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ String.format("Error with write: %s: %s", cls, msg));
+ }
+
+ protected boolean hasConfigurationError() {
+ ThingStatusInfo statusInfo = getThing().getStatusInfo();
+ return statusInfo.getStatus() == ThingStatus.OFFLINE
+ && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
+ }
+
+ protected void resetCommunicationError() {
+ ThingStatusInfo statusInfo = thing.getStatusInfo();
+ if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
+ && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ ChannelUID channelUID(String group, String id) {
+ return new ChannelUID(getThing().getUID(), group, id);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/AbstractBaseParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/AbstractBaseParser.java
new file mode 100644
index 0000000000000..ddda4421dd3a5
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/AbstractBaseParser.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
+import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.openhab.core.library.types.DecimalType;
+
+/**
+ * Base class for parsers with some helper methods
+ *
+ * @author Nagy Attila Gabor - Initial contribution
+ * @author Paul Frank - Added more methods
+ */
+@NonNullByDefault
+public class AbstractBaseParser {
+
+ /**
+ * Extract an optional double value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @return the parsed value or empty if the field is not implemented
+ */
+ protected Optional extractOptionalDouble(ModbusRegisterArray raw, int index) {
+ return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.INT16)
+ .map(value -> ((double) value.intValue()) / 10.0).filter(value -> value != (short) 0x8000);
+ }
+
+ /**
+ * Extract a mandatory double value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @param def the default value
+ *
+ * @return the parsed value or the default if the field is not implemented
+ */
+ protected Double extractDouble(ModbusRegisterArray raw, int index, double def) {
+ return Objects.requireNonNull(extractOptionalDouble(raw, index).orElse(def));
+ }
+
+ /**
+ * Extract an optional int16 value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @return the parsed value or empty if the field is not implemented
+ */
+ protected Optional extractOptionalInt16(ModbusRegisterArray raw, int index) {
+ return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.INT16).map(DecimalType::shortValue)
+ .filter(value -> value != (short) 0x8000);
+ }
+
+ /**
+ * Extract a mandatory int16 value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @param def the default value
+ *
+ * @return the parsed value or the default if the field is not implemented
+ */
+ protected Short extractInt16(ModbusRegisterArray raw, int index, short def) {
+ return Objects.requireNonNull(extractOptionalInt16(raw, index).orElse(def));
+ }
+
+ /**
+ * Extract an optional uint16 value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @return the parsed value or empty if the field is not implemented
+ */
+ protected Optional extractOptionalUInt16(ModbusRegisterArray raw, int index) {
+ return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.UINT16).map(DecimalType::intValue)
+ .filter(value -> value != 0xffff);
+ }
+
+ /**
+ * Extract a mandatory uint16 value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @param def the default value
+ *
+ * @return the parsed value or the default if the field is not implemented
+ */
+ protected Integer extractUInt16(ModbusRegisterArray raw, int index, int def) {
+ return Objects.requireNonNull(extractOptionalUInt16(raw, index).orElse(def));
+ }
+
+ /**
+ * Extract an optional acc32 value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @return the parsed value or empty if the field is not implemented
+ */
+ protected Optional extractOptionalUInt32(ModbusRegisterArray raw, int index) {
+ return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.UINT32).map(DecimalType::longValue)
+ .filter(value -> value != 0);
+ }
+
+ protected Optional extractOptionalInt32(ModbusRegisterArray raw, int index) {
+ return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.INT32).map(DecimalType::longValue)
+ .filter(value -> value != 0);
+ }
+
+ /**
+ * Extract a mandatory acc32 value
+ *
+ * @param raw the register array to extract from
+ *
+ * @param index the address of the field
+ *
+ * @param def the default value
+ *
+ * @return the parsed value or default if the field is not implemented
+ */
+ protected Long extractUInt32(ModbusRegisterArray raw, int index, long def) {
+ return Objects.requireNonNull(extractOptionalUInt32(raw, index).orElse(def));
+ }
+
+ protected Long extractInt32(ModbusRegisterArray raw, int index, long def) {
+ return Objects.requireNonNull(extractOptionalInt32(raw, index).orElse(def));
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/AmbientBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/AmbientBlockParser.java
new file mode 100644
index 0000000000000..eba6d0e99ce2c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/AmbientBlockParser.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.AmbientBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into an Ambient Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class AmbientBlockParser extends AbstractBaseParser {
+
+ public AmbientBlock parse(ModbusRegisterArray raw) {
+ AmbientBlock block = new AmbientBlock();
+ block.ambientErrorNumber = extractInt16(raw, 0, (short) 0);
+ block.ambientOperatingState = extractUInt16(raw, 1, (short) 0);
+ block.actualAmbientTemperature = extractInt16(raw, 2, (short) 0);
+ block.averageAmbientTemperature = extractInt16(raw, 3, (short) 0);
+ block.calculatedAmbientTemperature = extractInt16(raw, 4, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BoilerBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BoilerBlockParser.java
new file mode 100644
index 0000000000000..417ef3a44602a
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BoilerBlockParser.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.BoilerBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into a Boiler Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class BoilerBlockParser extends AbstractBaseParser {
+
+ public BoilerBlock parse(ModbusRegisterArray raw) {
+ BoilerBlock block = new BoilerBlock();
+ block.boilerErrorNumber = extractUInt16(raw, 0, (short) 0);
+ block.boilerOperatingState = extractInt16(raw, 1, (short) 0);
+ block.boilerActualHighTemperature = extractInt16(raw, 2, (short) 0);
+ block.boilerActualLowTemperature = extractInt16(raw, 3, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BoilerReg50BlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BoilerReg50BlockParser.java
new file mode 100644
index 0000000000000..80fa07289c9eb
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BoilerReg50BlockParser.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.BoilerReg50Block;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses modbus data into an BoilerReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class BoilerReg50BlockParser extends AbstractBaseParser {
+ public BoilerReg50Block parse(ModbusRegisterArray raw) {
+ BoilerReg50Block block = new BoilerReg50Block();
+ block.boilerMaximumBoilerTemperature = extractInt16(raw, 0, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BufferBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BufferBlockParser.java
new file mode 100644
index 0000000000000..fece431371e33
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BufferBlockParser.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.BufferBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into a Buffer Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class BufferBlockParser extends AbstractBaseParser {
+
+ public BufferBlock parse(ModbusRegisterArray raw) {
+ BufferBlock block = new BufferBlock();
+ block.bufferErrorNumber = extractInt16(raw, 0, (short) 0);
+ block.bufferOperatingState = extractUInt16(raw, 1, (short) 0);
+ block.bufferActualHighTemperature = extractInt16(raw, 2, (short) 0);
+ block.bufferActualLowTemperature = extractInt16(raw, 3, (short) 0);
+ block.bufferActualModbusTemperature = extractInt16(raw, 4, (short) 0);
+ block.bufferRequestType = extractInt16(raw, 5, (short) 0);
+ block.bufferrequestFlowLineTemperature = extractInt16(raw, 6, (short) 0);
+ block.bufferrequestReturnLineTemperature = extractInt16(raw, 7, (short) 0);
+ block.bufferrequestHeatSinkTemperature = extractInt16(raw, 8, (short) 0);
+ block.bufferrequestHeatingCapacity = extractInt16(raw, 9, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BufferReg50BlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BufferReg50BlockParser.java
new file mode 100644
index 0000000000000..839661f424b8b
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/BufferReg50BlockParser.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.BufferReg50Block;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into an BufferReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class BufferReg50BlockParser extends AbstractBaseParser {
+
+ public BufferReg50Block parse(ModbusRegisterArray raw) {
+ BufferReg50Block block = new BufferReg50Block();
+ block.bufferMaximumBufferTemperature = extractUInt16(raw, 0, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/EManagerBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/EManagerBlockParser.java
new file mode 100644
index 0000000000000..c7bf8882ca294
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/EManagerBlockParser.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.EManagerBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses inverter modbus data into an EManager Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class EManagerBlockParser extends AbstractBaseParser {
+
+ public EManagerBlock parse(ModbusRegisterArray raw) {
+ EManagerBlock block = new EManagerBlock();
+ block.emanagerErrorNumber = extractInt16(raw, 0, (short) 0);
+ block.emanagerOperatingState = extractUInt16(raw, 1, (short) 0);
+ block.emanagerActualPower = extractUInt16(raw, 2, (short) 0);
+ block.emanagerActualPowerSigned = extractInt16(raw, 2, (short) 0);
+ block.emanagerActualPowerConsumption = extractInt16(raw, 3, (short) 0);
+ block.emanagerPowerConsumptionSetpoint = extractInt16(raw, 4, (short) 0);
+
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatingCircuitBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatingCircuitBlockParser.java
new file mode 100644
index 0000000000000..f030e57712ce0
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatingCircuitBlockParser.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.HeatingCircuitBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda
+ * modbus data into a Heating Circuit Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class HeatingCircuitBlockParser extends AbstractBaseParser {
+
+ public HeatingCircuitBlock parse(ModbusRegisterArray raw) {
+ HeatingCircuitBlock block = new HeatingCircuitBlock();
+ block.heatingcircuitErrorNumber = extractInt16(raw, 0, (short) 0);
+ block.heatingcircuitOperatingState = extractUInt16(raw, 1, (short) 0);
+ block.heatingcircuitFlowLineTemperature = extractInt16(raw, 2, (short) 0);
+ block.heatingcircuitReturnLineTemperature = extractInt16(raw, 3, (short) 0);
+ block.heatingcircuitRoomDeviceTemperature = extractInt16(raw, 4, (short) 0);
+ block.heatingcircuitSetpointFlowLineTemperature = extractInt16(raw, 5, (short) 0);
+ block.heatingcircuitOperatingMode = extractInt16(raw, 6, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatingCircuitReg50BlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatingCircuitReg50BlockParser.java
new file mode 100644
index 0000000000000..967d3a215b666
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatingCircuitReg50BlockParser.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.HeatingCircuitReg50Block;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into a HeatingCircuitReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class HeatingCircuitReg50BlockParser extends AbstractBaseParser {
+
+ public HeatingCircuitReg50Block parse(ModbusRegisterArray raw) {
+ HeatingCircuitReg50Block block = new HeatingCircuitReg50Block();
+ block.heatingcircuitOffsetFlowLineTemperature = extractInt16(raw, 0, (short) 0);
+ block.heatingcircuitRoomHeatingTemperature = extractInt16(raw, 1, (short) 0);
+ block.heatingcircuitRoomCoolingTemperature = extractInt16(raw, 2, (short) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatpumpBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatpumpBlockParser.java
new file mode 100644
index 0000000000000..790354cde250f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/HeatpumpBlockParser.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.HeatpumpBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into a Heatpump Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class HeatpumpBlockParser extends AbstractBaseParser {
+
+ public HeatpumpBlock parse(ModbusRegisterArray raw) {
+ HeatpumpBlock block = new HeatpumpBlock();
+ block.heatpumpErrorState = extractUInt16(raw, 0, (short) 0);
+ block.heatpumpErrorNumber = extractInt16(raw, 1, (short) 0);
+ block.heatpumpState = extractUInt16(raw, 2, (short) 0);
+ block.heatpumpOperatingState = extractUInt16(raw, 3, (short) 0);
+ block.heatpumpTFlow = extractInt16(raw, 4, (short) 0);
+ block.heatpumpTReturn = extractInt16(raw, 5, (short) 0);
+ block.heatpumpVolSink = extractInt16(raw, 6, (short) 0);
+ block.heatpumpTEQin = extractInt16(raw, 7, (short) 0);
+ block.heatpumpTEQout = extractInt16(raw, 8, (short) 0);
+ block.heatpumpVolSource = extractInt16(raw, 9, (short) 0);
+ block.heatpumpCompressorRating = extractUInt16(raw, 10, (short) 0);
+ block.heatpumpQpHeating = extractInt16(raw, 11, (short) 0);
+ block.heatpumpFIPowerConsumption = extractInt16(raw, 12, (short) 0);
+ block.heatpumpCOP = extractInt16(raw, 13, (short) 0);
+ block.heatpumpRequestPassword = extractUInt16(raw, 14, (short) 0);
+ block.heatpumpRequestType = extractInt16(raw, 15, (short) 0);
+ block.heatpumpRequestTFlow = extractInt16(raw, 16, (short) 0);
+ block.heatpumpRequestTReturn = extractInt16(raw, 17, (short) 0);
+ block.heatpumpRequestHeatSink = extractInt16(raw, 18, (short) 0);
+ block.heatpumpRelaisState = extractInt16(raw, 19, (short) 0);
+ block.heatpumpVdAE = extractInt32(raw, 20, (long) 0);
+ block.heatpumpVdAQ = extractInt32(raw, 22, (long) 0);
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/SolarBlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/SolarBlockParser.java
new file mode 100644
index 0000000000000..8345c2d0f410e
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/SolarBlockParser.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+/**
+* Parses lambda modbus data into a Solar Block
+*
+* @author Paul Frank - Initial contribution
+* @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+*
+*/
+
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.SolarBlock;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parser for solar thermic component data from modbus registers
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class SolarBlockParser extends AbstractBaseParser {
+
+ public SolarBlock parse(ModbusRegisterArray registers) {
+ SolarBlock block = new SolarBlock();
+ block.solarErrorNumber = extractUInt16(registers, 0, 0);
+ block.solarOperatingState = extractUInt16(registers, 1, 0);
+ block.solarCollectorTemperature = extractInt16(registers, 2, (short) 0);
+ // Two different versions for usage in different configurations of the manufacturer
+ block.solarBuffer1Temperature = extractInt16(registers, 3, (short) 0);
+ block.solarBuffer2Temperature = extractUInt16(registers, 4, 0);
+
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/SolarReg50BlockParser.java b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/SolarReg50BlockParser.java
new file mode 100644
index 0000000000000..4acc9e0018eb6
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/java/org/openhab/binding/modbus/lambda/internal/parser/SolarReg50BlockParser.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2010-2025 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.modbus.lambda.internal.parser;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.lambda.internal.dto.SolarReg50Block;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+
+/**
+ * Parses lambda modbus data into a SolarReg50 Block
+ *
+ * @author Paul Frank - Initial contribution
+ * @author Christian Koch - modified for lambda heat pump based on stiebeleltron binding for modbus
+ *
+ */
+@NonNullByDefault
+public class SolarReg50BlockParser extends AbstractBaseParser {
+
+ public SolarReg50Block parse(ModbusRegisterArray raw) {
+ SolarReg50Block block = new SolarReg50Block();
+ block.solarMaximumBufferTemperature = extractInt16(raw, 0, (short) 0);
+ block.solarBufferChangeoverTemperature = extractInt16(raw, 1, (short) 0);
+
+ return block;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..017963f038028
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,10 @@
+
+
+ binding
+ Lambda Heat Pump Binding
+ Lambda heat pump binding via Modbus TCP/IP.
+ local
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/i18n/lambda.properties b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/i18n/lambda.properties
new file mode 100644
index 0000000000000..d363c48c97b86
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/i18n/lambda.properties
@@ -0,0 +1,156 @@
+# add-on
+
+addon.modbus.lambda.name = Lambda Heat Pump Binding
+addon.modbus.lambda.description = The Lambda Heat Pump binding supports the different parts of a Lambda Heat Pump heating.
+
+# thing types
+
+thing-type.modbus.general.label = Lambda General Sections
+thing-type.modbus.general.description = Provides Channels of the General Sections
+thing-type.modbus.heat-pump.label = Lambda Heat Pump
+thing-type.modbus.heat-pump.description = Provides Channels of a Heat Pump
+thing-type.modbus.boiler.label = Lambda Boiler
+thing-type.modbus.boiler.description = Provides Channels of a Boiler
+thing-type.modbus.buffer.label = Lambda Buffer
+thing-type.modbus.buffer.description = Provides Channels of a Buffer
+thing-type.modbus.heating-circuit.label = Lambda Heating Circuit
+thing-type.modbus.heating-circuit.description = Provides Channels of a Heating Circuit
+thing-type.modbus.solar.label = Lambda Solar
+thing-type.modbus.solar.description = Provides Channels of Solar
+
+# thing types config
+
+thing-type.config.general.modbusconfig.maxTries.label = Maximum Tries When Reading
+thing-type.config.general.modbusconfig.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+thing-type.config.general.modbusconfig.refresh.label = Refresh Interval
+thing-type.config.general.modbusconfig.refresh.description = Refresh interval in seconds. Use zero to disable automatic polling.
+thing-type.config.general.baseadress.label = BaseAdress of general section
+
+thing-type.config.boiler.modbusconfig.maxTries.label = Maximum Tries When Reading
+thing-type.config.boiler.modbusconfig.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+thing-type.config.boiler.modbusconfig.refresh.label = Refresh Interval
+thing-type.config.boiler.modbusconfig.refresh.description = Refresh interval in seconds. Use zero to disable automatic polling.
+thing-type.config.boiler.baseadress.label = BaseAdress of boiler
+
+thing-type.config.buffer.modbusconfig.maxTries.label = Maximum Tries When Reading
+thing-type.config.buffer.modbusconfig.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+thing-type.config.buffer.modbusconfig.refresh.label = Refresh Interval
+thing-type.config.buffer.modbusconfig.refresh.description = Refresh interval in seconds. Use zero to disable automatic polling.
+thing-type.config.buffer.baseadress.label = BaseAdress of buffer
+
+thing-type.config.heat-pump.modbusconfig.maxTries.label = Maximum Tries When Reading
+thing-type.config.heat-pump.modbusconfig.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+thing-type.config.heat-pump.modbusconfig.refresh.label = Refresh Interval
+thing-type.config.heat-pump.modbusconfig.refresh.description = Refresh interval in seconds. Use zero to disable automatic polling.
+thing-type.config.heat-pump.baseadress.label = BaseAdress of Heatpump
+
+thing-type.config.heating-circuit.modbusconfig.maxTries.label = Maximum Tries When Reading
+thing-type.config.heating-circuit.modbusconfig.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+thing-type.config.heating-circuit.modbusconfig.refresh.label = Refresh Interval
+thing-type.config.heating-circuit.modbusconfig.refresh.description = Refresh interval in seconds. Use zero to disable automatic polling.
+thing-type.config.heating-circuit.baseadress.label = BaseAdress of Heating Circuit
+
+thing-type.config.solar.modbusconfig.maxTries.label = Maximum Tries When Reading
+thing-type.config.solar.modbusconfig.maxTries.description = Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+thing-type.config.solar.modbusconfig.refresh.label = Refresh Interval
+thing-type.config.solar.modbusconfig.refresh.description = Refresh interval in seconds. Use zero to disable automatic polling.
+thing-type.config.solar.baseadress.label = Base Adress of Heating Circuit
+
+# channel group types
+
+channel-group-type.modbus.ambient.label = General Ambient Group
+channel-group-type.modbus.emanager.label = General E-Manager Group
+channel-group-type.modbus.heat-pump.label = Heatpump Group
+channel-group-type.modbus.boiler.label = Boiler Group
+channel-group-type.modbus.boilerreg50.label = Boiler Reg50 Group
+channel-group-type.modbus.buffer.label = Buffer Group
+channel-group-type.modbus.bufferreg50.label = Buffer Reg50 Group
+channel-group-type.modbus.heating-circuit.label = Heating-Circuit Group
+channel-group-type.modbus.heating-circuitreg50.label = Heating-Circuit Reg50 Group
+channel-group-type.modbus.solar.label = Solar Group
+channel-group-type.modbus.solarreg50.label = Solar Reg50 Group
+
+# channel types
+
+# General Ambient
+channel-type.modbus.ambient-error-number-channel.label = Ambient Error Number
+channel-type.modbus.ambient-operating-state-channel.label = Ambient Operating State
+channel-type.modbus.actual-ambient-temperature-channel.label = Actual Ambient Temperature
+channel-type.modbus.average-ambient-temperature-channel.label = Average Ambient Temperature 1h
+channel-type.modbus.calculated-ambient-temperature-channel.label = Calculated Ambient Temperature
+
+# General E-Manager
+channel-type.modbus.emanager-error-number-channel.label = E-Manager Error Number
+channel-type.modbus.emanager-operating-state-channel.label = E-Manager Operating State
+channel-type.modbus.emanager-actual-power-channel.label = Actual Power (input or excess)
+channel-type.modbus.emanager-actual-power-signed-channel.label = Actual Power Signed (input or excess)
+channel-type.modbus.emanager-actual-power-consumption-channel.label = Actual Power Consumption
+channel-type.modbus.emanager-power-consumption-setpoint-channel.label = Power Consumption setpoint as sum of all heat pumps
+
+# Heat Pump
+
+channel-type.modbus.heat-pump-error-state-channel.label = Heatpump Error State
+channel-type.modbus.heat-pump-error-number-channel.label = Heatpump Error Number
+channel-type.modbus.heat-pump-state-channel.label = Heatpump State
+channel-type.modbus.heat-pump-operating-state-channel.label = Heatpump Operating State
+channel-type.modbus.heat-pump-t-flow-channel.label = Heatpump T-Flow
+channel-type.modbus.heat-pump-t-return-channel.label = Heatpump T-Return
+channel-type.modbus.heat-pump-vol-sink-channel.label = Heatpump Volume flow heat sink
+channel-type.modbus.heat-pump-t-eqin-channel.label = Heatpump Energy source inlet temperature
+channel-type.modbus.heat-pump-t-eqout-channel.label = Heatpump Energy source outlet temperature
+channel-type.modbus.heat-pump-vol-source-channel.label = Heatpump Volume flow energy source
+channel-type.modbus.heat-pump-compressor-rating-channel.label = Heatpump Compressor unit rating
+channel-type.modbus.heat-pump-qp-heating-channel.label = Heatpump Actual heating capacity
+channel-type.modbus.heat-pump-fi-power-consumption-channel.label = Heatpump Frequency inverter actual power consumption
+channel-type.modbus.heat-pump-cop-channel.label = Heatpump COP
+channel-type.modbus.heat-pump-request-password-channel.label = Heatpump Request Password
+channel-type.modbus.heat-pump-request-type-channel.label = Heatpump Request Type
+channel-type.modbus.heat-pump-request-t-flow-channel.label = Heatpump requested T-Flow
+channel-type.modbus.heat-pump-request-t-return-channel.label = Heatpump requested T-Return
+channel-type.modbus.heat-pump-request-heat-sink-channel.label = Heatpump requested temperature difference between T-Flow and T-Return
+channel-type.modbus.heat-pump-relais-state-channel.label = Heatpump Relais State
+channel-type.modbus.heat-pump-vdae-channel.label = Heatpump VdA E
+channel-type.modbus.heat-pump-vdaq-channel.label = Heatpump VdA Q
+
+# Boiler
+channel-type.modbus.boiler-error-number-channel.label = Boiler Error Number
+channel-type.modbus.boiler-operating-state-channel.label = Boiler Operating State
+channel-type.modbus.boiler-actual-high-temperature-channel.label = Boiler Actual High Temperature
+channel-type.modbus.boiler-actual-low-temperature-channel.label = Boiler Actual Low Temperature
+channel-type.modbus.maximum-boiler-temperature-channel.label = Maximum Boiler Temperature
+
+# Buffer
+channel-type.modbus.buffer-error-number-channel.label = Buffer Error Number
+channel-type.modbus.buffer-operating-state-channel.label = Buffer Operating State
+channel-type.modbus.buffer-actual-high-temperature-channel.label = Buffer Actual High Temperature
+channel-type.modbus.buffer-actual-low-temperature-channel.label = Buffer Actual Low Temperature
+channel-type.modbus.buffer-actual-modbus-temperature-channel.label = Buffer Actual Modbus Temperature
+channel-type.modbus.buffer-request-type-channel.label = Buffer Request Type
+channel-type.modbus.buffer-request-flow-line-temperature-channel.label = Buffer Request Flow Line Temperature
+channel-type.modbus.buffer-request-return-line-temperature-channel.label = Buffer Request Return Line Temperature
+channel-type.modbus.buffer-request-heat-sink-temperature-difference-channel.label = Buffer Request Heat Sink Temperatur Difference
+channel-type.modbus.buffer-request-heating-capacity-channel.label = Buffer Request Heating Capacity
+channel-type.modbus.maximum-buffer-temperature-channel.label = Maximum Buffer Temperature
+
+# Heating Circuit
+channel-type.modbus.heating-circuit-error-number-channel.label = Heating circuit Error Number
+channel-type.modbus.heating-circuit-operating-state-channel.label = Heating circuit Operating State
+channel-type.modbus.heating-circuit-flow-line-temperature-channel.label = Heating circuit Flow Line Temperature
+channel-type.modbus.heating-circuit-return-line-temperature-channel.label = Heating circuit Return Line Temperature
+channel-type.modbus.heating-circuit-room-device-temperature-channel.label = Heating circuit Room Device Temperature
+channel-type.modbus.heating-circuit-setpoint-flow-line-temperature-channel.label = Heating circuit Setpoint Flow Line Temperature
+channel-type.modbus.heating-circuit-operating-mode-channel.label = Heating circuit Operating Mode
+channel-type.modbus.heating-circuit-target-temperature-flow-line-channel.label = Heating circuit Target Temperature Flow Line
+channel-type.modbus.heating-circuit-offset-flow-line-temperature-channel.label = Heating circuit Offset Flow Line Temperature
+channel-type.modbus.heating-circuit-room-heating-temperature-channel.label = Heating circuit Room Heating Temperature
+channel-type.modbus.heating-circuit-room-cooling-temperature-channel.label = Heating circuit Room Cooling Temperature
+
+# Solar
+
+channel-type.modbus.solar-error-number-channel.label = Solar Error Number
+channel-type.modbus.solar-operating-state-channel.label = Operating State
+channel-type.modbus.solar-collector-temperature-channel.label = Solar Collector Temperature
+channel-type.modbus.solar-buffer1-temperature-channel.label = Solar Buffer 1 Temperature
+channel-type.modbus.solar-buffer2-temperature-channel.label = Solar Buffer 2 Temperature
+channel-type.modbus.solar-maximum-buffer-temperature-channel.label = Solar Maximum Buffer Temperature
+channel-type.modbus.solar-buffer-changeover-temperature-channel.label = Solar Buffer Changeover Temperature
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/boiler-thing-types.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/boiler-thing-types.xml
new file mode 100644
index 0000000000000..9febd83fd84bc
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/boiler-thing-types.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+ Lambda Boiler connected through modbus
+
+
+
+
+
+
+
+
+
+ Poll interval in seconds. Increase this if you encounter connection errors.
+ 30
+
+
+
+ Number of retries before giving up reading from this thing.
+ 3
+
+
+
+ Subindex for multiple boiler components
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/buffer-thing-types.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/buffer-thing-types.xml
new file mode 100644
index 0000000000000..c24671ccac8d1
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/buffer-thing-types.xml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+ Lambda Buffer connected through modbus
+
+
+
+
+
+
+
+
+
+ Poll interval in seconds. Increase this if you encounter connection errors.
+ 30
+
+
+
+ Number of retries before giving up reading from this thing.
+ 3
+
+
+
+ Subindex for multiple buffer components
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/general-thing-types.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/general-thing-types.xml
new file mode 100644
index 0000000000000..d2387f1dc0d84
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/general-thing-types.xml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+ Ambient and E-Manager sections connected through modbus
+
+
+
+
+
+
+
+
+
+ Poll interval in seconds. Increase this if you encounter connection errors.
+ 30
+
+
+
+ Number of retries before giving up reading from this thing.
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Power
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/heat-pump-thing-types.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/heat-pump-thing-types.xml
new file mode 100644
index 0000000000000..503cf80c86680
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/heat-pump-thing-types.xml
@@ -0,0 +1,268 @@
+
+
+
+
+
+
+
+
+
+ Lambda Heat Pump connected through modbus
+
+
+
+
+
+
+
+
+ Refresh interval in seconds. Use zero to disable automatic polling.
+ 30
+ s
+
+
+
+
+ 3
+ Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+ true
+
+
+
+
+ See Lambda Modbus description
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:VolumetricFlowRate
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:VolumetricFlowRate
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number:Energy
+
+
+
+
+
+ Number:Energy
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/heating-circuit-thing-types.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/heating-circuit-thing-types.xml
new file mode 100644
index 0000000000000..45dc56c5d01cb
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/heating-circuit-thing-types.xml
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+ Lambda Heating Circuit connected through modbus
+
+
+
+
+
+
+
+
+
+
+ Refresh interval in seconds. Use zero to disable automatic polling.
+ 30
+ s
+
+
+
+
+ 3
+ Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+ true
+
+
+
+
+ See Lambda Modbus description
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/solar-thing-types.xml b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/solar-thing-types.xml
new file mode 100644
index 0000000000000..0343d394b225c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.lambda/src/main/resources/OH-INF/thing/solar-thing-types.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+ Solar thermic component data
+
+
+
+
+
+
+
+
+
+ Poll interval in seconds. Increase this if you encounter connection errors.
+ 30
+
+
+
+ Number of retries before giving up reading from this thing.
+ 3
+
+
+
+ Subindex for multiple solar components
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Error number (0 = No error)
+
+
+
+
+
+
+
+
+ Number
+
+ Operating state of the solar component
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+ Temperature of the solar collector
+
+
+
+
+ Number:Temperature
+
+ Temperature of the solar Buffer 1
+
+
+
+
+ Number:Temperature
+
+ Temperature of the solar Buffer 2
+
+
+
+
+ Number:Temperature
+
+ Maximum Buffer Temperature
+
+
+
+
+ Number:Temperature
+
+ Solar Buffer Changeover Temperature
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 818669b741fcf..f4d931e5a3146 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -277,6 +277,7 @@
org.openhab.binding.modbus
org.openhab.binding.modbus.e3dc
org.openhab.binding.modbus.kermi
+ org.openhab.binding.modbus.lambda
org.openhab.binding.modbus.sbc
org.openhab.binding.modbus.studer
org.openhab.binding.modbus.sungrow
diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml
index 50891eba1ab13..3001a55b642dc 100644
--- a/features/openhab-addons/src/main/resources/footer.xml
+++ b/features/openhab-addons/src/main/resources/footer.xml
@@ -57,6 +57,7 @@
mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.e3dc/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.helioseasycontrols/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.kermi/${project.version}
+ mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.lambda/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sbc/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.stiebeleltron/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.studer/${project.version}