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}