diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index d19f228c8b9..358238e10e5 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1151,6 +1151,11 @@
org.openhab.binding.modbus.helioseasycontrols
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.modbus.kermi
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.modbus.sbc
diff --git a/bundles/org.openhab.binding.modbus.kermi/README.md b/bundles/org.openhab.binding.modbus.kermi/README.md
new file mode 100644
index 00000000000..c5d99df1401
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/README.md
@@ -0,0 +1,235 @@
+# Kermi Heat pump
+
+Integrates the X-Center Device (X-Center Pro) of Kermi Heat pump.
+Kermi X-Center & other attached devices (in progress) are integrated into the Modbus Binding.
+
+This binding was tested and developed with Kermi x-change dynamic pro heat pump (build 2023).
+
+Hint: This binding _may_ also work with devices from "Bösch" in Austria, which is a sub-brand of Kermi, they are nearly identically.
+
+## Prerequisite
+
+Requirement is contacting Kermi Support to activate Modbus-TCP which can be connected directly by network.
+Older devices (non-Pro ?) were connected by Modbus-RCP - maybe you can try to connect them here using a Modbus-TCP modulator (e.x. from waveshare or similar).
+
+## Supported Things
+
+First you need a "Modbus TCP-Bridge" which establishes the basic connection towards your X-Center device.
+
+| Name | Thing Type ID | Description |
+|--------------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------|
+| Kermi Heat Pump X-Center | kermi-xcenter | Provides (electric) Power values, Flow-information, PV-States, Temperatures and general Information of your Kermi Heat Pump |
+
+## Discovery
+
+This binding does not support autodiscovery.
+
+## Device IDs
+
+| Device | Device ID | Comment |
+|---------------------|-----------|-------------------------------------------------|
+| X-Center | 40 | on cascade-circuits: slave1: 41, slave2: 42, ...|
+
+## Thing Configuration
+
+The needed Bridge can be found in the **Modbus Binding** and have to be added manually without Discovery.
+
+1. Create _Modbus TCP Slave (Bridge)_ with matching Settings of your Kermi Device:
+
+- **IP Address** - IP Address or hostname of your heatpump
+- **Port** - Port of modbus on your heatpump (normally 502)
+- **Device ID** - ID on modbus, 40 is default for Kermi (see [Device IDs](#device-ids))
+
+1. Create _Kermi Heat Pump X-Center_ and attach it to the previous installed _Modbus TCP Slave (Bridge)_.
+Configuration requires an appropriate Data Refresh Interval with more than 2000 Milliseconds, default is 5000.
+If it's too fast, you may experience errors in openHAB or your X-Center!
+Reboot if X-Center stops responding on network access.
+You can enable "PV Modulation" if you want to read the values (default: disabled)
+
+Details on Configurations explained below.
+
+### Modbus TCP Slave
+
+| Parameter | Type | Description |
+|-----------|---------|---------------------------------------------------------------|
+| host | text | IP Address or reachable hostname of your device |
+| port | integer | TCP Port of your Kermi device Modbus Settings. Default is 502 |
+| deviceId | integer | Modbus ID of your Kermi device Modbus Settings. Default is 40 |
+
+### Kermi Heat Pump X-Center
+
+Select as Bridge your previously created Modbus TCP Slave.
+
+| Parameter | Type | Description |
+|-----------|---------|----------------------------------------------------------------|
+| refresh | integer | Refresh Rate of X-Center values in Milliseconds (default:5000) |
+| pvEnabled | boolean | Read PV-Modulation (default:false) |
+
+
+### Kermi StorageSystemModule (support planned in future releases)
+
+Select as Bridge a separate (second) Modbus TCP Slave.
+
+| Parameter | Type | Description |
+|-----------|---------|--------------------------------------------------------------------|
+| host | text | IP Address or reachable hostname of your device (same as X-Center) |
+| port | integer | TCP Port of your Kermi device Modbus Settings. Default is 502 |
+| deviceId | integer | Modbus ID of your Kermi device Modbus Settings. Default is 50 |
+
+### Channel-Groups
+
+The X-Center / StorageSystemModule device offers quite an amount of channels.
+They are grouped into 7 channel-groups:
+
+- State
+- EnergySource
+- Charging Circuit
+- Power & Efficiency
+- Workhours
+- Alarm
+- PV Modulation
+
+### Channels
+
+#### Status
+
+| Channel Id | Channel | Type | Description |
+|-----------------|-----------------|--------|----------------------------------------------------------------------|
+| global-state-id | Global State ID | Number | State of heat pump as Number, displayed as readable State-Text in UI |
+
+Possible states:
+
+- Standby
+- Alarm
+- DrinkingWater
+- Heating
+- Defrost
+- Preparing
+- Blocked
+- EVU Blocktime
+- Unavailable
+
+#### Energy-Source
+
+| Channel Id | Channel Label | Channnel Id | Type | Description |
+|----------------------------|----------------------|-------------|---------------------|-------------------------------------------------|
+| exit-temperature | Exit temperature | | Number | in °C - Air temperature exiting heat pump |
+| incoming-temperature | Incoming temperature | | Number | in °C - Air temperature incoming into heat pump |
+| temperature-sensor-outside | Temperature Outside | | Number | in °C - Outside Air Temperature (if connected) |
+
+#### Charging Circuit
+
+| Channel Id | Channel Label | Type | Description |
+|--------------------|--------------------|----------------------------|-----------------------------------------------------------------------------------|
+| flow-temperature | Flow temperature | Number | in °C - Water temperature **from** heat pump to drinking water or heating circuit |
+| return-temperature | Return temperature | Number | in °C - Water temperature returning back **to** heat pump |
+| flow-speed | Flow speed | Number | in l/min - Flow speed of the water |
+
+#### Power and efficiency
+
+| Channel Id | Channel Label | Type | Description |
+|--------------------------------|---------------------------------------|---------------|--------------------------------------------------|
+| cop | Current COP | Number | current cop overall (Coefficient Of Performance) |
+| cop-heating | Current COP heating | Number | cop for heating |
+| cop-drinkingwater | Current COP drinking water | Number | cop for drinking water |
+| cop-cooling | Current COP cooling | Number | cop for cooling |
+| | | | |
+| power | Current power | Number | in W (Watt) - Power overall |
+| power-heating | Current power heating | Number | in W (Watt) - Power for heating |
+| power-drinkingwater | Current power drinking water | Number | in W (Watt) - Power for drinking water |
+| power-cooling | Current power cooling | Number | in W (Watt) - Power for cooling |
+| | | | |
+| electrical-power | Current electric power | Number | in W (Watt) - electric Power overall |
+| electrical-power-heating | Current electric power heating | Number | in W (Watt) - electric Power for heating |
+| electrical-power-drinkingwater | Current electric power drinking water | Number | in W (Watt) - electric Power for drinking water |
+| electrical-power-cooling | Current electric power cooling | Number | in W (Watt) - electric Power for cooling |
+
+#### Workhours
+
+| Channel Id | Channel Label | Type | Description |
+|--------------------------------|--------------------------------|--------------|--------------------------------------------------------|
+| workhours-fan | Fan workhours | Number | in h (hour) - worked hours of the fan |
+| workhours-storage-loading-pump | Storage Loading Pump workhours | Number | in h (hour) - worked hours of the storage loading pump |
+| workhours-compressor | Compressor workhours | Number | in h (hour) - worked hours of the compressor |
+
+#### Alarm
+
+| Channel Id | Channel Label | Type | Description |
+|-------------|---------------|--------|---------------------------------|
+| alarm-state | Alarm state | Switch | On / true if an alarm is raised |
+
+#### PV Modulation
+
+| Channel Id | Channel Label | Type | Description |
+|-------------------------------------|-----------------------------------|---------------------|-------------------------------------------------------------------|
+| pv-state | PV Modulation Active | Switch | On / true if PV Modulation is currently active |
+| pv-power | PV Power | Numbery | in W (Watt) - Power of PV Modulation |
+| pv-target-temperature-heating | Target temperature heating | Number | in °C - target Temperature in PV Mode of heating (storage) |
+| pv-target-temperature-drinkingwater | Target temperature drinking water | Number | in °C - target Temperature in PV Mode of drinking water (storage) |
+
+## Full Example
+
+Attention: Configuration by file is not recommended. You can configure everything in the main UI.
+
+### `kermi.things` Example
+
+```java
+Bridge modbus:tcp:device "Kermi X-Center Modbus TCP" [ host="xcenter", port=502, id=40 ] {
+ Bridge kermi-xcenter heatpump "Kermi X-Center Heat Pump" [ refresh=5000, pvEnabled=false ]
+}
+```
+
+### Items
+
+```java
+Number XCenter_Global_State_Id "X-Center Global State ID" (kermi) { channel="modbus:tcp:device:heatpump:state#global-state-id" }
+
+Number:Temperature Heatpump_FlowTemperature "Flow Temperature" (kermi,persist) { channel="modbus:tcp:device:heatpump:charging-circuit#flow-temperature" }
+Number:Temperature Heatpump_ReturnTemperature "Return Temperature" (kermi,persist) { channel="modbus:tcp:device:heatpump:charging-circuit#return-temperature" }
+Number:VolumetricFlowRate Heatpump_FlowSpeed "Flow Speed" (kermi,persist) { channel="modbus:tcp:device:heatpump:charging-circuit#flow-speed" }
+
+Number:Temperature Heatpump_ExitTemperature "Exit Temperature" (kermi,persist) { channel="modbus:tcp:device:heatpump:energy-source#exit-temperature" }
+Number:Temperature Heatpump_Incomingtemperature "Incoming temperature" (kermi,persist) { channel="modbus:tcp:device:heatpump:energy-source#incoming-temperature" }
+Number:Temperature Heatpump_TemperatureOutside "Temperature Outside" (kermi,persist) { channel="modbus:tcp:device:heatpump:energy-source#temperature-sensor-outside" }
+
+Number Heatpump_CurrentCOP "Current COP" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#cop" }
+Number Heatpump_CurrentCOPHeating "Current COP Heating" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#cop-heating" }
+Number Heatpump_CurrentCOPdrinkingwater "Current COP drinking water" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#cop-drinkingwater" }
+Number Heatpump_CurrentCOPCooling "Current COP Cooling" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#cop-cooling" }
+Number:Power Heatpump_CurrentPower "Current Power" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#power" }
+Number:Power Heatpump_Currentpowerheating "Current power heating" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#power-heating" }
+Number:Power Heatpump_CurrentPowerDrinkingWater "Current Power Drinking Water" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#power-drinkingwater" }
+Number:Power Heatpump_Currentpowercooling "Current power cooling" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#power-cooling" }
+Number:Power Heatpump_CurrentElectricPower "Current Electric Power" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#electric-power" }
+Number:Power Heatpump_CurrentElectricPowerHeating "Current Electric Power Heating" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#electric-power-heating" }
+Number:Power Heatpump_Currentelectricpowerdrinkingwater "Current electric power drinking water" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#electric-power-drinkingwater" }
+Number:Power Heatpump_CurrentElectricPowerCooling "Current Electric Power Cooling" (kermi) { channel="modbus:tcp:device:heatpump:power-channel#electric-power-cooling" }
+
+Switch Heatpump_PVModulationActive "PV Modulation Active" (kermi) { channel="modbus:tcp:device:heatpump:pv-modulation#pv-state" }
+Number:Power Heatpump_PVPower "PV Power" (kermi) { channel="modbus:tcp:device:heatpump:pv-modulation#pv-power" }
+Number:Temperature Heatpump_PVTempHeating "PV Temp Heating" (kermi) { channel="modbus:tcp:device:heatpump:pv-modulation#pv-target-temperature-heating" }
+Number:Temperature Heatpump_PVTempDrinkingwater "PV Temp Drinkingwater" (kermi) { channel="modbus:tcp:device:heatpump:pv-modulation#pv-target-temperature-drinkingwater" }
+
+Number:Time Heatpump_FanWorkhours "Fan Workhours" (kermi) { channel="modbus:tcp:device:heatpump:workhours#workhours-fan" }
+Number:Time Heatpump_StorageLoadingPumpWorkhours "StorageLoadingPump Workhours" (kermi) { channel="modbus:tcp:device:heatpump:workhours#workhours-storage-loading-pump" }
+Number:Time Heatpump_CompressorWorkhours "Compressor Workhours" (kermi) { channel="modbus:tcp:device:heatpump:workhours#workhours-compressor" }
+```
+
+## Persistence
+
+You can / should persist some items you want to track, maybe you track your power consumption with another device (PV-System or 'smart' electricity meter), so you can compare these values.
+
+Suggestion / Optional:
+As these (power & temperature) values are long-running ones, maybe you should invest a little amount of time using influxDB as persistence (additionally to your existing default persistence in openHAB).
+InfluxDB is a good storage-solution for long terms and uses very small space for its data. Please read the documentation for better understanding how it works.
+
+
+### Visualization
+
+As many other users I like and use the Grafana approach (in combination with influxdb).
+See here for more information [InfluxDB & Grafana Tutorial](https://community.openhab.org/t/influxdb-grafana-persistence-and-graphing/13761)
+
+### Credits
+
+Credits goes to Bernd Weymann (Author of E3DC-Modbus-Binding).
+I used its basic structure / code and handling of Modbus Messages for this binding. Thanks.
diff --git a/bundles/org.openhab.binding.modbus.kermi/pom.xml b/bundles/org.openhab.binding.modbus.kermi/pom.xml
new file mode 100644
index 00000000000..679f69badf4
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.3.0-SNAPSHOT
+
+
+ org.openhab.binding.modbus.kermi
+
+ openHAB Add-ons :: Bundles :: Kermi Modbus Binding
+
+
+
+ org.openhab.addons.bundles
+ org.openhab.binding.modbus
+ ${project.version}
+ provided
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiBindingConstants.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiBindingConstants.java
new file mode 100644
index 00000000000..4d75e8fc34c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiBindingConstants.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.ModbusBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link KermiBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+@NonNullByDefault
+public class KermiBindingConstants {
+
+ public static final String STATE_AND_ALARM_READ_ERRORS = "Status And Alarm Modbus Read Errors";
+ public static final String STATE_READ_ERROR = "Information Modbus Read Error";
+ public static final String DATA_READ_ERROR = "Data Modbus Read Error";
+ static final String PV_READ_ERROR = "PV Modbus Read Error";
+ public static final String ALARM_GROUP = "xcenter-alarm";
+ public static final String STATE_GROUP = "xcenter-state";
+ public static final String ENERGY_SOURCE_GROUP = "xcenter-energy-source";
+ public static final String CHARGING_CIRCUIT_GROUP = "xcenter-charging-circuit";
+ public static final String POWER_GROUP = "xcenter-power";
+ public static final String WORKHOURS_GROUP = "xcenter-workhours";
+ public static final String PV_GROUP = "xcenter-pv-modulation";
+ private static final String BINDING_ID = ModbusBindingConstants.BINDING_ID;
+
+ // Supported Thing Types
+ public static final ThingTypeUID THING_TYPE_KERMI_XCENTER = new ThingTypeUID(BINDING_ID, "kermi-xcenter");
+
+ // Channels for State
+ public static final String GLOBAL_STATE_ID_CHANNEL = "global-state-id";
+
+ // Alarm State
+ public static final String ALARM_STATE_CHANNEL = "alarm-state";
+
+ // Energy Source
+ public static final String FLOW_TEMPERATURE_CHANNEL = "flow-temperature";
+ public static final String RETURN_TEMPERATURE_CHANNEL = "return-temperature";
+ public static final String FLOW_SPEED_CHANNEL = "flow-speed";
+
+ // Charging Circuit
+ public static final String EXIT_TEMPERATURE_CHANNEL = "exit-temperature";
+ public static final String INCOMING_TEMPERATURE_CHANNEL = "incoming-temperature";
+ public static final String TEMPERATURE_SENSOR_OUTSIDE_CHANNEL = "temperature-sensor-outside";
+
+ // Power
+ public static final String COP_CHANNEL = "cop";
+ public static final String COP_HEATING_CHANNEL = "cop-heating";
+ public static final String COP_DRINKINGWATER_CHANNEL = "cop-drinkingwater";
+ public static final String COP_COOLING_CHANNEL = "cop-cooling";
+
+ public static final String POWER_CHANNEL = "power";
+ public static final String POWER_HEATING_CHANNEL = "power-heating";
+ public static final String POWER_DRINKINGWATER_CHANNEL = "power-drinkingwater";
+ public static final String POWER_COOLING_CHANNEL = "power-cooling";
+
+ public static final String ELECTRIC_POWER_CHANNEL = "electric-power";
+ public static final String ELECTRIC_POWER_HEATING_CHANNEL = "electric-power-heating";
+ public static final String ELECTRIC_POWER_DRINKINGWATER_CHANNEL = "electric-power-drinkingwater";
+ public static final String ELECTRIC_POWER_COOLING_CHANNEL = "electric-power-cooling";
+
+ // Work hours
+ public static final String WORKHOURS_FAN_CHANNEL = "workhours-fan";
+ public static final String WORKHOURS_STORAGE_LOADING_PUMP_CHANNEL = "workhours-storage-loading-pump";
+ public static final String WORKHOURS_COMPRESSOR_CHANNEL = "workhours-compressor";
+
+ // PV
+ public static final String PV_STATE_CHANNEL = "pv-state";
+ public static final String PV_POWER_CHANNEL = "pv-power";
+ public static final String PV_TARGET_TEMPERATURE_HEATING_CHANNEL = "pv-target-temperature-heating";
+ public static final String PV_TARGET_TEMPERATURE_DRINKINGWATER_CHANNEL = "pv-target-temperature-drinkingwater";
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiConfiguration.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiConfiguration.java
new file mode 100644
index 00000000000..f1401dc218f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link KermiConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+@NonNullByDefault
+public class KermiConfiguration {
+
+ public int refresh = 5000;
+ public boolean pvEnabled = false;
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiHandlerFactory.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiHandlerFactory.java
new file mode 100644
index 00000000000..899cd1272ff
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/KermiHandlerFactory.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.modbus.kermi.internal.handler.KermiXcenterThingHandler;
+import org.openhab.core.thing.Bridge;
+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;
+
+/**
+ * The {@link KermiHandlerFactory} is responsible for creating things and thing handlers.
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.kermi", service = ThingHandlerFactory.class)
+public class KermiHandlerFactory extends BaseThingHandlerFactory {
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return thingTypeUID.equals(KermiBindingConstants.THING_TYPE_KERMI_XCENTER);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (KermiBindingConstants.THING_TYPE_KERMI_XCENTER.equals(thingTypeUID)) {
+ return new KermiXcenterThingHandler((Bridge) thing);
+ } // else here
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/AlarmDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/AlarmDTO.java
new file mode 100644
index 00000000000..4ce1e2a229b
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/AlarmDTO.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.ALARM_REG_SIZE;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * The {@link AlarmDTO} Data object for Kermi Xcenter
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class AlarmDTO implements Data {
+
+ public OnOffType alarmIsActive;
+
+ public AlarmDTO(byte[] bArray) {
+ int status = ModbusBitUtilities.extractBit(bArray, ALARM_REG_SIZE);
+ alarmIsActive = OnOffType.from(status != 0);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/ChargingCircuitDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/ChargingCircuitDTO.java
new file mode 100644
index 00000000000..0c04990ec3d
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/ChargingCircuitDTO.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import static org.openhab.core.library.unit.Units.LITRE_PER_MINUTE;
+
+import javax.measure.quantity.Temperature;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ValueBuffer;
+import org.openhab.core.library.dimension.VolumetricFlowRate;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * The {@link ChargingCircuitDTO} Data object for Kermi Xcenter
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class ChargingCircuitDTO implements Data {
+
+ public QuantityType flowTemperature;
+ public QuantityType returnFlowTemperature;
+ public QuantityType flowSpeed;
+
+ public ChargingCircuitDTO(byte[] bArray) {
+ ValueBuffer wrap = ValueBuffer.wrap(bArray);
+ flowTemperature = QuantityType.valueOf(DataConverter.getSDoubleValue(wrap, 0.1), SIUnits.CELSIUS);
+ returnFlowTemperature = QuantityType.valueOf(DataConverter.getSDoubleValue(wrap, 0.1), SIUnits.CELSIUS);
+ flowSpeed = QuantityType.valueOf(DataConverter.getSDoubleValue(wrap, 0.1), LITRE_PER_MINUTE);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/DataConverter.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/DataConverter.java
new file mode 100644
index 00000000000..1c0ad3189ac
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/DataConverter.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
+import org.openhab.core.io.transport.modbus.ValueBuffer;
+
+/**
+ * The {@link DataConverter} Helper class to convert bytes from modbus into desired data format
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Kai Neuhaus - used for Modbus.Kermi Binding
+ */
+@NonNullByDefault
+public class DataConverter {
+
+ /**
+ * Retrieves a double value from the first two bytes of the provided {@link ValueBuffer} using the given correction
+ * factor.
+ *
+ * @param wrap The {@link ValueBuffer} from which to extract the bytes.
+ * @param factor The correction factor to apply to the extracted value.
+ * @return The calculated double value.
+ */
+ public static double getUDoubleValue(ValueBuffer wrap, double factor) {
+ return round(wrap.getUInt16() * factor, 2);
+ }
+
+ /**
+ * Retrieves a signed double value from the first two bytes of the provided {@link ValueBuffer} using the given
+ * correction factor.
+ *
+ * @param wrap The {@link ValueBuffer} from which to extract the bytes. This {@link ValueBuffer} should contain at
+ * least two bytes.
+ * @param factor The correction factor to apply to the extracted value. This factor is used to adjust the calculated
+ * value.
+ * @return The calculated signed double value. The value is obtained by extracting the first two bytes from the
+ * {@link ValueBuffer},
+ * converting them to a signed 16-bit integer, multiplying it by the correction factor, and rounding the
+ * result to two decimal places.
+ */
+ public static double getSDoubleValue(ValueBuffer wrap, double factor) {
+ return round(wrap.getSInt16() * factor, 2);
+ }
+
+ public static String getString(byte[] bArray) {
+ return ModbusBitUtilities.extractStringFromBytes(bArray, 0, bArray.length, StandardCharsets.US_ASCII).trim();
+ }
+
+ public static int toInt(BitSet bitSet) {
+ int intValue = 0;
+ for (int bit = 0; bit < bitSet.length(); bit++) {
+ if (bitSet.get(bit)) {
+ intValue |= (1 << bit);
+ }
+ }
+ return intValue;
+ }
+
+ public static double round(double value, int places) {
+ if (places < 0) {
+ throw new IllegalArgumentException();
+ }
+
+ long factor = (long) Math.pow(10, places);
+ long tmp = Math.round(value * factor);
+ return (double) tmp / factor;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/EnergySourceDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/EnergySourceDTO.java
new file mode 100644
index 00000000000..ac55db98c3f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/EnergySourceDTO.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import javax.measure.quantity.Temperature;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ValueBuffer;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * The {@link EnergySourceDTO} Data object for Kermi Xcenter
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class EnergySourceDTO implements Data {
+
+ public QuantityType exitTemperature;
+ public QuantityType incomingTemperature;
+ public QuantityType outsideTemperature;
+
+ public EnergySourceDTO(byte[] bArray) {
+ ValueBuffer wrap = ValueBuffer.wrap(bArray);
+ exitTemperature = QuantityType.valueOf(DataConverter.getSDoubleValue(wrap, 0.1), SIUnits.CELSIUS);
+ incomingTemperature = QuantityType.valueOf(DataConverter.getSDoubleValue(wrap, 0.1), SIUnits.CELSIUS);
+ outsideTemperature = QuantityType.valueOf(DataConverter.getSDoubleValue(wrap, 0.1), SIUnits.CELSIUS);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/PowerDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/PowerDTO.java
new file mode 100644
index 00000000000..2ce4bba8180
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/PowerDTO.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import javax.measure.quantity.Power;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ValueBuffer;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+
+/**
+ * The {@link PowerDTO} Data object for Kermi Xcenter State Block
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class PowerDTO implements Data {
+
+ public DecimalType cop;
+ public DecimalType copHeating;
+ public DecimalType copDrinkingwater;
+ public DecimalType copCooling;
+
+ public QuantityType power;
+ public QuantityType powerHeating;
+ public QuantityType powerDrinkingwater;
+ public QuantityType powerCooling;
+
+ public QuantityType electricPower;
+ public QuantityType electricPowerHeating;
+ public QuantityType electricPowerDrinkingwater;
+ public QuantityType electricPowerCooling;
+
+ public PowerDTO(byte[] bArray) {
+
+ ValueBuffer wrap = ValueBuffer.wrap(bArray);
+
+ cop = new DecimalType(DataConverter.getUDoubleValue(wrap, 0.1));
+ copHeating = new DecimalType(DataConverter.getUDoubleValue(wrap, 0.1));
+ copDrinkingwater = new DecimalType(DataConverter.getUDoubleValue(wrap, 0.1));
+ copCooling = new DecimalType(DataConverter.getUDoubleValue(wrap, 0.1));
+
+ power = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ powerHeating = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ powerDrinkingwater = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ powerCooling = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+
+ electricPower = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ electricPowerHeating = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ electricPowerDrinkingwater = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ electricPowerCooling = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 100), Units.WATT);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/PvDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/PvDTO.java
new file mode 100644
index 00000000000..f6da0ab9da7
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/PvDTO.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import javax.measure.quantity.Power;
+import javax.measure.quantity.Temperature;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
+import org.openhab.core.io.transport.modbus.ValueBuffer;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+
+/**
+ * The {@link PvDTO} Data object for Kermi Xcenter
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class PvDTO implements Data {
+
+ public OnOffType pvModulationActive;
+ public QuantityType pvModulationPower;
+ public QuantityType pvTargetTemperatureHeating;
+ public QuantityType pvTargetTemperatureDrinkingwater;
+
+ public PvDTO(byte[] bArray) {
+
+ int modActive = ModbusBitUtilities.extractBit(bArray, 0);
+ pvModulationActive = modActive == 0 ? OnOffType.OFF : OnOffType.ON;
+
+ ValueBuffer wrap = ValueBuffer.wrap(bArray);
+ // skip first bit -> modActive-Value
+ wrap.position(2);
+
+ pvModulationPower = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.1), Units.WATT);
+ pvTargetTemperatureHeating = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.1), SIUnits.CELSIUS);
+ pvTargetTemperatureDrinkingwater = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.1),
+ SIUnits.CELSIUS);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/StateDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/StateDTO.java
new file mode 100644
index 00000000000..a5b11944d4f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/StateDTO.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
+import org.openhab.core.library.types.DecimalType;
+
+/**
+ * The {@link StateDTO} Data object for Kermi Xcenter
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class StateDTO implements Data {
+
+ public DecimalType globalStateId;
+
+ public StateDTO(byte[] bArray) {
+ int status = ModbusBitUtilities.extractBit(bArray, 0);
+ globalStateId = new DecimalType(status);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/WorkHoursDTO.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/WorkHoursDTO.java
new file mode 100644
index 00000000000..f3bb44d9961
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/dto/WorkHoursDTO.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import javax.measure.quantity.Time;
+
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.core.io.transport.modbus.ValueBuffer;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+
+/**
+ * The {@link WorkHoursDTO} Data object for Kermi Xcenter State Block
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class WorkHoursDTO implements Data {
+
+ public QuantityType workHoursFan;
+ public QuantityType workHoursStorageLoadingPump;
+ public QuantityType workHoursCompressor;
+
+ public WorkHoursDTO(byte[] bArray) {
+
+ ValueBuffer wrap = ValueBuffer.wrap(bArray);
+
+ workHoursFan = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.1), Units.HOUR);
+ workHoursStorageLoadingPump = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.1), Units.HOUR);
+ workHoursCompressor = QuantityType.valueOf(DataConverter.getUDoubleValue(wrap, 0.1), Units.HOUR);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/handler/KermiXcenterThingHandler.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/handler/KermiXcenterThingHandler.java
new file mode 100644
index 00000000000..bce15580aea
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/handler/KermiXcenterThingHandler.java
@@ -0,0 +1,613 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.handler;
+
+import static org.openhab.binding.modbus.kermi.internal.KermiBindingConstants.*;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+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.kermi.internal.KermiBindingConstants;
+import org.openhab.binding.modbus.kermi.internal.KermiConfiguration;
+import org.openhab.binding.modbus.kermi.internal.dto.AlarmDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.ChargingCircuitDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.EnergySourceDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.PowerDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.PvDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.StateDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.WorkHoursDTO;
+import org.openhab.binding.modbus.kermi.internal.modbus.Data;
+import org.openhab.binding.modbus.kermi.internal.modbus.Parser;
+import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
+import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
+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.PollTask;
+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.binding.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link KermiXcenterThingHandler} Basic modbus connection to the Kermi device(s)
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+@NonNullByDefault
+public class KermiXcenterThingHandler extends BaseBridgeHandler {
+
+ public enum ReadStatus {
+ NOT_RECEIVED,
+ READ_SUCCESS,
+ READ_FAILED
+ }
+
+ // Energy source
+ private ChannelUID exitTemperatureChannel;
+ private ChannelUID incomingTemperatureChannel;
+ private ChannelUID outsideTemperatureChannel;
+
+ // Charging circuit
+ private ChannelUID flowTemperatureChannel;
+ private ChannelUID returnFlowTemperatureChannel;
+ private ChannelUID flowSpeedChannel;
+
+ // Power
+ private ChannelUID copChannel;
+ private ChannelUID copHeatingChannel;
+ private ChannelUID copDrinkingWaterChannel;
+ private ChannelUID copCoolingChannel;
+
+ private ChannelUID powerChannel;
+ private ChannelUID powerHeatingChannel;
+ private ChannelUID powerDrinkingWaterChannel;
+ private ChannelUID powerCoolingChannel;
+
+ private ChannelUID electricPowerChannel;
+ private ChannelUID electricPowerHeatingChannel;
+ private ChannelUID electricPowerDrinkingWaterChannel;
+ private ChannelUID electricPowerCoolingChannel;
+
+ // Global State
+ // private ChannelUID globalStateChannel;
+ private ChannelUID globalStateIdChannel;
+
+ // Alarm State
+ private ChannelUID alarmStateChannel;
+
+ // Work hours
+ private ChannelUID workHoursFanChannel;
+ private ChannelUID workHoursStorageLoadingPumpChannel;
+ private ChannelUID workHoursCompressorChannel;
+
+ // PV Modulation
+ private ChannelUID pvStateChannel;
+ private ChannelUID pvPowerChannel;
+ private ChannelUID pvTargetTemperatureHeatingChannel; // READ-WRITE
+ private ChannelUID pvTargetTemperatureDrinkingWaterChannel; // READ-WRITE
+
+ private final Logger logger = LoggerFactory.getLogger(KermiXcenterThingHandler.class);
+
+ private final Parser alarmParser = new Parser(Data.DataType.ALARM_STATE);
+ private ReadStatus alarmRead = ReadStatus.NOT_RECEIVED;
+ private final Parser chargingCircuitParser = new Parser(Data.DataType.CHARGING_CIRCUIT);
+ private ReadStatus chargingCircuitRead = ReadStatus.NOT_RECEIVED;
+ private final Parser energySourceParser = new Parser(Data.DataType.ENERGY_SOURCE);
+ private ReadStatus energySourceRead = ReadStatus.NOT_RECEIVED;
+ private final Parser powerParser = new Parser(Data.DataType.POWER);
+ private ReadStatus powerRead = ReadStatus.NOT_RECEIVED;
+ private final Parser pvParser = new Parser(Data.DataType.PV);
+ private ReadStatus pvRead = ReadStatus.NOT_RECEIVED;
+ private final Parser stateParser = new Parser(Data.DataType.STATE);
+ private ReadStatus stateRead = ReadStatus.NOT_RECEIVED;
+ private final Parser workHoursParser = new Parser(Data.DataType.WORK_HOURS);
+ private ReadStatus workHoursRead = ReadStatus.NOT_RECEIVED;
+
+ private @Nullable PollTask alarmPoller;
+ private @Nullable PollTask chargingCircuitPoller;
+ private @Nullable PollTask energySourcePoller;
+ private @Nullable PollTask powerPoller;
+ private @Nullable PollTask pvPoller;
+ private @Nullable PollTask statePoller;
+ private @Nullable PollTask workHourPoller;
+
+ private List<@Nullable PollTask> pollTasks = new ArrayList<>();
+
+ private @Nullable KermiConfiguration config;
+
+ /**
+ * Communication interface to the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusCommunicationInterface comms = null;
+ private int slaveId;
+
+ public KermiXcenterThingHandler(Bridge thing) {
+ super(thing);
+
+ // STATE
+ globalStateIdChannel = channelUID(thing, KermiBindingConstants.STATE_GROUP, GLOBAL_STATE_ID_CHANNEL);
+
+ // ALARM
+ alarmStateChannel = channelUID(thing, KermiBindingConstants.ALARM_GROUP, ALARM_STATE_CHANNEL);
+
+ // Energy source
+ exitTemperatureChannel = channelUID(thing, KermiBindingConstants.ENERGY_SOURCE_GROUP, EXIT_TEMPERATURE_CHANNEL);
+ incomingTemperatureChannel = channelUID(thing, KermiBindingConstants.ENERGY_SOURCE_GROUP,
+ INCOMING_TEMPERATURE_CHANNEL);
+ outsideTemperatureChannel = channelUID(thing, KermiBindingConstants.ENERGY_SOURCE_GROUP,
+ TEMPERATURE_SENSOR_OUTSIDE_CHANNEL);
+
+ // Loading circuit
+ flowTemperatureChannel = channelUID(thing, KermiBindingConstants.CHARGING_CIRCUIT_GROUP,
+ FLOW_TEMPERATURE_CHANNEL);
+ returnFlowTemperatureChannel = channelUID(thing, KermiBindingConstants.CHARGING_CIRCUIT_GROUP,
+ RETURN_TEMPERATURE_CHANNEL);
+ flowSpeedChannel = channelUID(thing, KermiBindingConstants.CHARGING_CIRCUIT_GROUP, FLOW_SPEED_CHANNEL);
+
+ // Power
+ copChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, COP_CHANNEL);
+ copHeatingChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, COP_HEATING_CHANNEL);
+ copDrinkingWaterChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, COP_DRINKINGWATER_CHANNEL);
+ copCoolingChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, COP_COOLING_CHANNEL);
+
+ powerChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, POWER_CHANNEL);
+ powerHeatingChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, POWER_HEATING_CHANNEL);
+ powerDrinkingWaterChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, POWER_DRINKINGWATER_CHANNEL);
+ powerCoolingChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, POWER_COOLING_CHANNEL);
+
+ electricPowerChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP, ELECTRIC_POWER_CHANNEL);
+ electricPowerHeatingChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP,
+ ELECTRIC_POWER_HEATING_CHANNEL);
+ electricPowerDrinkingWaterChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP,
+ ELECTRIC_POWER_DRINKINGWATER_CHANNEL);
+ electricPowerCoolingChannel = channelUID(thing, KermiBindingConstants.POWER_GROUP,
+ ELECTRIC_POWER_COOLING_CHANNEL);
+
+ // Work hours
+ workHoursFanChannel = channelUID(thing, KermiBindingConstants.WORKHOURS_GROUP, WORKHOURS_FAN_CHANNEL);
+ workHoursStorageLoadingPumpChannel = channelUID(thing, KermiBindingConstants.WORKHOURS_GROUP,
+ WORKHOURS_STORAGE_LOADING_PUMP_CHANNEL);
+ workHoursCompressorChannel = channelUID(thing, KermiBindingConstants.WORKHOURS_GROUP,
+ WORKHOURS_COMPRESSOR_CHANNEL);
+
+ // PV Modulation
+ pvStateChannel = channelUID(thing, KermiBindingConstants.PV_GROUP, PV_STATE_CHANNEL);
+ pvPowerChannel = channelUID(thing, KermiBindingConstants.PV_GROUP, PV_POWER_CHANNEL);
+ pvTargetTemperatureHeatingChannel = channelUID(thing, KermiBindingConstants.PV_GROUP,
+ PV_TARGET_TEMPERATURE_HEATING_CHANNEL); // READ-WRITE
+ pvTargetTemperatureDrinkingWaterChannel = channelUID(thing, KermiBindingConstants.PV_GROUP,
+ PV_TARGET_TEMPERATURE_DRINKINGWATER_CHANNEL); // READ-WRITE
+ }
+
+ public @Nullable ModbusCommunicationInterface getComms() {
+ return comms;
+ }
+
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // no control of Kermi device possible yet
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.UNKNOWN);
+ scheduler.execute(() -> {
+ KermiConfiguration localConfig = getConfigAs(KermiConfiguration.class);
+ config = localConfig;
+
+ if (config == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Kermi Configuration missing");
+ return;
+ }
+ ModbusCommunicationInterface localComms = connectEndpoint();
+ if (localComms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Connection failure on initialize...");
+ return;
+ }
+
+ /**
+ * Before asking: i tried to create 1 "big poller" and separate the data from the received bytes.
+ * But when polling "registers" which are not declared (from Kermi), the answer was corrupted or
+ * invalid.
+ * e.x. polling 100 to 150, expecting 50 registers the device returns 18 only, strange behaviour.
+ * Maybe polling will be improved in future - to have not that "huge" amount of pollerTasks.
+ */
+
+ // very slow requests
+ ModbusReadRequestBlueprint workHoursRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, WORK_HOURS_REG_START, WORK_HOURS_REG_SIZE, 3);
+ workHourPoller = localComms.registerRegularPoll(workHoursRequest, SLOW_POLL_REFRESH_TIME_MS,
+ WARMUP_TIME_LONG, this::handleWorkHoursResult, this::handleWorkHoursFailure);
+
+ pollTasks.add(workHourPoller);
+
+ // register low speed state & alarm poller
+ ModbusReadRequestBlueprint alarmRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, ALARM_REG_START, ALARM_REG_SIZE, 3);
+ alarmPoller = localComms.registerRegularPoll(alarmRequest, STATE_POLL_REFRESH_TIME_MS, WARMUP_TIME_LONG,
+ this::handleAlarmResult, this::handleAlarmFailure);
+
+ pollTasks.add(alarmPoller);
+
+ ModbusReadRequestBlueprint stateRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, STATE_REG_START, STATE_REG_SIZE, 3);
+ statePoller = localComms.registerRegularPoll(stateRequest, STATE_POLL_REFRESH_TIME_MS, WARMUP_TIME_LONG,
+ this::handleStateResult, this::handleStateFailure);
+
+ pollTasks.add(statePoller);
+
+ // default polling speed
+ ModbusReadRequestBlueprint chargingcircuitRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, CHARGING_CIRCUIT_REG_START,
+ CHARGING_CIRCUIT_REG_SIZE, 3);
+ chargingCircuitPoller = localComms.registerRegularPoll(chargingcircuitRequest, localConfig.refresh,
+ WARMUP_TIME_SHORT, this::handleChargingCircuitResult, this::handleChargingCircuitFailure);
+
+ pollTasks.add(chargingCircuitPoller);
+
+ ModbusReadRequestBlueprint energySourceRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, ENERGY_SOURCE_REG_START, ENERGY_SOURCE_REG_SIZE, 3);
+ energySourcePoller = localComms.registerRegularPoll(energySourceRequest, localConfig.refresh,
+ WARMUP_TIME_SHORT, this::handleEnergySourceResult, this::handleEnergySourceFailure);
+
+ pollTasks.add(energySourcePoller);
+
+ ModbusReadRequestBlueprint powerRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, POWER_REG_START, POWER_REG_SIZE, 3);
+ powerPoller = localComms.registerRegularPoll(powerRequest, localConfig.refresh, WARMUP_TIME_SHORT,
+ this::handlePowerResult, this::handlePowerFailure);
+
+ pollTasks.add(powerPoller);
+
+ if (localConfig.pvEnabled) {
+ ModbusReadRequestBlueprint pvRequest = new ModbusReadRequestBlueprint(slaveId,
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, PV_MODULATION_REG_START, PV_MODULATION_REG_SIZE,
+ 3);
+ pvPoller = localComms.registerRegularPoll(pvRequest, localConfig.refresh, WARMUP_TIME_SHORT,
+ this::handlePvResult, this::handlePvFailure);
+
+ pollTasks.add(pvPoller);
+ }
+
+ });
+ }
+
+ /**
+ * Get a reference to the modbus endpoint
+ */
+ private @Nullable ModbusCommunicationInterface connectEndpoint() {
+ if (comms != null) {
+ return comms;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("SlaveEndpointThingHandler is null, Thing & Bridge are offline");
+ return null;
+ }
+ try {
+ slaveId = slaveEndpointThingHandler.getSlaveId();
+ comms = slaveEndpointThingHandler.getCommunicationInterface();
+ } catch (EndpointNotInitializedException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Slave Endpoint not initialized");
+ logger.debug("Slave Endpoint not initialized, Thing is offline");
+ return null;
+ }
+ if (comms == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ logger.debug("CommunicationInterface is null, Thing & Bridge are offline");
+ return null;
+ } else {
+ return comms;
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ if (bridge.getHandler() instanceof ModbusEndpointThingHandler thingHandler) {
+ return thingHandler;
+ } else {
+ logger.debug("Bridge handler null or unexpected type: {}", bridge.getHandler());
+ return null;
+ }
+ }
+
+ /**
+ * Returns the channel UID for the specified group and channel id
+ *
+ * @param group String of channel group
+ * @param id String the channel id in that group
+ * @return the globally unique channel uid
+ */
+ private ChannelUID channelUID(Thing t, String group, String id) {
+ return new ChannelUID(t.getUID(), group, id);
+ }
+
+ /******** Start ResultHandler *****************/
+
+ private void handleAlarmResult(AsyncModbusReadResult result) {
+ if (alarmRead != ReadStatus.READ_SUCCESS) {
+ // update status only if bit switches
+ alarmRead = ReadStatus.READ_SUCCESS;
+ updateStatus();
+ }
+ alarmParser.handle(result);
+ Optional dtoOpt = alarmParser.parse(Data.DataType.ALARM_STATE);
+ if (dtoOpt.isPresent()) {
+ AlarmDTO alarmDTO = (AlarmDTO) dtoOpt.get();
+ updateState(alarmStateChannel, alarmDTO.alarmIsActive);
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.ALARM_STATE, alarmParser.toString());
+ }
+ }
+
+ private void handleAlarmFailure(AsyncModbusFailure result) {
+ if (alarmRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ alarmRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ private void handleChargingCircuitResult(AsyncModbusReadResult result) {
+ if (chargingCircuitRead != ReadStatus.READ_SUCCESS) {
+ // update status only if bit switches
+ chargingCircuitRead = ReadStatus.READ_SUCCESS;
+ updateStatus();
+ }
+ chargingCircuitParser.handle(result);
+ Optional dtoOpt = chargingCircuitParser.parse(Data.DataType.CHARGING_CIRCUIT);
+ if (dtoOpt.isPresent()) {
+ ChargingCircuitDTO chargingCircuitDTO = (ChargingCircuitDTO) dtoOpt.get();
+ updateState(flowTemperatureChannel, chargingCircuitDTO.flowTemperature);
+ updateState(returnFlowTemperatureChannel, chargingCircuitDTO.returnFlowTemperature);
+ updateState(flowSpeedChannel, chargingCircuitDTO.flowSpeed);
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.CHARGING_CIRCUIT,
+ chargingCircuitParser.toString());
+ }
+ }
+
+ private void handleChargingCircuitFailure(AsyncModbusFailure result) {
+ if (chargingCircuitRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ chargingCircuitRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ private void handleEnergySourceResult(AsyncModbusReadResult result) {
+ if (energySourceRead != ReadStatus.READ_SUCCESS) {
+ // update status only if bit switches
+ energySourceRead = ReadStatus.READ_SUCCESS;
+ updateStatus();
+ }
+ energySourceParser.handle(result);
+ Optional dtoOpt = energySourceParser.parse(Data.DataType.ENERGY_SOURCE);
+ if (dtoOpt.isPresent()) {
+ EnergySourceDTO energySourceDTO = (EnergySourceDTO) dtoOpt.get();
+ updateState(exitTemperatureChannel, energySourceDTO.exitTemperature);
+ updateState(incomingTemperatureChannel, energySourceDTO.incomingTemperature);
+ updateState(outsideTemperatureChannel, energySourceDTO.outsideTemperature);
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.ENERGY_SOURCE,
+ energySourceParser.toString());
+ }
+ }
+
+ private void handleEnergySourceFailure(AsyncModbusFailure result) {
+ if (energySourceRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ energySourceRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ private void handlePowerResult(AsyncModbusReadResult result) {
+ if (powerRead != ReadStatus.READ_SUCCESS) {
+ // update status only if bit switches
+ powerRead = ReadStatus.READ_SUCCESS;
+ updateStatus();
+ }
+ powerParser.handle(result);
+ Optional dtoOpt = powerParser.parse(Data.DataType.POWER);
+ if (dtoOpt.isPresent()) {
+ PowerDTO powerDTO = (PowerDTO) dtoOpt.get();
+
+ updateState(copChannel, powerDTO.cop);
+ updateState(copHeatingChannel, powerDTO.copHeating);
+ updateState(copDrinkingWaterChannel, powerDTO.copDrinkingwater);
+ updateState(copCoolingChannel, powerDTO.copCooling);
+
+ updateState(powerChannel, powerDTO.power);
+ updateState(powerHeatingChannel, powerDTO.powerHeating);
+ updateState(powerDrinkingWaterChannel, powerDTO.powerDrinkingwater);
+ updateState(powerCoolingChannel, powerDTO.powerCooling);
+
+ updateState(electricPowerChannel, powerDTO.electricPower);
+ updateState(electricPowerHeatingChannel, powerDTO.electricPowerHeating);
+ updateState(electricPowerDrinkingWaterChannel, powerDTO.electricPowerDrinkingwater);
+ updateState(electricPowerCoolingChannel, powerDTO.electricPowerCooling);
+
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.POWER, powerParser.toString());
+ }
+ }
+
+ private void handlePowerFailure(AsyncModbusFailure result) {
+ if (powerRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ powerRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ private void handlePvResult(AsyncModbusReadResult result) {
+ if (pvRead != ReadStatus.READ_SUCCESS) {
+ // update status only if bit switches
+ pvRead = ReadStatus.READ_SUCCESS;
+ updateStatus();
+ }
+ pvParser.handle(result);
+ Optional dtoOpt = pvParser.parse(Data.DataType.PV);
+ if (dtoOpt.isPresent()) {
+ PvDTO pvDTO = (PvDTO) dtoOpt.get();
+ updateState(pvStateChannel, pvDTO.pvModulationActive);
+ updateState(pvPowerChannel, pvDTO.pvModulationPower);
+ updateState(pvTargetTemperatureHeatingChannel, pvDTO.pvTargetTemperatureHeating);
+ updateState(pvTargetTemperatureDrinkingWaterChannel, pvDTO.pvTargetTemperatureDrinkingwater);
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.PV, pvParser.toString());
+ }
+ }
+
+ private void handlePvFailure(AsyncModbusFailure result) {
+ if (pvRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ pvRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ void handleStateResult(AsyncModbusReadResult result) {
+ if (stateRead != ReadStatus.READ_SUCCESS) {
+ // update status only if bit switches
+ stateRead = ReadStatus.READ_SUCCESS;
+ updateStatus();
+ }
+ stateParser.handle(result);
+ Optional dtoOpt = stateParser.parse(Data.DataType.STATE);
+ if (dtoOpt.isPresent()) {
+ StateDTO dto = (StateDTO) dtoOpt.get();
+ // updateState(globalStateChannel, dto.globalState);
+ updateState(globalStateIdChannel, dto.globalStateId);
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.STATE, stateParser.toString());
+ }
+ }
+
+ void handleStateFailure(AsyncModbusFailure result) {
+ if (stateRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ stateRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ void handleWorkHoursResult(AsyncModbusReadResult result) {
+ workHoursParser.handle(result);
+ Optional dtoOpt = workHoursParser.parse(Data.DataType.WORK_HOURS);
+ if (dtoOpt.isPresent()) {
+ WorkHoursDTO dto = (WorkHoursDTO) dtoOpt.get();
+ updateState(workHoursFanChannel, dto.workHoursFan);
+ updateState(workHoursStorageLoadingPumpChannel, dto.workHoursStorageLoadingPump);
+ updateState(workHoursCompressorChannel, dto.workHoursCompressor);
+ } else {
+ logger.debug("Unable to get {} from provider {}", Data.DataType.WORK_HOURS, workHoursParser.toString());
+ }
+ }
+
+ private void handleWorkHoursFailure(AsyncModbusFailure result) {
+ if (workHoursRead != ReadStatus.READ_FAILED) {
+ // update status only if bit switches
+ workHoursRead = ReadStatus.READ_FAILED;
+ logger.debug("Cause of failure: {}", result.getCause().getMessage());
+ updateStatus();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ ModbusCommunicationInterface localComms = comms;
+ if (localComms != null) {
+ for (PollTask p : pollTasks) {
+ PollTask localPoller = p;
+ if (localPoller != null) {
+ localComms.unregisterRegularPoll(localPoller);
+ }
+ }
+ }
+ // Comms will be close()'d by endpoint thing handler
+ comms = null;
+ }
+
+ private void updateStatus() {
+ logger.debug("Status update: State {} Data {} WorkHours {} PV {} ", stateRead, powerRead, workHoursRead,
+ pvRead);
+ if (stateRead != ReadStatus.NOT_RECEIVED) { // && dataRead != ReadStatus.NOT_RECEIVED
+ if (stateRead == ReadStatus.READ_SUCCESS) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ KermiBindingConstants.STATE_READ_ERROR);
+ }
+ if (stateRead == alarmRead) {
+ // both reads are ok or else both failed
+ if (stateRead == ReadStatus.READ_SUCCESS) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ KermiBindingConstants.STATE_AND_ALARM_READ_ERRORS);
+ }
+ } else {
+ // either info or data read failed - update status with details
+ if (stateRead == ReadStatus.READ_FAILED) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ KermiBindingConstants.STATE_READ_ERROR);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ KermiBindingConstants.DATA_READ_ERROR);
+ }
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/Data.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/Data.java
new file mode 100644
index 00000000000..152274d044a
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/Data.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.modbus;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Data} Marker Interface for delivered Modbus Data
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+@NonNullByDefault
+public interface Data {
+ enum DataType {
+ STATE,
+ ALARM_STATE,
+ ENERGY_SOURCE,
+ CHARGING_CIRCUIT,
+ POWER,
+ WORK_HOURS,
+ PV,
+ DATA // marks all types besides INFO/STATE
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/KermiModbusConstans.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/KermiModbusConstans.java
new file mode 100644
index 00000000000..cfa6375d23f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/KermiModbusConstans.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.modbus;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link KermiModbusConstans} Variables for register handling.
+ * The numbers are taken from Kermi ExcelChart for Modbus
+ * (Registers start from 0 (not 1!) so from the documented registers subtract 1)
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+@NonNullByDefault
+public class KermiModbusConstans {
+
+ public static final int WARMUP_TIME_LONG = 1500;
+ public static final int WARMUP_TIME_SHORT = 500;
+
+ // "String" registers at the beginning shall be read with very low frequency - 1 hour
+ public static final int SLOW_POLL_REFRESH_TIME_MS = 60 * 60 * 1000;
+
+ // poll every 30 seconds
+ public static final int STATE_POLL_REFRESH_TIME_MS = 30 * 1000;
+
+ // Constants where a certain Block starts and block size. Note: General offset is -1 so INFO_REG from E3DC Modbus
+ // Spec starts at 1 but it's Register 0!
+ public static final int XCENTER_DATA_REG_START = 1;
+ public static final int XCENTER_DATA_REG_SIZE = 111;
+
+ public static final int ENERGY_SOURCE_REG_START = 1;
+ public static final int ENERGY_SOURCE_REG_SIZE = 3;
+
+ public static final int CHARGING_CIRCUIT_REG_START = 50;
+ public static final int CHARGING_CIRCUIT_REG_SIZE = 3;
+
+ public static final int POWER_REG_START = 100;
+ public static final int POWER_REG_SIZE = 12;
+
+ /** INFO BLOCK **/
+ public static final int WORK_HOURS_REG_START = 150;
+ public static final int WORK_HOURS_REG_SIZE = 3;
+
+ public static final int STATE_REG_START = 200;
+ public static final int STATE_REG_SIZE = 1;
+
+ public static final int ALARM_REG_START = 250;
+ public static final int ALARM_REG_SIZE = 1;
+ /** END INFO BLOCK **/
+
+ /** PV BLOCK **/
+ public static final int PV_MODULATION_REG_START = 300;
+ public static final int PV_MODULATION_REG_SIZE = 4;
+
+ public static final int DATA_REGISTER_LENGTH = 111;
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/Parser.java b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/Parser.java
new file mode 100644
index 00000000000..d1b4b6a9d0e
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/java/org/openhab/binding/modbus/kermi/internal/modbus/Parser.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.modbus;
+
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.ALARM_REG_SIZE;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.CHARGING_CIRCUIT_REG_SIZE;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.DATA_REGISTER_LENGTH;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.ENERGY_SOURCE_REG_SIZE;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.POWER_REG_SIZE;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.PV_MODULATION_REG_SIZE;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.STATE_REG_SIZE;
+import static org.openhab.binding.modbus.kermi.internal.modbus.KermiModbusConstans.WORK_HOURS_REG_SIZE;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.modbus.kermi.internal.dto.AlarmDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.ChargingCircuitDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.EnergySourceDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.PowerDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.PvDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.StateDTO;
+import org.openhab.binding.modbus.kermi.internal.dto.WorkHoursDTO;
+import org.openhab.binding.modbus.kermi.internal.modbus.Data.DataType;
+import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
+import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Parser} class receives callbacks from modbus poller
+ *
+ * @author Bernd Weymann - Initial contribution
+ * @author Kai Neuhaus - adapted for Kermi
+ */
+@NonNullByDefault
+public class Parser {
+ private static final int MEASURE_COUNT = 100;
+ private final Logger logger = LoggerFactory.getLogger(Parser.class);
+ private DataType callbackType;
+ private byte[] bArray;
+ private int size;
+ private int counter = 0;
+ private long maxDuration = Long.MIN_VALUE;
+ private long minDuration = Long.MAX_VALUE;
+ private long avgDuration = 0;
+
+ public Parser(DataType type) {
+ callbackType = type;
+ if (type.equals(DataType.STATE)) {
+ size = (STATE_REG_SIZE) * 2;
+ bArray = new byte[size];
+ } else if (type.equals(DataType.ENERGY_SOURCE)) {
+ size = ENERGY_SOURCE_REG_SIZE * 2;
+ bArray = new byte[size];
+ } else if (type.equals(DataType.CHARGING_CIRCUIT)) {
+ size = CHARGING_CIRCUIT_REG_SIZE * 2;
+ bArray = new byte[size];
+ } else if (type.equals(DataType.POWER)) {
+ size = POWER_REG_SIZE * 2;
+ bArray = new byte[size];
+ } else if (type.equals(DataType.ALARM_STATE)) {
+ size = ALARM_REG_SIZE * 2;
+ bArray = new byte[size];
+ } else if (type.equals(DataType.PV)) {
+ size = PV_MODULATION_REG_SIZE * 2;
+ bArray = new byte[size];
+ } else if (type.equals(DataType.WORK_HOURS)) {
+ size = WORK_HOURS_REG_SIZE * 2;
+ bArray = new byte[size];
+ } else {
+ size = DATA_REGISTER_LENGTH * 2;
+ bArray = new byte[size];
+ }
+ }
+
+ public void handle(AsyncModbusReadResult result) {
+ long startTime = System.currentTimeMillis();
+ Optional opt = result.getRegisters();
+ if (opt.isPresent()) {
+ setArray(opt.get().getBytes());
+
+ long duration = System.currentTimeMillis() - startTime;
+ avgDuration += duration;
+ minDuration = Math.min(minDuration, duration);
+ maxDuration = Math.max(maxDuration, duration);
+ counter++;
+ if (counter % MEASURE_COUNT == 0) {
+ logger.debug("Min {} Max {} Avg {}", minDuration, maxDuration, avgDuration / MEASURE_COUNT);
+ avgDuration = 0;
+ minDuration = Long.MAX_VALUE;
+ maxDuration = Long.MIN_VALUE;
+ }
+ } else {
+ logger.warn("Modbus read result doesn't return expected registers");
+ }
+ }
+
+ public synchronized void setArray(byte[] b) {
+ if (b.length != size) {
+ logger.warn("Wrong byte size received. Should be {} but is {}. Data maybe corrupted!", size, b.length);
+ }
+ bArray = b.clone();
+ }
+
+ public Optional parse(DataType type) {
+ synchronized (bArray) {
+ if (type.equals(DataType.STATE) && callbackType.equals(DataType.STATE)) {
+ return Optional.of(new StateDTO(bArray));
+ } else if (type.equals(DataType.ENERGY_SOURCE) && callbackType.equals(DataType.ENERGY_SOURCE)) {
+ return Optional.of(new EnergySourceDTO(bArray));
+ } else if (type.equals(DataType.CHARGING_CIRCUIT) && callbackType.equals(DataType.CHARGING_CIRCUIT)) {
+ return Optional.of(new ChargingCircuitDTO(bArray));
+ } else if (type.equals(DataType.POWER) && callbackType.equals(DataType.POWER)) {
+ return Optional.of(new PowerDTO(bArray));
+ } else if (type.equals(DataType.PV) && callbackType.equals(DataType.PV)) {
+ return Optional.of(new PvDTO(bArray));
+ } else if (type.equals(DataType.ALARM_STATE) && callbackType.equals(DataType.ALARM_STATE)) {
+ return Optional.of(new AlarmDTO(bArray));
+ } else if (type.equals(DataType.WORK_HOURS) && callbackType.equals(DataType.WORK_HOURS)) {
+ return Optional.of(new WorkHoursDTO(bArray));
+ }
+ }
+ logger.warn("Wrong Block requested. Request is {} but type is {}", type, callbackType);
+ return Optional.empty();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getName()).append(":").append(callbackType);
+ return sb.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/i18n/kermi.properties b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/i18n/kermi.properties
new file mode 100644
index 00000000000..1b5666322b5
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/i18n/kermi.properties
@@ -0,0 +1,99 @@
+# thing types
+
+thing-type.modbus.kermi-xcenter.label = Kermi X-Center Heat Pump
+thing-type.modbus.kermi-xcenter.description = Provide Energy values, String Details and general Information of your Kermi Heat Pump
+
+# thing types config
+
+thing-type.config.modbus.kermi-xcenter.refresh.label = Refresh Interval
+thing-type.config.modbus.kermi-xcenter.refresh.description = Refresh Rate of values in milliseconds
+thing-type.config.modbus.kermi-xcenter.pvEnabled.label = Read PV-Modulation States
+thing-type.config.modbus.kermi-xcenter.pvEnabled.description = Enabled reading of PV-Modulation (disable to skip reading)
+
+# channel group types
+
+channel-group-type.modbus.xcenter-alarm-values.label = Alarm Values
+channel-group-type.modbus.xcenter-alarm-values.description = Alarm State of your Kermi Heatpump X-Center
+channel-group-type.modbus.xcenter-charging-circuit-values.label = Charging Circuit
+channel-group-type.modbus.xcenter-charging-circuit-values.description = Information about the charging circuit
+channel-group-type.modbus.xcenter-energy-source-values.label = Energy-Source
+channel-group-type.modbus.xcenter-energy-source-values.description = Information about the energy-source
+channel-group-type.modbus.xcenter-power-values.label = Power and efficency
+channel-group-type.modbus.xcenter-power-values.description = Information about power and efficiency
+channel-group-type.modbus.xcenter-pv-modulation-values.label = PV Modulation
+channel-group-type.modbus.xcenter-pv-modulation-values.description = PV Modulation Information and Settings for Kermi Heatpump X-Center
+channel-group-type.modbus.xcenter-state-values.label = Status
+channel-group-type.modbus.xcenter-state-values.description = Basic Information and State of your Kermi Heatpump X-Center
+channel-group-type.modbus.xcenter-workhours-values.label = Workhours
+channel-group-type.modbus.xcenter-workhours-values.description = Basic Information of workhours
+
+# channel types
+
+channel-type.modbus.alarm-state-channel.label = Alarm State
+channel-type.modbus.alarm-state-channel.description = Current alarm-state of your heat pump
+
+
+channel-type.modbus.flow-temperature-channel.label = Flow temperature
+channel-type.modbus.flow-temperature-channel.description = B16 - Vorlauftemperatur WP
+channel-type.modbus.return-temperature-channel.label = Return temperature
+channel-type.modbus.return-temperature-channel.description = B17 - Rücklauftemperatur WP
+channel-type.modbus.flow-speed-channel.label = Flow speed
+channel-type.modbus.flow-speed-channel.description = P13 - Durchfluss WP
+
+
+channel-type.modbus.exit-temperature-channel.label = Exit temperature
+channel-type.modbus.exit-temperature-channel.description = B14 - Energiequelle Austrittstemperatur
+channel-type.modbus.incoming-temperature-channel.label = Incoming temperature
+channel-type.modbus.incoming-temperature-channel.description = B15 - Energiequelle Eintrittstemperatur
+channel-type.modbus.temperature-sensor-outside-channel.label = Temperature Outside
+channel-type.modbus.temperature-sensor-outside-channel.description = BOT - Außentemperaturfühler
+
+
+channel-type.modbus.cop-channel.label = Current COP
+channel-type.modbus.cop-channel.description = Current COP of your installation
+channel-type.modbus.cop-heating-channel.label = Current COP heating
+channel-type.modbus.cop-heating-channel.description =
+channel-type.modbus.cop-drinkingwater-channel.label = Current COP drinking water
+channel-type.modbus.cop-drinkingwater-channel.description =
+channel-type.modbus.cop-cooling-channel.label = Current COP cooling
+channel-type.modbus.cop-cooling-channel.description =
+channel-type.modbus.power-channel.label = Current power
+channel-type.modbus.power-channel.description =
+channel-type.modbus.power-heating-channel.label = Current power heating
+channel-type.modbus.power-heating-channel.description =
+channel-type.modbus.power-drinkingwater-channel.label = Current power drinking water
+channel-type.modbus.power-drinkingwater-channel.description =
+channel-type.modbus.power-cooling-channel.label = Current power cooling
+channel-type.modbus.power-cooling-channel.description =
+channel-type.modbus.electric-power-channel.label = Current electric power
+channel-type.modbus.electric-power-channel.description =
+channel-type.modbus.electric-power-heating-channel.label = Current electric power heating
+channel-type.modbus.electric-power-heating-channel.description =
+channel-type.modbus.electric-power-drinkingwater-channel.label = Current electric power drinking water
+channel-type.modbus.electric-power-drinkingwater-channel.description =
+channel-type.modbus.electric-power-cooling-channel.label = Current electric power cooling
+channel-type.modbus.electric-power-cooling-channel.description =
+
+
+channel-type.modbus.pv-state-channel.label = PV Modulation Active
+channel-type.modbus.pv-state-channel.description = State of PV-Modulation
+channel-type.modbus.pv-power-channel.label = PV Power
+channel-type.modbus.pv-power-channel.description =
+channel-type.modbus.pv-target-temperature-heating-channel.label = PV Temp Heating
+channel-type.modbus.pv-target-temperature-heating-channel.description =
+channel-type.modbus.pv-target-temperature-drinkingwater-channel.label = PV Temp Drinkingwater
+channel-type.modbus.pv-target-temperature-drinkingwater-channel.description =
+
+
+channel-type.modbus.global-state-id-channel.label = Global State
+channel-type.modbus.global-state-id-channel.description = Current mode of your heat pump
+channel-type.modbus.global-alarm-channel.label = Global Alarm State
+channel-type.modbus.global-alarm-channel.description = If there's a global alarm
+
+
+channel-type.modbus.workhours-fan-channel.label = Fan Workhours
+channel-type.modbus.workhours-fan-channel.description = Work hours of the fan
+channel-type.modbus.workhours-storage-loading-pump-channel.label = StorageLoadingPump Workhours
+channel-type.modbus.workhours-storage-loading-pump-channel.description = Work hours of the storage loading pump
+channel-type.modbus.workhours-compressor-channel.label = Compressor Workhours
+channel-type.modbus.workhours-compressor-channel.description = Work hours of the compressor
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/bridge-kermi-xcenter.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/bridge-kermi-xcenter.xml
new file mode 100644
index 00000000000..29eacb6301a
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/bridge-kermi-xcenter.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Kermi Heat Pump X-Center
+ Provide Energy values, String Details and general Information of your Kermi Heat Pump
+
+
+
+
+
+
+
+
+
+
+
+ Refresh Interval
+ Refresh Rate of X-Center values in Milliseconds
+ 5000
+ true
+
+
+ Read PV-Modulation States
+ Enabled reading of PV-Modulation (disable to skip reading)
+ false
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-alarm-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-alarm-channel-groups.xml
new file mode 100644
index 00000000000..fc1b2fae727
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-alarm-channel-groups.xml
@@ -0,0 +1,13 @@
+
+
+
+ Alarm
+ Alarm State of your Kermi Heatpump X-Center
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-alarm-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-alarm-channel-types.xml
new file mode 100644
index 00000000000..9a3b19b18f6
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-alarm-channel-types.xml
@@ -0,0 +1,11 @@
+
+
+
+ Contact
+ Alarm State
+ Current alarm-state of your heat pump
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-charging-circuit-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-charging-circuit-channel-groups.xml
new file mode 100644
index 00000000000..ced2e283d04
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-charging-circuit-channel-groups.xml
@@ -0,0 +1,15 @@
+
+
+
+ Charging Circuit
+ Information about the charging circuit
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-charging-circuit-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-charging-circuit-channel-types.xml
new file mode 100644
index 00000000000..f325705d3d3
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-charging-circuit-channel-types.xml
@@ -0,0 +1,24 @@
+
+
+
+ Number:Temperature
+ Flow Temperature
+ B16 - Flow Temperature
+
+
+
+ Number:Temperature
+ Return Temperature
+ B17 - Return Temperature
+
+
+
+ Number:VolumetricFlowRate
+ Flow Speed
+ P13 - Flow speed
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-energy-source-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-energy-source-channel-groups.xml
new file mode 100644
index 00000000000..55f1d1f044f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-energy-source-channel-groups.xml
@@ -0,0 +1,15 @@
+
+
+
+ Energy-Source
+ Information about the energy-source
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-energy-source-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-energy-source-channel-types.xml
new file mode 100644
index 00000000000..2ceeba71ba0
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-energy-source-channel-types.xml
@@ -0,0 +1,24 @@
+
+
+
+ Number:Temperature
+ Exit Temperature
+ B14 - Exit Temperature Energy Source
+
+
+
+ Number:Temperature
+ Incoming temperature
+ B15 - Incoming Temperature Energy Source
+
+
+
+ Number:Temperature
+ Temperature Outside
+ BOT - Sensor Outside Temperature
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-power-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-power-channel-groups.xml
new file mode 100644
index 00000000000..24347babaef
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-power-channel-groups.xml
@@ -0,0 +1,24 @@
+
+
+
+ Power and Efficiency
+ Information about power and efficiency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-power-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-power-channel-types.xml
new file mode 100644
index 00000000000..04f65a65d47
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-power-channel-types.xml
@@ -0,0 +1,68 @@
+
+
+
+ Number
+ Current COP
+
+
+
+ Number
+ Current COP Heating
+
+
+
+ Number
+ Current COP drinking water
+
+
+
+ Number
+ Current COP Cooling
+
+
+
+
+ Number:Power
+ Current Power
+
+
+
+ Number:Power
+ Current power heating
+
+
+
+ Number:Power
+ Current Power Drinking Water
+
+
+
+ Number:Power
+ Current power cooling
+
+
+
+
+ Number:Power
+ Current Electric Power
+
+
+
+ Number:Power
+ Current Electric Power Heating
+
+
+
+ Number:Power
+ Current electric power drinking water
+
+
+
+ Number:Power
+ Current Electric Power Cooling
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-pv-modulation-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-pv-modulation-channel-groups.xml
new file mode 100644
index 00000000000..8bbb1c4ab9f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-pv-modulation-channel-groups.xml
@@ -0,0 +1,18 @@
+
+
+
+ PV Modulation
+ PV Modulation Information and Settings for Kermi Heatpump X-Center
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-pv-modulation-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-pv-modulation-channel-types.xml
new file mode 100644
index 00000000000..5791762cf29
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-pv-modulation-channel-types.xml
@@ -0,0 +1,26 @@
+
+
+
+ Switch
+ PV Modulation Active
+ State of PV-Modulation
+
+
+ Number:Power
+ PV Power
+
+
+
+ Number:Temperature
+ PV Temp Heating
+
+
+
+ Number:Temperature
+ PV Temp Drinkingwater
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-state-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-state-channel-groups.xml
new file mode 100644
index 00000000000..45d677ee1e8
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-state-channel-groups.xml
@@ -0,0 +1,13 @@
+
+
+
+ Status
+ Basic Information and State of your Kermi Heatpump X-Center
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-state-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-state-channel-types.xml
new file mode 100644
index 00000000000..9853a2341cb
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-state-channel-types.xml
@@ -0,0 +1,29 @@
+
+
+
+ Number
+ Global State
+ Current state of your heat pump
+
+
+ Standby
+ Alarm
+ DrinkingWater
+ Heating
+ Defrost
+ Preparing
+ Blocked
+ EVU Blocktime
+ Unavailable
+
+
+
+
+ Contact
+ Global Alarm State
+ If there's a global alarm
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-workhours-channel-groups.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-workhours-channel-groups.xml
new file mode 100644
index 00000000000..8adab2dfcbe
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-workhours-channel-groups.xml
@@ -0,0 +1,15 @@
+
+
+
+ Workhours
+ Basic Information of workhours
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-workhours-channel-types.xml b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-workhours-channel-types.xml
new file mode 100644
index 00000000000..884465eb991
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/main/resources/OH-INF/thing/xcenter-workhours-channel-types.xml
@@ -0,0 +1,24 @@
+
+
+
+ Number:Time
+ Fan Workhours
+ Work hours of the fan
+
+
+
+ Number:Time
+ StorageLoadingPump Workhours
+ Work hours of the storage loading pump
+
+
+
+ Number:Time
+ Compressor Workhours
+ Work hours of the compressor
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.kermi/src/test/java/org/openhab/binding/modbus/kermi/internal/dto/StateDTOTest.java b/bundles/org.openhab.binding.modbus.kermi/src/test/java/org/openhab/binding/modbus/kermi/internal/dto/StateDTOTest.java
new file mode 100644
index 00000000000..b8e3c93df2c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.kermi/src/test/java/org/openhab/binding/modbus/kermi/internal/dto/StateDTOTest.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2024 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.kermi.internal.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Test for {@link StateDTO} class
+ *
+ * @author Kai Neuhaus - Initial contribution
+ */
+public class StateDTOTest {
+
+ @BeforeEach
+ void setUp() {
+ }
+
+ @AfterEach
+ void tearDown() {
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index fe57819ae34..8820a5989e0 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -265,6 +265,7 @@
org.openhab.binding.minecraft
org.openhab.binding.modbus
org.openhab.binding.modbus.e3dc
+ org.openhab.binding.modbus.kermi
org.openhab.binding.modbus.sbc
org.openhab.binding.modbus.studer
org.openhab.binding.modbus.sungrow