diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 95eba8ac828c2..4c1495f4e8252 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -816,6 +816,11 @@
org.openhab.binding.keba
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.kermi
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.km200
diff --git a/bundles/org.openhab.binding.kermi/NOTICE b/bundles/org.openhab.binding.kermi/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.kermi/README.md b/bundles/org.openhab.binding.kermi/README.md
new file mode 100644
index 0000000000000..f90a80e34fd80
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/README.md
@@ -0,0 +1,78 @@
+# Kermi Binding
+
+This binding connects to [Kermi x-center controller](https://www.kermi.com/en/de/indoor-climate/products/heat-pumps-and-storage/x-center-controller/ "x-center-controller") for heat pumps.
+
+# Kermi Binding
+
+Current support is developed and tested on
+
+* a franchised version of the Kermi heatpump, namely the [Heizbösch MOZART13AC-RW60](https://www.boesch.at/produkte/heizen/waermepumpe/luft/modulierende-luft-wasser-waermepumpe-mozart-aussenaufstellung~495589) heatpump manager version _1.6.0.118_ .
+
+No official documentation could be found or gathered. This plug-in is based
+on reverse engineering the protocol.
+
+## Supported Things
+
+The x-center consists of a heat-pump manager that provides the communication bridge.
+
+- `bridge`: The communication bridge, provided by the heat pump manager
+- `drinkingwater-heating`: The storage module responsible for heating the drinking water (Default bus address = 51)
+- `room-heating`: The storage module responsible for heating rooms (Default bus address = 50)
+- `heatpump-manager`: As thing
+- `heatpump`: The heatpump element itself (Default bus address = 40)
+
+## Discovery
+
+There is no obvious way to get a listing of all Datapoints for a given Device. Due to this, on first
+connection to a site, the API for the UI User Interface is used, to iterate over all menu entries to
+collect the datapoints - which may take a while.
+
+The gathered data is then stored in `OH_USERDATA/binding.kermi` and loaded on subsequent binding lifecycle
+changes. The cache data is bound to the device-uuid and its serial number. If these values change,
+the datapoints are automatically re-initialized.
+
+## Binding Configuration
+
+The following samples are provided, representing the current state of my usage.
+
+```
+// kermi.things
+Bridge kermi:bridge:heatpumpbridge [hostname="xcenter-url",password="password",refreshInterval=60] {
+ Thing drinkingwater-heating dwheating [ address=51 ]
+ Thing room-heating rheating [ address=50 ]
+ Thing heatpump heatpump [ address=40 ]
+}
+
+```
+
+The plugin is configured to collect all `WellKnownName` datapoints, so generally
+all of them should be supported. At the moment I only test the following items in read-only mode.
+
+```
+// kermi.items
+Number:Temperature Drinking_water_temperature {channel="kermi:drinkingwater-heating:heatpumpbridge:dwheating:BufferSystem_TweTemperatureActual"}
+
+Number:Temperature Heating_current_temperature_buffer {channel="kermi:room-heating:heatpumpbridge:rheating:BufferSystem_HeatingTemperatureActual"}
+Number:Temperature Cooling_current_temperature_buffer {channel="kermi:room-heating:heatpumpbridge:rheating:BufferSystem_CoolingTemperatureActual"}
+
+Number:Temperature Outside_temperature {channel="kermi:room-heating:heatpumpbridge:rheating:LuftTemperatur"}
+
+Number:Power Current_Power_Inverter {channel="kermi:heatpump:heatpumpbridge:heatpump:Rubin_CurrentPowerInverter"}
+```
+
+# Changelog
+
+20.10.23
+* Support numeric values for datapointType = 0
+* Support string values for datapointType = 3
+* Support string values for datapointType = 4
+
+# ToDo / Future Tasks
+
+* Change default query time, resemble webinterface behaviour (every 10 seconds to GetFavorites)
+* Support channels for bridge
+* Somehow add DatapointConfigId to channel, seems not supported
+* Collection of statistics providing virtual channels
+ * 24/h power consumption (all, heating, drinking-water)
+ * number of cycles (all, heating, drinking-water)
+ * time between cycles
diff --git a/bundles/org.openhab.binding.kermi/pom.xml b/bundles/org.openhab.binding.kermi/pom.xml
new file mode 100644
index 0000000000000..88aa9ad841e04
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.2.0-SNAPSHOT
+
+
+ org.openhab.binding.kermi
+
+ openHAB Add-ons :: Bundles :: Kermi Binding
+
+
+
+ org.apache.commons
+ commons-collections4
+ 4.1
+ compile
+
+
+
+
diff --git a/bundles/org.openhab.binding.kermi/src/main/feature/feature.xml b/bundles/org.openhab.binding.kermi/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..a8b2ce368ff13
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.kermi/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBaseDeviceConfiguration.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBaseDeviceConfiguration.java
new file mode 100644
index 0000000000000..9e38d387f18a2
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBaseDeviceConfiguration.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class KermiBaseDeviceConfiguration {
+ public Integer address;
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBindingConstants.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBindingConstants.java
new file mode 100644
index 0000000000000..6869e33762acc
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBindingConstants.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal;
+
+import java.io.File;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * The {@link KermiBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Marco Descher - Initial contribution
+ */
+@NonNullByDefault
+public class KermiBindingConstants {
+
+ private static final String BINDING_ID = "kermi";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
+ public static final ThingTypeUID THING_TYPE_DRINKINGWATER_HEATING = new ThingTypeUID(BINDING_ID,
+ "drinkingwater-heating");
+ public static final ThingTypeUID THING_TYPE_ROOM_HEATING = new ThingTypeUID(BINDING_ID, "room-heating");
+ public static final ThingTypeUID THING_TYPE_HEATPUMP = new ThingTypeUID(BINDING_ID, "heatpump");
+ public static final ThingTypeUID THING_TYPE_HEATPUMP_MANAGER = new ThingTypeUID(BINDING_ID, "heatpump-manager");
+
+ public static final ChannelTypeUID CHANNEL_TYPE_TEMPERATURE = new ChannelTypeUID(BINDING_ID, "temperature");
+ public static final ChannelTypeUID CHANNEL_TYPE_POWER = new ChannelTypeUID(BINDING_ID, "power");
+ public static final ChannelTypeUID CHANNEL_TYPE_NUMBER = new ChannelTypeUID(BINDING_ID, "number");
+ public static final ChannelTypeUID CHANNEL_TYPE_ONOFF = new ChannelTypeUID(BINDING_ID, "onoff");
+ public static final ChannelTypeUID CHANNEL_TYPE_STRING = new ChannelTypeUID(BINDING_ID, "string");
+
+ // Device Constants
+ public static final String DEVICE_ID_HEATPUMP_MANAGER = "00000000-0000-0000-0000-000000000000";
+ public static final int DEVICE_TYPE_HEATING_SYSTEM = 95;
+ public static final int DEVICE_TYPE_HEATPUMP = 97;
+ public static final int DEVICE_TYPE_HEATPUMP_MANAGER = 0;
+
+ public static final String WELL_KNOWN_NAME_BS_TWE_TEMP_ACT = "BufferSystem_TweTemperatureActual";
+ public static final String WELL_KNOWN_NAME_FS_COOL_TEMP_ACT = "BufferSystem_CoolingTemperatureActual";
+ public static final String WELL_KNOWN_NAME_FS_HEAT_TEMP_ACT = "BufferSystem_HeatingTemperatureActual";
+ public static final String WELL_KNOWN_NAME_COMB_HEATPUMP_STATE = "Rubin_CombinedHeatpumpState";
+ public static final String WELL_KNOWN_NAME_COMB_HEATPUMP_CURR_COP = "Rubin_CurrentCOP";
+ public static final String WELL_KNOWN_NAME_CURR_OUT_CAP = "Rubin_CurrentOutputCapacity";
+ public static final String WELL_KNOWN_NAME_HEAT_AIR_TEMPERATURE = "LuftTemperatur";
+
+ // All Urls
+ public static final String HPM_DEVICE_GETDEVICESBYFILTER_URL = "http://%IP%/api/Device/GetDevicesByFilter/00000000-0000-0000-0000-000000000000";
+ public static final String HPM_DEVICE_GETDEVICE_URL = "http://%IP%/api/Device/GetDevice/00000000-0000-0000-0000-000000000000";
+ public static final String HPM_DEVICE_GETALLDEVICES_URL = "http://%IP%/api/Device/GetAllDevices/00000000-0000-0000-0000-000000000000";
+ public static final String HPM_MENU_GETCHILDENTRIES_URL = "http://%IP%/api/Menu/GetChildEntries/00000000-0000-0000-0000-000000000000";
+ public static final String HPM_DATAPOINT_READVALUES_URL = "http://%IP%/api/Datapoint/ReadValues/00000000-0000-0000-0000-000000000000";
+
+ public static String parseUrl(String url, String ip) {
+ return url.replace("%IP%", ip.trim());
+ }
+
+ public static File getKermiUserDataFolder() {
+ File kermiUserDataFolder = new File(OpenHAB.getUserDataFolder(), "binding.kermi");
+ kermiUserDataFolder.mkdir();
+ return kermiUserDataFolder;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBridgeConfiguration.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBridgeConfiguration.java
new file mode 100644
index 0000000000000..9cdfe2921aac2
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiBridgeConfiguration.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class KermiBridgeConfiguration {
+ public String hostname;
+ public String password;
+ public Integer refreshInterval;
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiCommunicationException.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiCommunicationException.java
new file mode 100644
index 0000000000000..b081fc8dca91d
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiCommunicationException.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Exception for unexpected response from or communication failure with the Kermi controller.
+ *
+ * @author Marco Descher - Initial contribution
+ */
+@NonNullByDefault
+public class KermiCommunicationException extends IOException {
+ private static final long serialVersionUID = 619020705591964155L;
+
+ public KermiCommunicationException(String message) {
+ super(message);
+ }
+
+ public KermiCommunicationException(Throwable ex) {
+ super(ex);
+ }
+
+ public KermiCommunicationException(String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiThingHandlerFactory.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiThingHandlerFactory.java
new file mode 100644
index 0000000000000..32466ef348b4d
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/KermiThingHandlerFactory.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal;
+
+import static org.openhab.binding.kermi.internal.KermiBindingConstants.*;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.kermi.internal.api.KermiHttpUtil;
+import org.openhab.binding.kermi.internal.handler.KermiBaseThingHandler;
+import org.openhab.binding.kermi.internal.handler.KermiBridgeHandler;
+import org.openhab.binding.kermi.internal.model.KermiSiteInfo;
+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 KermiThingHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Marco Descher - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.kermi", service = ThingHandlerFactory.class)
+public class KermiThingHandlerFactory extends BaseThingHandlerFactory {
+
+ private KermiHttpUtil httpUtil;
+ private KermiSiteInfo kermiSiteInfo;
+
+ public KermiThingHandlerFactory() {
+ httpUtil = new KermiHttpUtil();
+ kermiSiteInfo = new KermiSiteInfo();
+ }
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet() {
+
+ private static final long serialVersionUID = 1L;
+ {
+ add(THING_TYPE_BRIDGE);
+ add(THING_TYPE_HEATPUMP_MANAGER);
+ add(THING_TYPE_HEATPUMP);
+ add(THING_TYPE_DRINKINGWATER_HEATING);
+ add(THING_TYPE_ROOM_HEATING);
+ }
+ };
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (thingTypeUID.equals(THING_TYPE_BRIDGE)) {
+ return new KermiBridgeHandler((Bridge) thing, httpUtil, kermiSiteInfo);
+ } else if (thingTypeUID.equals(THING_TYPE_HEATPUMP_MANAGER)) {
+ return new KermiBaseThingHandler(thing, httpUtil, kermiSiteInfo);
+ } else if (thingTypeUID.equals(THING_TYPE_DRINKINGWATER_HEATING)) {
+ return new KermiBaseThingHandler(thing, httpUtil, kermiSiteInfo);
+ } else if (thingTypeUID.equals(THING_TYPE_HEATPUMP)) {
+ return new KermiBaseThingHandler(thing, httpUtil, kermiSiteInfo);
+ } else if (thingTypeUID.equals(THING_TYPE_ROOM_HEATING)) {
+ return new KermiBaseThingHandler(thing, httpUtil, kermiSiteInfo);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/BaseResponse.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/BaseResponse.java
new file mode 100644
index 0000000000000..ebe91f3daabf5
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/BaseResponse.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class BaseResponse {
+
+ @SerializedName("ResponseData")
+ private T responseData;
+
+ @SerializedName("StatusCode")
+ private int statusCode;
+
+ @SerializedName("ExceptionData")
+ private Object exceptionData;
+
+ @SerializedName("DisplayText")
+ private String displayText;
+
+ @SerializedName("DetailedText")
+ private String detailedText;
+
+ public T getResponseData() {
+ return responseData;
+ }
+
+ public void setResponseData(T responseData) {
+ this.responseData = responseData;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public void setStatusCode(int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ public Object getExceptionData() {
+ return exceptionData;
+ }
+
+ public void setExceptionData(Object exceptionData) {
+ this.exceptionData = exceptionData;
+ }
+
+ public String getDisplayText() {
+ return displayText;
+ }
+
+ public void setDisplayText(String displayText) {
+ this.displayText = displayText;
+ }
+
+ public String getDetailedText() {
+ return detailedText;
+ }
+
+ public void setDetailedText(String detailedText) {
+ this.detailedText = detailedText;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Bundle.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Bundle.java
new file mode 100644
index 0000000000000..ee727d55c5246
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Bundle.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class Bundle {
+
+ @SerializedName("DatapointBundleId")
+ private String datapointBundleId;
+
+ @SerializedName("Datapoints")
+ private List datapoints;
+
+ @SerializedName("DisplayName")
+ private String displayName;
+
+ public String getDatapointBundleId() {
+ return datapointBundleId;
+ }
+
+ public void setDatapointBundleId(String datapointBundleId) {
+ this.datapointBundleId = datapointBundleId;
+ }
+
+ public List getDatapoints() {
+ return datapoints;
+ }
+
+ public void setDatapoints(List datapoints) {
+ this.datapoints = datapoints;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Config.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Config.java
new file mode 100644
index 0000000000000..c58efd1f4cdf0
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Config.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import java.util.Map;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class Config {
+
+ @SerializedName("DatapointConfigId")
+ private String datapointConfigId;
+
+ @SerializedName("DisplayName")
+ private String displayName;
+
+ @SerializedName("Description")
+ private String description;
+
+ @SerializedName("WellKnownName")
+ private String wellKnownName;
+
+ @SerializedName("Unit")
+ private String unit;
+
+ @SerializedName("DatapointType")
+ private int datapointType;
+
+ @SerializedName("PossibleValues")
+ private Map possibleValues;
+
+ public String getDatapointConfigId() {
+ return datapointConfigId;
+ }
+
+ public void setDatapointConfigId(String datapointConfigId) {
+ this.datapointConfigId = datapointConfigId;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getWellKnownName() {
+ return wellKnownName;
+ }
+
+ public void setWellKnownName(String wellKnownName) {
+ this.wellKnownName = wellKnownName;
+ }
+
+ public String getUnit() {
+ return unit;
+ }
+
+ public void setUnit(String unit) {
+ this.unit = unit;
+ }
+
+ public int getDatapointType() {
+ return datapointType;
+ }
+
+ public void setDatapointType(int datapointType) {
+ this.datapointType = datapointType;
+ }
+
+ public Map getPossibleValues() {
+ return possibleValues;
+ }
+
+ public void setPossibleValues(Map possibleValues) {
+ this.possibleValues = possibleValues;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Datapoint.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Datapoint.java
new file mode 100644
index 0000000000000..c265f486eb10c
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Datapoint.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class Datapoint {
+
+ @SerializedName("Config")
+ private Config config;
+
+ @SerializedName("DatapointValue")
+ private DatapointValue datapointValue;
+
+ public Config getConfig() {
+ return config;
+ }
+
+ public void setConfig(Config config) {
+ this.config = config;
+ }
+
+ public DatapointValue getDatapointValue() {
+ return datapointValue;
+ }
+
+ public void setDatapointValue(DatapointValue datapointValue) {
+ this.datapointValue = datapointValue;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DatapointReadValuesResponse.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DatapointReadValuesResponse.java
new file mode 100644
index 0000000000000..412306d99c38b
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DatapointReadValuesResponse.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import java.util.List;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class DatapointReadValuesResponse extends BaseResponse> {
+
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DatapointValue.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DatapointValue.java
new file mode 100644
index 0000000000000..3ebb2a473db78
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DatapointValue.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class DatapointValue {
+
+ @SerializedName("Value")
+ private Object value;
+
+ @SerializedName("DatapointConfigId")
+ private String datapointConfigId;
+
+ @SerializedName("DeviceId")
+ private String deviceId;
+
+ @SerializedName("Flags")
+ private int flags;
+
+ public Object getValue() {
+ return value;
+ }
+
+ public void setValue(Object value) {
+ this.value = value;
+ }
+
+ public String getDatapointConfigId() {
+ return datapointConfigId;
+ }
+
+ public void setDatapointConfigId(String datapointConfigId) {
+ this.datapointConfigId = datapointConfigId;
+ }
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public int getFlags() {
+ return flags;
+ }
+
+ public void setFlags(int flags) {
+ this.flags = flags;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Device.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Device.java
new file mode 100644
index 0000000000000..8cdf6a0259289
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/Device.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class Device {
+
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DeviceInfo.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DeviceInfo.java
new file mode 100644
index 0000000000000..d0e971fd1302d
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DeviceInfo.java
@@ -0,0 +1,177 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class DeviceInfo {
+
+ @SerializedName("DeviceId")
+ private String deviceId;
+
+ @SerializedName("Protocol")
+ private int protocol;
+
+ @SerializedName("PortAddress")
+ private String portAddress;
+
+ @SerializedName("SoftwareVersion")
+ private String softwareVersion;
+
+ @SerializedName("Address")
+ private String address;
+
+ @SerializedName("HomeServerSenderAddress")
+ private String homeServerAddress;
+
+ @SerializedName("Name")
+ private String name;
+
+ @SerializedName("Description")
+ private String description;
+
+ @SerializedName("Serial")
+ private String serial;
+
+ @SerializedName("ParentMenuEntryId")
+ private String parentMenuEntryId;
+
+ @SerializedName("ParentDeviceId")
+ private String parentDeviceId;
+
+ @SerializedName("DeviceType")
+ private String deviceType;
+
+ @SerializedName("DeviceOptions")
+ private List deviceOptions;
+
+ @SerializedName("VisualizationDatapoints")
+ private List visualizationDatapoints;
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public int getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(int protocol) {
+ this.protocol = protocol;
+ }
+
+ public String getPortAddress() {
+ return portAddress;
+ }
+
+ public void setPortAddress(String portAddress) {
+ this.portAddress = portAddress;
+ }
+
+ public String getSoftwareVersion() {
+ return softwareVersion;
+ }
+
+ public void setSoftwareVersion(String softwareVersion) {
+ this.softwareVersion = softwareVersion;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getHomeServerAddress() {
+ return homeServerAddress;
+ }
+
+ public void setHomeServerAddress(String homeServerAddress) {
+ this.homeServerAddress = homeServerAddress;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getSerial() {
+ return serial;
+ }
+
+ public void setSerial(String serial) {
+ this.serial = serial;
+ }
+
+ public String getParentMenuEntryId() {
+ return parentMenuEntryId;
+ }
+
+ public void setParentMenuEntryId(String parentMenuEntryId) {
+ this.parentMenuEntryId = parentMenuEntryId;
+ }
+
+ public String getParentDeviceId() {
+ return parentDeviceId;
+ }
+
+ public void setParentDeviceId(String parentDeviceId) {
+ this.parentDeviceId = parentDeviceId;
+ }
+
+ public String getDeviceType() {
+ return deviceType;
+ }
+
+ public void setDeviceType(String deviceType) {
+ this.deviceType = deviceType;
+ }
+
+ public List getDeviceOptions() {
+ return deviceOptions;
+ }
+
+ public void setDeviceOptions(List deviceOptions) {
+ this.deviceOptions = deviceOptions;
+ }
+
+ public List getVisualizationDatapoints() {
+ return visualizationDatapoints;
+ }
+
+ public void setVisualizationDatapoints(List visualizationDatapoints) {
+ this.visualizationDatapoints = visualizationDatapoints;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DeviceOption.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DeviceOption.java
new file mode 100644
index 0000000000000..11161bde971da
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/DeviceOption.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class DeviceOption {
+
+ @SerializedName("OptionId")
+ private String optionId;
+
+ @SerializedName("Name")
+ private String name;
+
+ @SerializedName("IsActivated")
+ private boolean isActivated;
+
+ public String getOptionId() {
+ return optionId;
+ }
+
+ public void setOptionId(String optionId) {
+ this.optionId = optionId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isActivated() {
+ return isActivated;
+ }
+
+ public void setActivated(boolean isActivated) {
+ this.isActivated = isActivated;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/GetDeviceResponse.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/GetDeviceResponse.java
new file mode 100644
index 0000000000000..22821d9d59795
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/GetDeviceResponse.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class GetDeviceResponse extends BaseResponse {
+
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/GetDevicesResponse.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/GetDevicesResponse.java
new file mode 100644
index 0000000000000..deb070dfe6cb3
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/GetDevicesResponse.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import java.util.List;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class GetDevicesResponse extends BaseResponse> {
+
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/KermiHttpUtil.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/KermiHttpUtil.java
new file mode 100644
index 0000000000000..85de779690a8e
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/KermiHttpUtil.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import static org.openhab.binding.kermi.internal.KermiBindingConstants.parseUrl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jetty.client.HttpResponseException;
+import org.openhab.binding.kermi.internal.KermiBindingConstants;
+import org.openhab.binding.kermi.internal.KermiCommunicationException;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import io.micrometer.core.instrument.util.StringUtils;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class KermiHttpUtil {
+
+ private static final String CONTENT_TYPE = "application/json; charset=utf-8";
+
+ private final Logger logger = LoggerFactory.getLogger(KermiHttpUtil.class);
+
+ private String hostname = "";
+ private String password = "";
+ private Properties httpHeaders;
+ private Gson gson;
+
+ public KermiHttpUtil() {
+ httpHeaders = new Properties();
+ gson = new Gson();
+ }
+
+ private String getBaseApiUrl() {
+ return "http://" + hostname + "/api/";
+ }
+
+ public void setHostname(String hostname) {
+ this.hostname = hostname;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ /**
+ * Issue a HTTP GET request and retry on failure
+ *
+ * @param url the url to execute
+ * @param timeout the socket timeout in milliseconds to wait for data
+ * @return the response body
+ * @throws KermiCommunicationException when the request execution failed or interrupted
+ */
+ @SuppressWarnings("null")
+ public synchronized String executeUrl(String httpMethod, String url, String content, String contentType)
+ throws KermiCommunicationException {
+ if (StringUtils.isBlank(hostname)) {
+ return "Not connected";
+ }
+
+ int attemptCount = 1;
+ try {
+ while (true) {
+ Throwable lastException = null;
+ String result = null;
+ try {
+ InputStream inputStream = (content != null)
+ ? new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))
+ : null;
+ result = HttpUtil.executeUrl(httpMethod, url, httpHeaders, inputStream, contentType, 5000);
+ logger.debug("[{} {}] {}", httpMethod, url, result);
+ } catch (IOException e) {
+ // HttpUtil::executeUrl wraps InterruptedException into IOException.
+ // Unwrap and rethrow it so that we don't retry on InterruptedException
+ if (e.getCause() instanceof InterruptedException) {
+ throw (InterruptedException) e.getCause();
+ }
+
+ if (e.getCause() instanceof ExecutionException) {
+ ExecutionException iex = (ExecutionException) e.getCause();
+ if (iex != null && iex.getCause() instanceof HttpResponseException) {
+ HttpResponseException hre = (HttpResponseException) iex.getCause();
+ if (401 == hre.getResponse().getStatus()) {
+ logger.debug("Perform login");
+ attemptCount = 0;
+ performLogin();
+ }
+ }
+ }
+ lastException = e;
+ }
+
+ if (result != null) {
+ if (attemptCount > 1) {
+ logger.debug("Attempt #{} successful {}", attemptCount, url);
+ }
+ return result;
+ }
+
+ if (attemptCount >= 3) {
+ logger.debug("Failed connecting to {} after {} attempts.", url, attemptCount, lastException);
+ }
+
+ logger.debug("HTTP error on attempt #{} {}", attemptCount, url);
+ Thread.sleep(500 * attemptCount);
+ attemptCount++;
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new KermiCommunicationException("Interrupted", e);
+ }
+ }
+
+ private void performLogin() throws KermiCommunicationException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("Password", password);
+ executeUrl("POST", getBaseApiUrl() + "Security/Login", jsonObject.toString(), CONTENT_TYPE);
+ }
+
+ public GetDevicesResponse getAllDevices() throws KermiCommunicationException {
+ String response = executeUrl("GET", parseUrl(KermiBindingConstants.HPM_DEVICE_GETALLDEVICES_URL, hostname),
+ null, null);
+ return gson.fromJson(response, GetDevicesResponse.class);
+ }
+
+ public MenuGetChildEntriesResponse getMenuChildEntries(String deviceId, @NonNull String parentMenuEntryId)
+ throws KermiCommunicationException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("DeviceId", deviceId);
+ jsonObject.addProperty("ParentMenuEntryId", parentMenuEntryId);
+ jsonObject.addProperty("WithDetails", Boolean.TRUE);
+
+ String response = executeUrl("POST", parseUrl(KermiBindingConstants.HPM_MENU_GETCHILDENTRIES_URL, hostname),
+ jsonObject.toString(), CONTENT_TYPE);
+ return gson.fromJson(response, MenuGetChildEntriesResponse.class);
+ }
+
+ /**
+ * Fetch update datapoint values for the given idTuples
+ *
+ * @param idTuples with [0] being the DeviceId, and [1] being the DatapointConfigId
+ * @return
+ * @throws KermiCommunicationException
+ */
+ public DatapointReadValuesResponse getDatapointReadValues(Set idTuples)
+ throws KermiCommunicationException {
+ JsonObject jsonObject = new JsonObject();
+ JsonArray datapointValues = new JsonArray(idTuples.size());
+ jsonObject.add("DatapointValues", datapointValues);
+ idTuples.forEach(idt -> {
+ JsonObject entryObject = new JsonObject();
+ entryObject.addProperty("DeviceId", idt[0]);
+ entryObject.addProperty("DatapointConfigId", idt[1]);
+ datapointValues.add(entryObject);
+ });
+
+ String response = executeUrl("POST", parseUrl(KermiBindingConstants.HPM_DATAPOINT_READVALUES_URL, hostname),
+ jsonObject.toString(), CONTENT_TYPE);
+ return gson.fromJson(response, DatapointReadValuesResponse.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuEntry.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuEntry.java
new file mode 100644
index 0000000000000..8d9f7bf6c2ab0
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuEntry.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class MenuEntry {
+
+ @SerializedName("MenuEntryId")
+ private String menuEntryId;
+
+ @SerializedName("ParentMenuEntryId")
+ private String parentMenuEntryId;
+
+ public String getMenuEntryId() {
+ return menuEntryId;
+ }
+
+ public void setMenuEntryId(String menuEntryId) {
+ this.menuEntryId = menuEntryId;
+ }
+
+ public String getParentMenuEntryId() {
+ return parentMenuEntryId;
+ }
+
+ public void setParentMenuEntryId(String parentMenuEntryId) {
+ this.parentMenuEntryId = parentMenuEntryId;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuEntryResponse.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuEntryResponse.java
new file mode 100644
index 0000000000000..853d48f8bf212
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuEntryResponse.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class MenuEntryResponse {
+
+ @SerializedName("DeviceId")
+ private String deviceId;
+
+ @SerializedName("ParentMenuEntryId")
+ private String parentMenuEntryId;
+
+ @SerializedName("MenuEntries")
+ private List menuEntries;
+
+ @SerializedName("Bundles")
+ private List bundles;
+
+ @SerializedName("Devices")
+ private List devices;
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public String getParentMenuEntryId() {
+ return parentMenuEntryId;
+ }
+
+ public void setParentMenuEntryId(String parentMenuEntryId) {
+ this.parentMenuEntryId = parentMenuEntryId;
+ }
+
+ public List getMenuEntries() {
+ return menuEntries;
+ }
+
+ public void setMenuEntries(List menuEntries) {
+ this.menuEntries = menuEntries;
+ }
+
+ public List getBundles() {
+ return bundles;
+ }
+
+ public void setBundles(List bundles) {
+ this.bundles = bundles;
+ }
+
+ public List getDevices() {
+ return devices;
+ }
+
+ public void setDevices(List devices) {
+ this.devices = devices;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuGetChildEntriesResponse.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuGetChildEntriesResponse.java
new file mode 100644
index 0000000000000..3dc0c82def19f
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/MenuGetChildEntriesResponse.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+@NonNullByDefault
+public class MenuGetChildEntriesResponse extends BaseResponse {
+
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/VisualizationDatapoint.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/VisualizationDatapoint.java
new file mode 100644
index 0000000000000..ea660e8a2abb5
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/api/VisualizationDatapoint.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.api;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class VisualizationDatapoint {
+
+ @SerializedName("Config")
+ private Config config;
+
+ @SerializedName("DatapointValue")
+ private DatapointValue datapointValue;
+
+ public Config getConfig() {
+ return config;
+ }
+
+ public void setConfig(Config config) {
+ this.config = config;
+ }
+
+ public DatapointValue getDatapointValue() {
+ return datapointValue;
+ }
+
+ public void setDatapointValue(DatapointValue datapointValue) {
+ this.datapointValue = datapointValue;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBaseThingHandler.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBaseThingHandler.java
new file mode 100644
index 0000000000000..d5da6c13187cd
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBaseThingHandler.java
@@ -0,0 +1,244 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.handler;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.openhab.binding.kermi.internal.KermiBaseDeviceConfiguration;
+import org.openhab.binding.kermi.internal.KermiBindingConstants;
+import org.openhab.binding.kermi.internal.KermiCommunicationException;
+import org.openhab.binding.kermi.internal.api.Config;
+import org.openhab.binding.kermi.internal.api.Datapoint;
+import org.openhab.binding.kermi.internal.api.DeviceInfo;
+import org.openhab.binding.kermi.internal.api.KermiHttpUtil;
+import org.openhab.binding.kermi.internal.model.KermiSiteInfo;
+import org.openhab.binding.kermi.internal.model.KermiSiteInfoUtil;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+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.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class KermiBaseThingHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(KermiBaseThingHandler.class);
+ private final KermiHttpUtil httpUtil;
+ private final KermiSiteInfo kermiSiteInfo;
+ private final KermiBaseThingHandlerUtil kermiBaseThingHandlerUtil;
+ private String busAddress;
+ private String deviceId;
+
+ public KermiBaseThingHandler(Thing thing, KermiHttpUtil httpUtil, KermiSiteInfo kermiSiteInfo) {
+ super(thing);
+ this.httpUtil = httpUtil;
+ this.kermiSiteInfo = kermiSiteInfo;
+ this.kermiBaseThingHandlerUtil = new KermiBaseThingHandlerUtil();
+ }
+
+ public KermiHttpUtil getHttpUtil() {
+ return httpUtil;
+ }
+
+ public KermiSiteInfo getKermiSiteInfo() {
+ return kermiSiteInfo;
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ kermiSiteInfo.putRefreshBinding(channelUID.getId(), deviceId);
+ logger.trace("Thing {} linked channel {}", getThing().getUID(), channelUID);
+ super.channelLinked(channelUID);
+ }
+
+ @Override
+ public void channelUnlinked(ChannelUID channelUID) {
+ kermiSiteInfo.removeRefreshBinding(channelUID.getId(), deviceId);
+ logger.trace("Thing {} unlinked channel {}", getThing().getUID(), channelUID);
+ super.channelUnlinked(channelUID);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateChannel(channelUID.getId());
+ }
+ }
+
+ @Override
+ public void initialize() {
+ if (KermiBindingConstants.THING_TYPE_HEATPUMP_MANAGER.equals(getThing().getThingTypeUID())) {
+ deviceId = KermiBindingConstants.DEVICE_ID_HEATPUMP_MANAGER;
+ logger.debug("Initializing heatpump-manager with deviceId {}", deviceId);
+ } else {
+ KermiBaseDeviceConfiguration config = getConfigAs(KermiBaseDeviceConfiguration.class);
+ busAddress = config.address.toString();
+ deviceId = kermiSiteInfo.getDeviceInfoByAddress(busAddress).getDeviceId();
+ logger.debug("Initializing busAddress {} with deviceId {}", busAddress, deviceId);
+ }
+
+ Bridge bridge = getBridge();
+ if (bridge == null || bridge.getHandler() == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
+ } else if (bridge.getStatus() == ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.UNKNOWN);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+
+ // determine channels for thing
+ // get device info by address
+ try {
+ ThingBuilder thingBuilder = editThing();
+ DeviceInfo deviceInfo = kermiSiteInfo.getDeviceInfoByAddress(busAddress);
+ thingBuilder.withLabel(deviceInfo.getName());
+
+ List deviceDatapoints = KermiSiteInfoUtil.collectDeviceDatapoints(httpUtil, deviceInfo);
+ deviceDatapoints.forEach(datapoint -> addDatapointAsChannel(getThing().getUID(), datapoint, thingBuilder));
+ updateThing(thingBuilder.build());
+ } catch (KermiCommunicationException e) {
+ // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ logger.warn("Communication exception", e);
+ }
+
+ getThing().getChannels().forEach(channel -> {
+ if (isLinked(channel.getUID())) {
+ channelLinked(channel.getUID());
+ }
+ });
+ }
+
+ private void addDatapointAsChannel(ThingUID thingUID, Datapoint datapoint, ThingBuilder thingBuilder) {
+ Config datapointConfig = datapoint.getConfig();
+ ChannelTypeUID channelTypeUID = kermiBaseThingHandlerUtil.determineChannelTypeUID(datapointConfig);
+ if (channelTypeUID != null) {
+ if (StringUtils.isNotBlank(datapointConfig.getWellKnownName())) {
+ ChannelUID channelUID = new ChannelUID(getThing().getUID(), datapointConfig.getWellKnownName());
+
+ Channel channel = ChannelBuilder.create(channelUID).withType(channelTypeUID).withLabel(busAddress)
+ .withLabel(datapointConfig.getDisplayName()).withDescription(datapointConfig.getDescription())
+ .build();
+ thingBuilder.withChannel(channel);
+ logger.debug("{} added channel {}", thingUID, datapointConfig.getWellKnownName());
+ }
+ } else {
+ logger.info("{} unsupported channel-type for datapointConfigId {}", thingUID,
+ datapointConfig.getDatapointConfigId());
+ }
+ }
+
+ public String getBusAddress() {
+ return busAddress;
+ }
+
+ /**
+ * Update linked channels
+ */
+ protected void updateChannels() {
+ for (Channel channel : getThing().getChannels()) {
+ if (isLinked(channel.getUID())) {
+ updateChannel(channel.getUID().getId());
+ }
+ }
+ }
+
+ public void updateProperties(DeviceInfo deviceInfo) {
+ if (deviceInfo == null) {
+ return;
+ }
+
+ Map<@NonNull String, @NonNull String> properties = editProperties();
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceInfo.getSerial());
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceInfo.getSoftwareVersion());
+ properties.put("DeviceType", deviceInfo.getDeviceType());
+ properties.put("DeviceId", deviceInfo.getDeviceId());
+ updateProperties(properties);
+ }
+
+ /**
+ * Update the channel from the last data
+ *
+ * @param channelId the id identifying the channel to be updated
+ */
+ protected void updateChannel(String channelId) {
+ if (!isLinked(channelId)) {
+ return;
+ }
+
+ State state = getValue(channelId);
+ if (state == null) {
+ state = UnDefType.NULL;
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("Update channel {} with state {} ({})", channelId, state.toString(),
+ state.getClass().getSimpleName());
+ }
+ updateState(channelId, state);
+ }
+
+ protected State getValue(String channelId) {
+ final String[] fields = channelId.split("#");
+ if (fields.length < 1) {
+ return null;
+ }
+
+ final String fieldName = fields[0];
+ return getKermiSiteInfo().getStateByWellKnownName(fieldName, deviceId);
+ }
+
+ /**
+ * Called by the bridge to fetch data and update channels
+ *
+ * @param bridgeConfiguration the connected bridge configuration
+ */
+ public void refresh() {
+ try {
+ handleRefresh();
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } catch (KermiCommunicationException | RuntimeException e) {
+ logger.debug("Exception caught in refresh() for {}", getThing().getUID().getId(), e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+
+ protected void handleRefresh() throws KermiCommunicationException {
+ DeviceInfo deviceInfo = getKermiSiteInfo().getDeviceInfoByAddress(getBusAddress());
+ if (deviceInfo != null) {
+ updateProperties(deviceInfo);
+ } else {
+ throw new KermiCommunicationException("Not yet initialized");
+ }
+
+ updateChannels();
+ };
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBaseThingHandlerUtil.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBaseThingHandlerUtil.java
new file mode 100644
index 0000000000000..cffb1381a6f64
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBaseThingHandlerUtil.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.handler;
+
+import javax.measure.Unit;
+
+import org.openhab.binding.kermi.internal.KermiBindingConstants;
+import org.openhab.binding.kermi.internal.api.Config;
+import org.openhab.binding.kermi.internal.model.KermiSiteInfoUtil;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+import tech.units.indriya.unit.Units;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class KermiBaseThingHandlerUtil {
+
+ public ChannelTypeUID determineChannelTypeUID(Config datapointConfig) {
+ switch (datapointConfig.getDatapointType()) {
+ case 0:
+ // enumeration values, there should be a specific item-type for each
+ // of these channels, we simply return the numeric value by now
+ return KermiBindingConstants.CHANNEL_TYPE_NUMBER;
+ case 1:
+ Unit> unit = KermiSiteInfoUtil.determineUnitByString(datapointConfig.getUnit());
+ if (Units.WATT.equals(unit)) {
+ return KermiBindingConstants.CHANNEL_TYPE_POWER;
+ } else if (Units.CELSIUS.equals(unit)) {
+ return KermiBindingConstants.CHANNEL_TYPE_TEMPERATURE;
+ }
+ return KermiBindingConstants.CHANNEL_TYPE_NUMBER;
+ case 2:
+ return KermiBindingConstants.CHANNEL_TYPE_ONOFF;
+ case 3:
+ return KermiBindingConstants.CHANNEL_TYPE_STRING;
+ case 4:
+ // time value?
+ return KermiBindingConstants.CHANNEL_TYPE_STRING;
+ default:
+ break;
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBridgeHandler.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBridgeHandler.java
new file mode 100644
index 0000000000000..ff06ddbcd0c4b
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/handler/KermiBridgeHandler.java
@@ -0,0 +1,175 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.handler;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.kermi.internal.KermiBridgeConfiguration;
+import org.openhab.binding.kermi.internal.KermiCommunicationException;
+import org.openhab.binding.kermi.internal.api.DeviceInfo;
+import org.openhab.binding.kermi.internal.api.GetDevicesResponse;
+import org.openhab.binding.kermi.internal.api.KermiHttpUtil;
+import org.openhab.binding.kermi.internal.model.KermiSiteInfo;
+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.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+@NonNullByDefault
+public class KermiBridgeHandler extends BaseBridgeHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(KermiBridgeHandler.class);
+ private static final int DEFAULT_REFRESH_PERIOD = 10;
+ private final Set services = new HashSet<>();
+ private @Nullable ScheduledFuture> refreshJob;
+
+ private KermiHttpUtil httpUtil;
+ private KermiSiteInfo kermiSiteInfo;
+
+ public KermiBridgeHandler(Bridge bridge, KermiHttpUtil httpUtil, KermiSiteInfo kermiSiteInfo) {
+ super(bridge);
+ this.httpUtil = httpUtil;
+ this.kermiSiteInfo = kermiSiteInfo;
+ }
+
+ @Override
+ public void initialize() {
+ final KermiBridgeConfiguration config = getConfigAs(KermiBridgeConfiguration.class);
+
+ boolean validConfig = true;
+ String errorMsg = null;
+
+ String hostname = config.hostname;
+ if (hostname == null || hostname.isBlank()) {
+ errorMsg = "Parameter 'hostname' is mandatory and must be configured";
+ validConfig = false;
+ }
+ String password = config.password;
+ if (password == null || password.isBlank()) {
+ errorMsg = "Parameter 'password' is mandatory and must be configured";
+ validConfig = false;
+ }
+
+ if (config.refreshInterval != null && config.refreshInterval <= 0) {
+ errorMsg = "Parameter 'refresh' must be at least 1 second";
+ validConfig = false;
+ }
+
+ if (validConfig) {
+ httpUtil.setHostname(config.hostname);
+ httpUtil.setPassword(config.password);
+
+ try {
+ initializeKermiSiteInfo();
+
+ startAutomaticRefresh();
+ } catch (KermiCommunicationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ logger.error("Communication error", e);
+ }
+
+ // TODO add channels
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (refreshJob != null) {
+ refreshJob.cancel(true);
+ refreshJob = null;
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
+ if (childHandler instanceof KermiBaseThingHandler) {
+ this.services.add((KermiBaseThingHandler) childHandler);
+ restartAutomaticRefresh();
+ } else {
+ logger.debug("Child handler {} not added because it is not an instance of KermiBaseThingHandler",
+ childThing.getUID().getId());
+ }
+ }
+
+ private void restartAutomaticRefresh() {
+ if (refreshJob != null) { // refreshJob should be null if the config isn't valid
+ refreshJob.cancel(false);
+ startAutomaticRefresh();
+ }
+ }
+
+ @SuppressWarnings("null")
+ private void startAutomaticRefresh() {
+ if (refreshJob == null || refreshJob.isCancelled()) {
+ final KermiBridgeConfiguration config = getConfigAs(KermiBridgeConfiguration.class);
+ Runnable runnable = () -> {
+ try {
+ updateData();
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ for (KermiBaseThingHandler service : services) {
+ service.refresh();
+ }
+ } catch (KermiCommunicationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ kermiSiteInfo.clearSiteInfo();
+ logger.error("Communication error", e);
+ }
+ };
+
+ int delay = (config.refreshInterval != null) ? config.refreshInterval.intValue() : DEFAULT_REFRESH_PERIOD;
+ refreshJob = scheduler.scheduleWithFixedDelay(runnable, 1, delay, TimeUnit.SECONDS);
+ }
+ }
+
+ private void initializeKermiSiteInfo() throws KermiCommunicationException {
+ GetDevicesResponse getDevicesResponse = httpUtil.getAllDevices();
+ List deviceInfo = getDevicesResponse.getResponseData();
+ Map deviceInfoMap = deviceInfo.stream()
+ .collect(Collectors.toMap(DeviceInfo::getDeviceId, Function.identity()));
+ kermiSiteInfo.initializeSiteInfo(httpUtil, deviceInfoMap);
+ }
+
+ private void updateData() throws KermiCommunicationException {
+ if (!kermiSiteInfo.isInitialized()) {
+ initializeKermiSiteInfo();
+ }
+ kermiSiteInfo.updateStateValues(httpUtil);
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/KermiSiteInfo.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/KermiSiteInfo.java
new file mode 100644
index 0000000000000..197b3c3c8e38a
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/KermiSiteInfo.java
@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.model;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.measure.Unit;
+
+import org.apache.commons.collections4.BidiMap;
+import org.apache.commons.collections4.bidimap.DualHashBidiMap;
+import org.eclipse.jdt.annotation.NonNull;
+import org.openhab.binding.kermi.internal.KermiBindingConstants;
+import org.openhab.binding.kermi.internal.KermiCommunicationException;
+import org.openhab.binding.kermi.internal.api.Datapoint;
+import org.openhab.binding.kermi.internal.api.DatapointReadValuesResponse;
+import org.openhab.binding.kermi.internal.api.DatapointValue;
+import org.openhab.binding.kermi.internal.api.DeviceInfo;
+import org.openhab.binding.kermi.internal.api.KermiHttpUtil;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import tech.units.indriya.unit.Units;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+@Component
+public class KermiSiteInfo {
+
+ private Logger logger = LoggerFactory.getLogger(KermiSiteInfo.class);
+
+ private Map deviceIdToDeviceInfo;
+
+ private Map dataPointConfigIdToDatapoint;
+ private BidiMap wellKnownNameToDatapointConfigId;
+
+ // regularly updated
+ private Map wellKnownNameToCurrentState;
+
+ // The WellKnownName datapoint values that are
+ // bound to a channel, and thus included in the
+ // refresh call
+ private Set wellKnownNameRefreshBinding;
+
+ public KermiSiteInfo() {
+ deviceIdToDeviceInfo = Collections.synchronizedMap(new HashMap<>());
+ wellKnownNameToDatapointConfigId = new DualHashBidiMap<>();
+ wellKnownNameRefreshBinding = Collections.synchronizedSet(new HashSet<>());
+ wellKnownNameToCurrentState = Collections.synchronizedMap(new HashMap<>());
+ dataPointConfigIdToDatapoint = Collections.synchronizedMap(new HashMap<>());
+ }
+
+ public DeviceInfo getHeatpumpManagerDeviceInfo() {
+ return deviceIdToDeviceInfo.get(KermiBindingConstants.DEVICE_ID_HEATPUMP_MANAGER);
+ }
+
+ public void updateSiteInfo(Map deviceInfo) {
+ clearSiteInfo();
+ deviceIdToDeviceInfo.putAll(deviceInfo);
+ }
+
+ public void clearSiteInfo() {
+ deviceIdToDeviceInfo.clear();
+ }
+
+ public boolean isInitialized() {
+ return !deviceIdToDeviceInfo.isEmpty();
+ }
+
+ /**
+ * Initialize the information about this site. Iterate through all DeviceInfo elements to collect all
+ * VisualizationDataPoints
+ *
+ * @param _deviceInfo
+ * @throws KermiCommunicationException
+ */
+ public void initializeSiteInfo(KermiHttpUtil httpUtil, Map<@NonNull String, @NonNull DeviceInfo> deviceInfo)
+ throws KermiCommunicationException {
+ clearSiteInfo();
+
+ deviceIdToDeviceInfo.putAll(deviceInfo);
+
+ Collection devices = deviceIdToDeviceInfo.values();
+ for (DeviceInfo device : devices) {
+ List deviceDatapoints = KermiSiteInfoUtil.collectDeviceDatapoints(httpUtil, device);
+ for (Datapoint datapoint : deviceDatapoints) {
+ String wellKnownName = datapoint.getConfig().getWellKnownName();
+ if (wellKnownName != null) {
+ String datapointConfigId = datapoint.getConfig().getDatapointConfigId();
+ wellKnownNameToDatapointConfigId.put(wellKnownName, datapointConfigId);
+ dataPointConfigIdToDatapoint.put(datapointConfigId, datapoint);
+ }
+ }
+ }
+ }
+
+ public void putRefreshBinding(@NonNull String wellKnownId, @NonNull String deviceId) {
+ String datapointConfigId = wellKnownNameToDatapointConfigId.get(wellKnownId);
+ wellKnownNameRefreshBinding.add(new String[] { deviceId, datapointConfigId });
+ }
+
+ public void removeRefreshBinding(@NonNull String wellKnownId, @NonNull String deviceId) {
+ String datapointConfigId = wellKnownNameToDatapointConfigId.get(wellKnownId);
+ wellKnownNameRefreshBinding.remove(new String[] { deviceId, datapointConfigId });
+ }
+
+ public void updateStateValues(@NonNull KermiHttpUtil httpUtil) throws KermiCommunicationException {
+ if (wellKnownNameRefreshBinding.isEmpty()) {
+ return;
+ }
+
+ DatapointReadValuesResponse datapointReadValues = httpUtil.getDatapointReadValues(wellKnownNameRefreshBinding);
+ List datapointValues = datapointReadValues.getResponseData();
+ datapointValues.forEach(dpv -> {
+ // parallel stream?
+ String wellKnownName = wellKnownNameToDatapointConfigId.getKey(dpv.getDatapointConfigId());
+ State currentState = convertDatapointValueToState(dpv);
+ wellKnownNameToCurrentState.put(wellKnownName, currentState);
+
+ });
+ }
+
+ public State convertDatapointValueToState(DatapointValue datapointValue) {
+ // getDatapoint as resolved in #initializeSiteInfo
+ Datapoint datapoint = dataPointConfigIdToDatapoint.get(datapointValue.getDatapointConfigId());
+ if (datapoint == null || datapoint.getConfig() == null) {
+ logger.warn("Could not determine datapoint for datapointConfigId {}",
+ datapointValue.getDatapointConfigId());
+ return null;
+ }
+
+ int datapointType = datapoint.getConfig().getDatapointType();
+ Object value = datapointValue.getValue();
+ if (0 == datapointType) {
+ if (value instanceof Double dValue) {
+ value = dValue.intValue();
+ }
+ // enumeration, needs to translate via "PossibleValues"
+ String valueString = null;
+ Map possibleValues = datapoint.getConfig().getPossibleValues();
+ if (possibleValues != null) {
+ valueString = possibleValues.get(value.toString());
+ }
+ String finalValue = (valueString != null) ? "(" + value + ") " + valueString : "(" + value + ")";
+ return new StringType(finalValue);
+ } else if (1 == datapointType) {
+ // Numeric value or "NaN"
+ if (Objects.equals("NaN", value)) {
+ return new QuantityType<>();
+ }
+ Unit> unit = KermiSiteInfoUtil.determineUnitByString(datapoint.getConfig().getUnit());
+ if (value instanceof Double dValue) {
+ if (Units.WATT.equals(unit)) {
+ dValue *= 1000;
+ }
+ return new QuantityType<>(dValue, unit);
+ }
+ } else if (2 == datapointType) {
+ // OnOff Type
+ if (value instanceof Boolean bool) {
+ return bool ? OnOffType.ON : OnOffType.OFF;
+ }
+ }
+
+ logger.warn("Unknown datapointType {} or datapointValue {} ({}) in {}", datapointType,
+ datapointValue.getValue(), datapointValue.getValue().getClass().getName(),
+ datapoint.getConfig().getWellKnownName());
+
+ return null;
+ }
+
+ public State getStateByWellKnownName(String wellKnownName, String deviceId) {
+ return wellKnownNameToCurrentState.get(wellKnownName);
+ }
+
+ public DeviceInfo getDeviceInfoByAddress(String busAddress) {
+ return deviceIdToDeviceInfo.values().stream().filter(deviceInfo -> busAddress.equals(deviceInfo.getAddress()))
+ .findFirst().orElse(null);
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/KermiSiteInfoUtil.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/KermiSiteInfoUtil.java
new file mode 100644
index 0000000000000..1f0e3829492fa
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/KermiSiteInfoUtil.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.model;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.measure.Unit;
+
+import org.openhab.binding.kermi.internal.KermiBindingConstants;
+import org.openhab.binding.kermi.internal.KermiCommunicationException;
+import org.openhab.binding.kermi.internal.api.Datapoint;
+import org.openhab.binding.kermi.internal.api.DeviceInfo;
+import org.openhab.binding.kermi.internal.api.KermiHttpUtil;
+import org.openhab.binding.kermi.internal.api.MenuEntry;
+import org.openhab.binding.kermi.internal.api.MenuEntryResponse;
+import org.openhab.binding.kermi.internal.api.MenuGetChildEntriesResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonIOException;
+import com.google.gson.stream.JsonReader;
+
+import tech.units.indriya.unit.Units;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class KermiSiteInfoUtil {
+
+ private static Logger logger = LoggerFactory.getLogger(KermiSiteInfoUtil.class);
+
+ /**
+ * Collects the datapoints for a given device, if a cache file is available, the cache is loaded
+ *
+ * @param httpUtil
+ * @param deviceInfo
+ * @return
+ * @throws KermiCommunicationException
+ * @throws IOException
+ * @throws JsonIOException
+ */
+ public static List collectDeviceDatapoints(KermiHttpUtil httpUtil, DeviceInfo deviceInfo)
+ throws KermiCommunicationException {
+
+ List dataPoints = loadDeviceDatapointsCache(deviceInfo);
+ if (dataPoints == null) {
+ logger.info("Collecting Datapoints for Device {}", deviceInfo.getDeviceId());
+ MenuGetChildEntriesResponse rootResponse = httpUtil.getMenuChildEntries(deviceInfo.getDeviceId(),
+ KermiBindingConstants.DEVICE_ID_HEATPUMP_MANAGER);
+ MenuEntryResponse rootChildEntry = rootResponse.getResponseData();
+
+ dataPoints = new ArrayList();
+ collectAndTraverse(httpUtil, deviceInfo.getDeviceId(), dataPoints, rootChildEntry);
+ storeDeviceDatapointsCache(deviceInfo, dataPoints);
+ }
+
+ return dataPoints;
+ }
+
+ private static void storeDeviceDatapointsCache(DeviceInfo deviceInfo, List dataPoints)
+ throws KermiCommunicationException {
+ File file = new File(KermiBindingConstants.getKermiUserDataFolder(),
+ deviceInfo.getDeviceId() + "-" + deviceInfo.getSerial().trim() + ".json");
+
+ ListDatapointCacheFile listDatapointCacheFile = new ListDatapointCacheFile();
+ listDatapointCacheFile.setDeviceId(deviceInfo.getDeviceId());
+ listDatapointCacheFile.setSerial(deviceInfo.getSerial());
+ listDatapointCacheFile.setAddress(deviceInfo.getAddress());
+ listDatapointCacheFile.setName(deviceInfo.getName());
+
+ // clean the values, we don't need them
+ List datapointsWithoutValues = dataPoints.stream().map(dp -> {
+ dp.setDatapointValue(null);
+ return dp;
+ }).collect(Collectors.toList());
+ listDatapointCacheFile.setDatapoints(datapointsWithoutValues);
+ try (FileWriter filewriter = new FileWriter(file, StandardCharsets.UTF_8)) {
+ new Gson().toJson(listDatapointCacheFile, filewriter);
+ } catch (JsonIOException | IOException e) {
+ throw new KermiCommunicationException(e);
+ }
+ }
+
+ private static List loadDeviceDatapointsCache(DeviceInfo deviceInfo) {
+ File file = new File(KermiBindingConstants.getKermiUserDataFolder(),
+ deviceInfo.getDeviceId() + "-" + deviceInfo.getSerial().trim() + ".json");
+ if (file.exists()) {
+ try (JsonReader reader = new JsonReader(new FileReader(file, StandardCharsets.UTF_8))) {
+ logger.debug("Loading cached datapoints for device {}", deviceInfo.getDeviceId());
+ ListDatapointCacheFile cacheFile = new Gson().fromJson(reader, ListDatapointCacheFile.class);
+ return cacheFile.getDatapoints();
+ } catch (IOException e) {
+ logger.warn("Error loading device datapoint cache file", e);
+ }
+ }
+
+ return null;
+ }
+
+ private static void collectAndTraverse(KermiHttpUtil httpUtil, String deviceId, List dataPoints,
+ MenuEntryResponse menuEntry) throws KermiCommunicationException {
+ if (!menuEntry.getBundles().isEmpty()) {
+ menuEntry.getBundles().forEach(bundle -> dataPoints.addAll(bundle.getDatapoints()));
+ }
+ List menuEntries = menuEntry.getMenuEntries();
+ for (MenuEntry me : menuEntries) {
+ try {
+ Thread.sleep(25);
+ } catch (InterruptedException e) {
+ // just some throttling
+ }
+ MenuGetChildEntriesResponse menuChildEntry = httpUtil.getMenuChildEntries(deviceId, me.getMenuEntryId());
+ collectAndTraverse(httpUtil, deviceId, dataPoints, menuChildEntry.getResponseData());
+ }
+ }
+
+ /**
+ *
+ * @param unitString
+ * @return null
if unit could not be determined
+ */
+ public static Unit> determineUnitByString(String unitString) {
+ if ("kW".equals(unitString)) {
+ return Units.WATT;
+ } else if ("°C".equals(unitString)) {
+ return Units.CELSIUS;
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/ListDatapointCacheFile.java b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/ListDatapointCacheFile.java
new file mode 100644
index 0000000000000..82ddbcf802cd0
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/java/org/openhab/binding/kermi/internal/model/ListDatapointCacheFile.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2023 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.kermi.internal.model;
+
+import java.util.List;
+
+import org.openhab.binding.kermi.internal.api.Datapoint;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Marco Descher - Initial contribution
+ */
+public class ListDatapointCacheFile {
+
+ @SerializedName("DeviceId")
+ private String deviceId;
+
+ @SerializedName("Serial")
+ private String serial;
+
+ @SerializedName("Address")
+ private String address;
+
+ @SerializedName("Name")
+ private String name;
+
+ @SerializedName("Datapoints")
+ private List datapoints;
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public String getSerial() {
+ return serial;
+ }
+
+ public void setSerial(String serial) {
+ this.serial = serial;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getDatapoints() {
+ return datapoints;
+ }
+
+ public void setDatapoints(List datapoints) {
+ this.datapoints = datapoints;
+ }
+}
diff --git a/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..ab717408124c6
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,11 @@
+
+
+
+ binding
+ Kermi Binding
+ Binding for Kermi x-center Heatpump Manager
+ local
+
+
diff --git a/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/config/deviceConfig.xml b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/config/deviceConfig.xml
new file mode 100644
index 0000000000000..1386fc0840269
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/config/deviceConfig.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Specific device identifier
+
+
+
+
diff --git a/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/i18n/kermi.properties b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/i18n/kermi.properties
new file mode 100644
index 0000000000000..0c2f44015a92d
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/i18n/kermi.properties
@@ -0,0 +1,3 @@
+# FIXME: please add all English translations to this file so the texts can be translated using Crowdin
+# FIXME: to generate the content of this file run: mvn i18n:generate-default-translations
+# FIXME: see also: https://www.openhab.org/docs/developer/utils/i18n.html
diff --git a/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644
index 0000000000000..ebe641ad27c96
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/thing/bridge.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ A bridge to connect Kermi devices
+
+
+ network-address
+
+ The hostname or IP address of the gateway/device
+
+
+
+ The password to access the bridge api
+
+
+
+ Specifies the refresh interval in seconds.
+ 60
+
+
+
+
diff --git a/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..31e23270ba994
--- /dev/null
+++ b/bundles/org.openhab.binding.kermi/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+ Kermi Heatpump Manager
+
+
+
+
+
+
+
+
+
+
+
+
+ Specific device identifier
+ 51
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Specific device identifier
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Specific device identifier
+ 50
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number
+
+
+
+
+
+ Switch
+
+
+
+
+ String
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index b58688b52dbfa..0ec9797470b8d 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -197,6 +197,7 @@
org.openhab.binding.juicenet
org.openhab.binding.kaleidescape
org.openhab.binding.keba
+ org.openhab.binding.kermi
org.openhab.binding.km200
org.openhab.binding.knx
org.openhab.binding.kodi