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