(.*?)<\\/tr>");
+ Matcher matcher2 = pattern2.matcher(userTable);
+
+ int idx = 0;
+ while (matcher2.find()) {
+ String line = matcher2.group(1);
+
+ if (idx > 0) {
+ Pattern pattern3 = Pattern.compile("(.*?)<\\/td>");
+ Matcher matcher3 = pattern3.matcher(line);
+
+ int idxCell = 0;
+ String userName = "";
+ String userEdit = "";
+ String userId = "";
+ while (matcher3.find()) {
+ String cell = matcher3.group(2);
+ String header = matcher3.group(1);
+
+ if (idxCell == 0) {
+ userName = cell;
+ } else if (idxCell == 5) {
+ userEdit = header;
+ }
+ idxCell++;
+ }
+
+ if ("".equals(userName)) {
+ continue;
+ }
+
+ Pattern pattern4 = Pattern.compile("userid=(.+?)");
+ Matcher matcher4 = pattern4.matcher(userEdit);
+
+ SiemensHvacMetadataUser user = new SiemensHvacMetadataUser();
+ user.setName(userName);
+
+ if (matcher4.find()) {
+ userId = matcher4.group(1);
+ user.setId(Integer.parseInt(userId));
+ } else {
+ userId = null;
+ user.setId(-1);
+ }
+
+ request = "main.app?section=settings&subsection=user&action=modify";
+ if (userId != null) {
+ request = request + "&userid=" + userId;
+ }
+ response = lcHvacConnector.doBasicRequest(request);
+
+ Pattern pattern5 = Pattern.compile("((.*|\\n)*?) ",
+ Pattern.MULTILINE);
+ Matcher matcher5 = pattern5.matcher(response);
+
+ if (matcher5.find()) {
+ String optionsList = matcher5.group(1);
+
+ Pattern pattern6 = java.util.regex.Pattern
+ .compile("(.*) ", Pattern.MULTILINE);
+ Matcher matcher6 = pattern6.matcher(optionsList);
+
+ while (matcher6.find()) {
+ String id = matcher6.group(1);
+ String opt = matcher6.group(2);
+ String lang = matcher6.group(3);
+
+ if (opt.indexOf("selected") >= 0) {
+ user.setLanguage(lang);
+ user.setLanguageId(Integer.parseInt(id));
+ }
+ }
+ }
+
+ userList.put(userName, user);
+ }
+
+ idx++;
+
+ }
+ }
+ }
+ }
+ }
+
+ public void changeLanguage(SiemensHvacMetadataUser user, int lang) {
+ try {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ String request = "main.app?section=settings&subsection=user&action=modify";
+ if (user.getId() != -1) {
+ request = request + "&userid=" + user.getId();
+ }
+ request = request + "&language=" + lang + "&submit=OK";
+ if (lcHvacConnector != null) {
+ lcHvacConnector.doBasicRequest(request);
+ lcHvacConnector.resetSessionId(null, false);
+ lcHvacConnector.resetSessionId(null, true);
+ }
+
+ } catch (
+
+ Exception e) {
+ logger.error("siemensHvac:ResolveDpt:Error during dp reading: {}", e.getLocalizedMessage());
+ // Reset sessionId so we redone _auth on error
+ }
+ }
+
+ public void readDeviceList() {
+ try {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ ArrayList lcDevices = devices;
+
+ lcDevices = new ArrayList();
+ devices = lcDevices;
+ String request = "api/devicelist/list.json?";
+
+ JsonObject response = null;
+ if (lcHvacConnector != null) {
+ response = lcHvacConnector.doRequest(request);
+ }
+ JsonArray devicesList = null;
+ if (response != null) {
+ devicesList = response.getAsJsonArray("Devices");
+ }
+
+ if (devicesList == null) {
+ return;
+ }
+
+ for (JsonElement device : devicesList) {
+
+ JsonObject obj = (JsonObject) device;
+ String name = "";
+ String addr = "";
+ String type = "";
+ String serialNr = "";
+ String treeDate = "";
+ String treeTime = "";
+ boolean treeGenerated = false;
+
+ if (obj.has("Name")) {
+ name = obj.get("Name").getAsString();
+ }
+
+ if (obj.has("Addr")) {
+ addr = obj.get("Addr").getAsString();
+ }
+
+ if (obj.has("Type")) {
+ type = obj.get("Type").getAsString();
+ }
+
+ if (obj.has("SerialNr")) {
+ serialNr = obj.get("SerialNr").getAsString();
+ }
+
+ if (obj.has("TreeDate")) {
+ treeDate = obj.get("TreeDate").getAsString();
+ }
+
+ if (obj.has("TreeTime")) {
+ treeTime = obj.get("TreeTime").getAsString();
+ }
+
+ if (obj.has("TreeGenerated")) {
+ treeGenerated = obj.get("TreeGenerated").getAsBoolean();
+ }
+
+ SiemensHvacMetadataDevice deviceObj = new SiemensHvacMetadataDevice();
+ deviceObj.setName(name);
+ deviceObj.setAddr(addr);
+ deviceObj.setSerialNr(serialNr);
+ deviceObj.setType(type);
+ deviceObj.setTreeDate(treeDate);
+ deviceObj.setTreeTime(treeTime);
+ deviceObj.setTreeGenerated(treeGenerated);
+
+ String request2 = "api/menutree/device_root.json?TreeName=Web&SerialNumber=" + serialNr;
+ if (lcHvacConnector != null) {
+ JsonObject response2 = lcHvacConnector.doRequest(request2);
+
+ if (response2 != null && response2.has("TreeItem")) {
+ JsonObject tree = response2.getAsJsonObject("TreeItem");
+ if (tree.has("Id")) {
+ int treeId = tree.get("Id").getAsInt();
+ deviceObj.setTreeId(treeId);
+ }
+ }
+ }
+
+ lcDevices.add(deviceObj);
+ }
+
+ } catch (Exception e) {
+ logger.error("siemensHvac:ResolveDpt:Error during dp reading: {}", e.getLocalizedMessage());
+ // Reset sessionId so we redone _auth on error
+ }
+ }
+
+ public void readMetaData(@Nullable SiemensHvacMetadata parent, int id, boolean localized) {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ String request = "api/menutree/list.json?";
+ if (id != -1) {
+ request = request + "&Id=" + id;
+ }
+
+ if (lcHvacConnector != null) {
+ lcHvacConnector.doRequest(request, new SiemensHvacCallback() {
+
+ @Override
+ public void execute(URI uri, int status, @Nullable Object response) {
+ logger.debug("response for {}, status {}:", uri, status);
+ if (response instanceof JsonObject jsonResponse) {
+ decodeMetaDataResult(jsonResponse, parent, id, localized);
+ } else {
+ logger.debug("error status {}: {}", uri, status);
+ }
+ }
+ });
+ }
+ }
+
+ public void decodeMetaDataResult(JsonObject resultObj, @Nullable SiemensHvacMetadata parent, int id,
+ boolean localized) {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ if (resultObj.has("MenuItems")) {
+ if (parent != null) {
+ logger.debug("Decode menuItem for: {}", parent.getShortDesc());
+ }
+ SiemensHvacMetadata childNode;
+ JsonArray menuItems = resultObj.getAsJsonArray("MenuItems");
+
+ for (JsonElement child : menuItems) {
+ JsonObject menuItem = child.getAsJsonObject();
+
+ int itemId = -1;
+ if (menuItem.has("Id")) {
+ itemId = menuItem.get("Id").getAsInt();
+ }
+
+ SiemensHvacMetadataMenu menu = (SiemensHvacMetadataMenu) parent;
+
+ if (menu.hasChild(itemId)) {
+ childNode = menu.getChild(itemId);
+ } else {
+ childNode = new SiemensHvacMetadataMenu();
+ childNode.setId(itemId);
+ childNode.setParent(parent);
+
+ if (parent != null) {
+ menu.addChild(childNode);
+ }
+ }
+
+ if (menuItem.has("Text")) {
+ JsonObject descObj = menuItem.getAsJsonObject("Text");
+
+ int catId = -1;
+ int groupId = -1;
+ int subItemId = -1;
+ String longDesc = "";
+ String shortDesc = "";
+
+ if (descObj.has("CatId")) {
+ catId = descObj.get("CatId").getAsInt();
+ }
+ if (descObj.has("GroupId")) {
+ groupId = descObj.get("GroupId").getAsInt();
+ }
+ if (descObj.has("Id")) {
+ subItemId = descObj.get("Id").getAsInt();
+ }
+
+ if (descObj.has("Long")) {
+ longDesc = descObj.get("Long").getAsString();
+ }
+ if (descObj.has("Short")) {
+ shortDesc = descObj.get("Short").getAsString();
+ }
+
+ childNode.setSubId(subItemId);
+ childNode.setCatId(catId);
+ childNode.setGroupId(groupId);
+ if (!localized) {
+ childNode.setShortDescEn(shortDesc);
+ childNode.setLongDescEn(longDesc);
+ } else {
+ childNode.setShortDesc(shortDesc);
+ childNode.setLongDesc(longDesc);
+ }
+
+ readMetaData(childNode, itemId, localized);
+ }
+
+ }
+ }
+ if (resultObj.has("DatapointItems"))
+
+ {
+ if (parent != null) {
+ logger.debug("Decode dp for: {}", parent.getShortDesc());
+ }
+
+ SiemensHvacMetadata childNode;
+ JsonArray dptItems = resultObj.getAsJsonArray("DatapointItems");
+
+ Map idMap = new Hashtable();
+
+ for (JsonElement child : dptItems) {
+ JsonObject dptItem = child.getAsJsonObject();
+
+ int nodeId = -1;
+ int dpSubKey = -1;
+ boolean hasWriteAccess = false;
+ String address = "";
+
+ if (dptItem.has("Id")) {
+ nodeId = dptItem.get("Id").getAsInt();
+ }
+
+ SiemensHvacMetadataMenu menu = (SiemensHvacMetadataMenu) parent;
+
+ if (menu.hasChild(nodeId)) {
+ childNode = menu.getChild(nodeId);
+ } else {
+ childNode = new SiemensHvacMetadataDataPoint();
+ childNode.setId(nodeId);
+ childNode.setParent(parent);
+
+ menu.addChild(childNode);
+ }
+
+ if (dptItem.has("Address")) {
+ address = dptItem.get("Address").getAsString();
+ }
+ if (dptItem.has("DpSubKey")) {
+ dpSubKey = dptItem.get("DpSubKey").getAsInt();
+ }
+ if (dptItem.has("WriteAccess")) {
+ hasWriteAccess = dptItem.get("WriteAccess").getAsBoolean();
+ }
+
+ SiemensHvacMetadataDataPoint dptChild = (SiemensHvacMetadataDataPoint) childNode;
+
+ dptChild.setId(nodeId);
+ dptChild.setAddress(address);
+ dptChild.setDptSubKey(dpSubKey);
+ dptChild.setWriteAccess(hasWriteAccess);
+
+ idMap.put("" + nodeId, dptChild);
+
+ if (dptItem.has("Text")) {
+ JsonObject descObj = dptItem.getAsJsonObject("Text");
+
+ int catId = -1;
+ int groupId = -1;
+ int subItemId = -1;
+ String longDesc = "";
+ String shortDesc = "";
+
+ if (descObj.has("CatId")) {
+ catId = descObj.get("CatId").getAsInt();
+ }
+ if (descObj.has("GroupId")) {
+ groupId = descObj.get("GroupId").getAsInt();
+ }
+ if (descObj.has("Id")) {
+ subItemId = descObj.get("Id").getAsInt();
+ }
+ if (descObj.has("Long")) {
+ longDesc = descObj.get("Long").getAsString();
+ }
+ if (descObj.has("Short")) {
+ shortDesc = descObj.get("Short").getAsString();
+ }
+
+ childNode.setSubId(subItemId);
+ childNode.setCatId(catId);
+ childNode.setGroupId(groupId);
+
+ if (!localized) {
+ childNode.setShortDescEn(shortDesc);
+ childNode.setLongDescEn(longDesc);
+ } else {
+ childNode.setShortDesc(shortDesc);
+ childNode.setLongDesc(longDesc);
+ }
+ }
+
+ }
+
+ String request2 = "main.app?section=popcard&idtype=4";
+ if (id != -1) {
+ request2 = request2 + "&id=" + id;
+ }
+
+ if (lcHvacConnector != null) {
+ lcHvacConnector.doRequest(request2, new SiemensHvacCallback() {
+
+ @Override
+ public void execute(URI uri, int status, @Nullable Object response) {
+ if (response != null) {
+ String st = (String) response;
+ st = st.replace("\n", "");
+
+ Pattern pattern = Pattern
+ .compile("td class=\\\"dp_linenumber\\\".*?>(.*?)<\\/td>.+?(?=id)id=\"dp(.+?)\"");
+ Matcher matcher = pattern.matcher(st);
+
+ while (matcher.find()) {
+ String id = matcher.group(2);
+ String dptId = matcher.group(1);
+
+ if (id != null && dptId != null && !id.isEmpty() && !dptId.isEmpty()) {
+ if (idMap.containsKey(id)) {
+ SiemensHvacMetadataDataPoint child = idMap.get(id);
+ if (child != null) {
+ child.setDptId(dptId);
+ }
+ }
+
+ }
+ }
+ }
+ }
+ });
+ }
+
+ }
+ }
+
+ @Override
+ public @Nullable SiemensHvacMetadata getDptMap(@Nullable String key) {
+ if (key == null) {
+ return null;
+ }
+
+ if (dptMap.containsKey("byMenu" + key)) {
+ return dptMap.get("byMenu" + key);
+ }
+ if (dptMap.containsKey("byName" + key)) {
+ return dptMap.get("byName" + key);
+ }
+ if (dptMap.containsKey("byDptId" + key)) {
+ return dptMap.get("byDptId" + key);
+ }
+ if (dptMap.containsKey("byId" + key)) {
+ return dptMap.get("byId" + key);
+ }
+
+ return null;
+ }
+
+ public void loadMetaDataFromCache() {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ File file = null;
+
+ try {
+ file = new File(JSON_DIR + File.separator + "siemens.json");
+
+ if (!file.exists()) {
+ return;
+ }
+
+ byte[] bytes = Files.readAllBytes(file.toPath());
+ String js = new String(bytes, StandardCharsets.UTF_8);
+
+ if (lcHvacConnector != null) {
+ root = lcHvacConnector.getGsonWithAdapter().fromJson(js, SiemensHvacMetadataMenu.class);
+ }
+ } catch (IOException ioe) {
+ logger.warn("Couldn't read Siemens MetaData information from file '{}'.", file.getAbsolutePath());
+
+ }
+ }
+
+ public void saveMetaDataToCache() {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ File file = null;
+
+ try {
+ file = new File(JSON_DIR + File.separator + "siemens.json");
+
+ if (!file.exists()) {
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ }
+
+ try (FileOutputStream os = new FileOutputStream(file)) {
+ if (lcHvacConnector != null) {
+ String js = lcHvacConnector.getGsonWithAdapter().toJson(root);
+
+ byte[] bt = js.getBytes();
+ os.write(bt);
+ os.flush();
+ }
+ }
+
+ } catch (IOException ioe) {
+ logger.warn("Couldn't write Siemens MetaData information to file '{}'.", file.getAbsolutePath());
+
+ }
+ }
+
+ public void resolveDptDetails(SiemensHvacMetadataDataPoint dpt, ResolveCount rv) {
+ SiemensHvacConnector lcHvacConnector = hvacConnector;
+ if (dpt.getDetailsResolved()) {
+ return;
+ }
+
+ String request = "api/menutree/datapoint_desc.json?Id=" + dpt.getId();
+ if (lcHvacConnector != null) {
+ lcHvacConnector.doRequest(request, new SiemensHvacCallback() {
+
+ @Override
+ public void execute(URI uri, int status, @Nullable Object response) {
+ if (response instanceof JsonObject) {
+ rv.decreaseResolveCount();
+ logger.debug("siemensHvac:Initialization():ToResolve() {}", rv.getResolveCount());
+ dpt.resolveDptDetails((JsonObject) response);
+ } else {
+ logger.debug("Invalid response from Siemens gateway, result is not a JsonObject");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void invalidate() {
+ root = null;
+ SiemensHvacConnector lcHavConnector = hvacConnector;
+ SiemensHvacChannelGroupTypeProvider lcChannelGroupTypeProvider = channelGroupTypeProvider;
+ SiemensHvacThingTypeProvider lcThingTypeProvider = thingTypeProvider;
+ SiemensHvacChannelTypeProvider lcChannelTypeProvider = channelTypeProvider;
+ SiemensHvacConfigDescriptionProvider lcConfigDescriptionProvider = configDescriptionProvider;
+
+ if (lcHavConnector != null) {
+ lcHavConnector.invalidate();
+ }
+
+ if (lcChannelGroupTypeProvider != null) {
+ lcChannelGroupTypeProvider.invalidate();
+ }
+
+ if (lcThingTypeProvider != null) {
+ lcThingTypeProvider.invalidate();
+ }
+
+ if (lcChannelTypeProvider != null) {
+ lcChannelTypeProvider.invalidate();
+ }
+
+ if (lcConfigDescriptionProvider != null) {
+ lcConfigDescriptionProvider.invalidate();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataUser.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataUser.java
new file mode 100644
index 00000000000..64647fdc533
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataUser.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.metadata;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacMetadataUser {
+ private String name;
+ private int id;
+ private String language;
+ private int languageId;
+
+ public SiemensHvacMetadataUser() {
+ name = "";
+ language = "";
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public int getLanguageId() {
+ return languageId;
+ }
+
+ public void setLanguageId(int languageId) {
+ this.languageId = languageId;
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacCallback.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacCallback.java
new file mode 100644
index 00000000000..32766a7854f
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacCallback.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.network;
+
+import java.net.URI;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacCallback {
+ /**
+ * Runs callback code after response completion.
+ */
+ void execute(URI uri, int status, @Nullable Object response);
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnector.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnector.java
new file mode 100644
index 00000000000..3b5583cf83e
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnector.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.network;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeConfig;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeThingHandler;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacConnector {
+
+ @Nullable
+ String doBasicRequest(String uri) throws SiemensHvacException;
+
+ @Nullable
+ JsonObject doRequest(String req);
+
+ @Nullable
+ JsonObject doRequest(String req, @Nullable SiemensHvacCallback callback);
+
+ void waitAllPendingRequest();
+
+ void waitNoNewRequest();
+
+ void onComplete(Request request, SiemensHvacRequestHandler reqListener) throws SiemensHvacException;
+
+ void onError(Request request, SiemensHvacRequestHandler reqListener,
+ SiemensHvacRequestListener.ErrorSource errorSource, boolean mayRetry) throws SiemensHvacException;
+
+ void setSiemensHvacBridgeBaseThingHandler(SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler);
+
+ @Nullable
+ SiemensHvacBridgeConfig getBridgeConfiguration();
+
+ void resetSessionId(@Nullable String sessionIdToInvalidate, boolean web);
+
+ void displayRequestStats();
+
+ Gson getGson();
+
+ Gson getGsonWithAdapter();
+
+ int getRequestCount();
+
+ int getErrorCount();
+
+ SiemensHvacRequestListener.ErrorSource getErrorSource();
+
+ void invalidate();
+
+ void setTimeOut(int timeout);
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnectorImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnectorImpl.java
new file mode 100644
index 00000000000..eaa5bc649d6
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnectorImpl.java
@@ -0,0 +1,669 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.network;
+
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeConfig;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeThingHandler;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadata;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataMenu;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.types.Type;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class SiemensHvacConnectorImpl implements SiemensHvacConnector {
+
+ private final Logger logger = LoggerFactory.getLogger(SiemensHvacConnectorImpl.class);
+
+ private Map currentHandlerRegistry = new ConcurrentHashMap<>();
+ private Map handlerInErrorRegistry = new ConcurrentHashMap<>();
+
+ private Map oldSessionId = new HashMap<>();
+
+ private final Gson gson;
+ private final Gson gsonWithAdapter;
+
+ private @Nullable String sessionId = null;
+ private @Nullable String sessionIdHttp = null;
+ private @Nullable SiemensHvacBridgeConfig config = null;
+
+ protected final HttpClientFactory httpClientFactory;
+
+ protected HttpClient httpClient;
+
+ private Map updateCommand;
+
+ private int requestCount = 0;
+ private int errorCount = 0;
+ private int timeout = 10;
+ private SiemensHvacRequestListener.ErrorSource errorSource = SiemensHvacRequestListener.ErrorSource.ErrorBridge;
+
+ private @Nullable SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler;
+
+ @Activate
+ public SiemensHvacConnectorImpl(@Reference HttpClientFactory httpClientFactory) {
+ GsonBuilder builder = new GsonBuilder();
+ gson = builder.setPrettyPrinting().create();
+
+ RuntimeTypeAdapterFactory adapter = RuntimeTypeAdapterFactory
+ .of(SiemensHvacMetadata.class);
+ adapter.registerSubtype(SiemensHvacMetadataMenu.class);
+ adapter.registerSubtype(SiemensHvacMetadataDataPoint.class);
+
+ gsonWithAdapter = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
+
+ this.updateCommand = new Hashtable();
+ this.httpClientFactory = httpClientFactory;
+
+ SslContextFactory ctxFactory = new SslContextFactory.Client(true);
+ ctxFactory.setRenegotiationAllowed(false);
+ ctxFactory.setEnableCRLDP(false);
+ ctxFactory.setEnableOCSP(false);
+ ctxFactory.setTrustAll(true);
+ ctxFactory.setValidateCerts(false);
+ ctxFactory.setValidatePeerCerts(false);
+ ctxFactory.setEndpointIdentificationAlgorithm(null);
+
+ this.httpClient = new HttpClient(ctxFactory);
+ this.httpClient.setMaxConnectionsPerDestination(10);
+ this.httpClient.setMaxRequestsQueuedPerDestination(10000);
+ this.httpClient.setConnectTimeout(10000);
+ this.httpClient.setFollowRedirects(false);
+
+ try {
+ this.httpClient.start();
+ } catch (Exception e) {
+ logger.error("Failed to start http client: {}", e.getMessage());
+ }
+ }
+
+ @Override
+ public void setSiemensHvacBridgeBaseThingHandler(
+ @Nullable SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler) {
+ this.hvacBridgeBaseThingHandler = hvacBridgeBaseThingHandler;
+ }
+
+ public void unsetSiemensHvacBridgeBaseThingHandler(SiemensHvacBridgeThingHandler hvacBridgeBaseThingHandler) {
+ this.hvacBridgeBaseThingHandler = null;
+ }
+
+ @Override
+ public void onComplete(@Nullable Request request, SiemensHvacRequestHandler reqHandler)
+ throws SiemensHvacException {
+ unregisterRequestHandler(reqHandler);
+ }
+
+ public static String extractSessionId(String query) {
+ int idx1 = query.indexOf("SessionId=");
+ int idx2 = query.indexOf("&", idx1 + 1);
+ if (idx2 < 0) {
+ idx2 = query.length();
+ }
+
+ String sessionId = query.substring(idx1 + 10, idx2);
+ return sessionId;
+ }
+
+ @Override
+ public void onError(@Nullable Request request, @Nullable SiemensHvacRequestHandler reqHandler,
+ SiemensHvacRequestListener.ErrorSource errorSource, boolean mayRetry) throws SiemensHvacException {
+ if (reqHandler == null || request == null) {
+ throw new SiemensHvacException("internalError: onError call with reqHandler == null");
+ }
+
+ boolean doRetry = mayRetry;
+ // Don't retry if we have do it multiple time
+ if (reqHandler.getRetryCount() >= 5) {
+ doRetry = false;
+ }
+
+ // Don't retry if we lost session, just abort the request, and wait next loop
+ if (sessionIdHttp == null || sessionId == null) {
+ doRetry = false;
+ }
+
+ if (!doRetry) {
+ logger.debug("unable to handle request, doRetry = false, cancel it");
+ unregisterRequestHandler(reqHandler);
+ registerHandlerError(reqHandler);
+ errorCount++;
+ this.errorSource = errorSource;
+ return;
+ }
+
+ try {
+ // Wait one second before retrying the request to avoid flooding the gateway
+ Thread.sleep(1000);
+ } catch (InterruptedException ex) {
+ // We can silently ignore this one
+ }
+
+ if (sessionIdHttp == null) {
+ doAuth(true);
+ }
+
+ if (sessionId == null) {
+ doAuth(false);
+ }
+
+ try {
+ URI uri = request.getURI();
+ String query = uri.toString();
+
+ String sessionIdInQuery = extractSessionId(query);
+ if (query.indexOf("main.app") >= 0) {
+ String sessionIdHttpLc = sessionIdHttp;
+
+ if (sessionIdHttpLc != null && !sessionIdHttpLc.equals(sessionIdInQuery)) {
+ uri = new URI(query.replace(sessionIdInQuery, sessionIdHttpLc));
+ }
+ } else {
+ String sessionIdLc = sessionId;
+
+ if (sessionIdLc != null && !sessionIdLc.equals(sessionIdInQuery)) {
+ uri = new URI(query.replace(sessionIdInQuery, sessionIdLc));
+ }
+ }
+
+ final Request retryRequest = httpClient.newRequest(uri);
+ request.method(HttpMethod.GET);
+ reqHandler.setRequest(retryRequest);
+ reqHandler.incrementRetryCount();
+
+ if (retryRequest != null) {
+ executeRequest(retryRequest, reqHandler);
+ }
+ } catch (URISyntaxException ex) {
+ throw new SiemensHvacException("Error during gateway request", ex);
+ }
+ }
+
+ private @Nullable ContentResponse executeRequest(final Request request) throws SiemensHvacException {
+ return executeRequest(request, (SiemensHvacCallback) null);
+ }
+
+ private @Nullable ContentResponse executeRequest(final Request request, @Nullable SiemensHvacCallback callback)
+ throws SiemensHvacException {
+ requestCount++;
+
+ // For asynchronous request, we create a RequestHandler that will enable us to follow request state
+ SiemensHvacRequestHandler requestHandler = null;
+ if (callback != null) {
+ requestHandler = new SiemensHvacRequestHandler(callback, this);
+ requestHandler.setRequest(request);
+ currentHandlerRegistry.put(requestHandler, requestHandler);
+ }
+
+ return executeRequest(request, requestHandler);
+ }
+
+ private void unregisterRequestHandler(SiemensHvacRequestHandler handler) throws SiemensHvacException {
+ synchronized (currentHandlerRegistry) {
+ if (currentHandlerRegistry.containsKey(handler)) {
+ currentHandlerRegistry.remove(handler);
+ }
+ }
+ }
+
+ private void registerHandlerError(SiemensHvacRequestHandler handler) {
+ synchronized (handlerInErrorRegistry) {
+ handlerInErrorRegistry.put(handler, handler);
+ }
+ }
+
+ private @Nullable ContentResponse executeRequest(final Request request,
+ @Nullable SiemensHvacRequestHandler requestHandler) throws SiemensHvacException {
+ // Give a high timeout because we queue a lot of async request,
+ // so enqueued them will take some times ...
+ request.timeout(timeout, TimeUnit.SECONDS);
+
+ ContentResponse response = null;
+
+ try {
+ if (requestHandler != null) {
+ SiemensHvacRequestListener requestListener = new SiemensHvacRequestListener(requestHandler);
+ request.send(requestListener);
+ } else {
+ response = request.send();
+ }
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ throw new SiemensHvacException("siemensHvac:Exception by executing request: "
+ + anominized(request.getURI().toString()) + " ; " + e.getLocalizedMessage());
+ }
+ return response;
+ }
+
+ private void initConfig() throws SiemensHvacException {
+ SiemensHvacBridgeThingHandler lcHvacBridgeBaseThingHandler = hvacBridgeBaseThingHandler;
+
+ if (lcHvacBridgeBaseThingHandler != null) {
+ config = lcHvacBridgeBaseThingHandler.getBridgeConfiguration();
+ } else {
+ throw new SiemensHvacException(
+ "siemensHvac:Exception unable to get config because hvacBridgeBaseThingHandler is null");
+ }
+ }
+
+ @Override
+ public @Nullable SiemensHvacBridgeConfig getBridgeConfiguration() {
+ return config;
+ }
+
+ private void doAuth(boolean http) throws SiemensHvacException {
+ synchronized (this) {
+ logger.debug("siemensHvac:doAuth()");
+
+ initConfig();
+
+ SiemensHvacBridgeConfig config = this.config;
+ if (config == null) {
+ throw new SiemensHvacException("Missing SiemensHvacOZW Bridge configuration");
+ }
+
+ String baseUri = config.baseUrl;
+ String uri = "";
+
+ if (http) {
+ uri = "main.app";
+ } else {
+ uri = String.format("api/auth/login.json?user=%s&pwd=%s", config.userName, config.userPassword);
+ }
+
+ final Request request = httpClient.newRequest(baseUri + uri);
+ if (http) {
+ request.method(HttpMethod.POST).param("user", config.userName).param("pwd", config.userPassword);
+ } else {
+ request.method(HttpMethod.GET);
+ }
+
+ logger.debug("siemensHvac:doAuth:connect()");
+
+ ContentResponse response = executeRequest(request);
+ if (response != null) {
+ int statusCode = response.getStatus();
+
+ if (statusCode == HttpStatus.OK_200) {
+ String result = response.getContentAsString();
+
+ if (http) {
+ CookieStore cookieStore = httpClient.getCookieStore();
+ List cookies = cookieStore.getCookies();
+
+ for (HttpCookie httpCookie : cookies) {
+ if (httpCookie.getName().equals("SessionId")) {
+ sessionIdHttp = httpCookie.getValue();
+ }
+
+ }
+
+ if (sessionIdHttp == null) {
+ logger.debug("Session request auth was unsuccessful in _doAuth()");
+ }
+ } else {
+ if (result != null) {
+ JsonObject resultObj = getGson().fromJson(result, JsonObject.class);
+
+ if (resultObj != null && resultObj.has("Result")) {
+ JsonElement resultVal = resultObj.get("Result");
+ JsonObject resultObj2 = resultVal.getAsJsonObject();
+
+ if (resultObj2.has("Success")) {
+ boolean successVal = resultObj2.get("Success").getAsBoolean();
+
+ if (successVal) {
+ if (resultObj.has("SessionId")) {
+ sessionId = resultObj.get("SessionId").getAsString();
+ logger.debug("Have new SessionId: {} ", sessionId);
+ }
+ }
+ }
+ }
+
+ logger.debug("siemensHvac:doAuth:decodeResponse:()");
+
+ if (sessionId == null) {
+ throw new SiemensHvacException(
+ "Session request auth was unsuccessful in _doAuth(), please verify login parameters");
+ }
+ }
+
+ }
+ }
+ }
+
+ logger.trace("siemensHvac:doAuth:connect()");
+ }
+ }
+
+ @Override
+ public @Nullable String doBasicRequest(String uri) throws SiemensHvacException {
+ return doBasicRequest(uri, null);
+ }
+
+ public @Nullable String doBasicRequestAsync(String uri, @Nullable SiemensHvacCallback callback)
+ throws SiemensHvacException {
+ return doBasicRequest(uri, callback);
+ }
+
+ public @Nullable String doBasicRequest(String uri, @Nullable SiemensHvacCallback callback)
+ throws SiemensHvacException {
+ if (sessionIdHttp == null) {
+ doAuth(true);
+ }
+
+ if (sessionId == null) {
+ doAuth(false);
+ }
+
+ SiemensHvacBridgeConfig config = this.config;
+ if (config == null) {
+ throw new SiemensHvacException("Missing SiemensHvac OZW Bridge configuration");
+ }
+
+ String baseUri = config.baseUrl;
+
+ String mUri = uri;
+ if (!mUri.endsWith("?")) {
+ mUri = mUri + "&";
+ }
+ if (mUri.indexOf("main.app") >= 0) {
+ mUri = mUri + "SessionId=" + sessionIdHttp;
+ } else {
+ mUri = mUri + "SessionId=" + sessionId;
+ }
+
+ CookieStore c = httpClient.getCookieStore();
+ java.net.HttpCookie cookie = new HttpCookie("SessionId", sessionIdHttp);
+ cookie.setPath("/");
+ cookie.setVersion(0);
+
+ try {
+ c.add(new URI(baseUri), cookie);
+ } catch (URISyntaxException ex) {
+ throw new SiemensHvacException(String.format("URI is not correctly formatted: %s", baseUri), ex);
+ }
+
+ logger.debug("Execute request: {}", uri);
+ final Request request = httpClient.newRequest(baseUri + mUri);
+ request.method(HttpMethod.GET);
+
+ ContentResponse response = executeRequest(request, callback);
+ if (callback == null && response != null) {
+ int statusCode = response.getStatus();
+
+ if (statusCode == HttpStatus.OK_200) {
+ return response.getContentAsString();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public @Nullable JsonObject doRequest(String req) {
+ return doRequest(req, null);
+ }
+
+ @Override
+ public @Nullable JsonObject doRequest(String req, @Nullable SiemensHvacCallback callback) {
+ try {
+ String response = doBasicRequest(req, callback);
+
+ if (response != null) {
+ JsonObject resultObj = getGson().fromJson(response, JsonObject.class);
+
+ if (resultObj != null && resultObj.has("Result")) {
+ JsonObject subResultObj = resultObj.getAsJsonObject("Result");
+
+ if (subResultObj.has("Success")) {
+ boolean result = subResultObj.get("Success").getAsBoolean();
+ if (result) {
+ return resultObj;
+ }
+ }
+
+ }
+
+ return null;
+ }
+ } catch (SiemensHvacException e) {
+ logger.warn("siemensHvac:DoRequest:Exception by executing jsonRequest: {} ; {} ", req,
+ e.getLocalizedMessage());
+ }
+
+ return null;
+ }
+
+ @Override
+ public void displayRequestStats() {
+ logger.debug("DisplayRequestStats: ");
+ logger.debug(" currentRuning : {}", getCurrentHandlerRegistryCount());
+ logger.debug(" errors : {}", getHandlerInErrorRegistryCount());
+ }
+
+ @Override
+ public void waitAllPendingRequest() {
+ logger.debug("WaitAllPendingRequest:start");
+ try {
+ boolean allRequestDone = false;
+ int idx = 0;
+
+ while (!allRequestDone) {
+ allRequestDone = false;
+ int currentRequestCount = getCurrentHandlerRegistryCount();
+
+ logger.debug("WaitAllPendingRequest:waitAllRequestDone {}: {}", idx, currentRequestCount);
+
+ if (currentRequestCount == 0) {
+ allRequestDone = true;
+ }
+ Thread.sleep(1000);
+
+ if ((idx % 50) == 0) {
+ checkStaleRequest();
+ }
+ idx++;
+ }
+ } catch (InterruptedException ex) {
+ logger.debug("WaitAllPendingRequest:interrupted in WaitAllRequest");
+ }
+
+ logger.debug("WaitAllPendingRequest:end WaitAllPendingRequest");
+ }
+
+ public void checkStaleRequest() {
+ synchronized (currentHandlerRegistry) {
+ logger.debug("check stale request::begin");
+ int staleRequest = 0;
+
+ for (SiemensHvacRequestHandler handler : currentHandlerRegistry.keySet()) {
+ long elapseTime = handler.getElapsedTime();
+ if (elapseTime > 150) {
+ String uri = "";
+ Request request = handler.getRequest();
+ if (request != null) {
+ uri = request.getURI().toString();
+ }
+ logger.debug("find stale request: {} {}", elapseTime, anominized(uri));
+ staleRequest++;
+
+ try {
+ unregisterRequestHandler(handler);
+ registerHandlerError(handler);
+ } catch (SiemensHvacException ex) {
+ logger.debug("error unregistring handler: {}", handler);
+ }
+
+ }
+ }
+
+ logger.debug("check stale request::end: {}", staleRequest);
+ }
+ }
+
+ public String anominized(String uri) {
+ int p0 = uri.indexOf("pwd=");
+ if (p0 > 0) {
+ return uri.substring(0, p0) + "pwd=xxxxx";
+ }
+
+ return uri;
+ }
+
+ private int getCurrentHandlerRegistryCount() {
+ synchronized (currentHandlerRegistry) {
+ return currentHandlerRegistry.keySet().size();
+ }
+ }
+
+ private int getHandlerInErrorRegistryCount() {
+ synchronized (handlerInErrorRegistry) {
+ return handlerInErrorRegistry.keySet().size();
+ }
+ }
+
+ @Override
+ public void waitNoNewRequest() {
+ logger.debug("WaitNoNewRequest:start");
+ try {
+ int lastRequestCount = getCurrentHandlerRegistryCount();
+ boolean newRequest = true;
+ while (newRequest) {
+ Thread.sleep(5000);
+ int newRequestCount = getCurrentHandlerRegistryCount();
+ if (newRequestCount != lastRequestCount) {
+ logger.debug("waitNoNewRequest {}/{})", newRequestCount, lastRequestCount);
+ lastRequestCount = newRequestCount;
+ } else {
+ newRequest = false;
+ }
+ }
+ } catch (InterruptedException ex) {
+ logger.debug("WaitAllPendingRequest:interrupted in WaitAllRequest");
+ }
+
+ logger.debug("WaitNoNewRequest:end WaitAllStartingRequest");
+ }
+
+ @Override
+ public Gson getGson() {
+ return gson;
+ }
+
+ @Override
+ public Gson getGsonWithAdapter() {
+ return gsonWithAdapter;
+ }
+
+ public void addDpUpdate(String itemName, Type dp) {
+ synchronized (updateCommand) {
+ updateCommand.put(itemName, dp);
+ }
+ }
+
+ @Override
+ public void resetSessionId(@Nullable String sessionIdToInvalidate, boolean web) {
+ if (web) {
+ if (sessionIdToInvalidate == null) {
+ sessionIdHttp = null;
+ } else {
+ if (!oldSessionId.containsKey(sessionIdToInvalidate) && sessionIdToInvalidate.equals(sessionIdHttp)) {
+ oldSessionId.put(sessionIdToInvalidate, true);
+
+ logger.debug("Invalidate sessionIdHttp: {}", sessionIdToInvalidate);
+ sessionIdHttp = null;
+ }
+ }
+ } else {
+ if (sessionIdToInvalidate == null) {
+ sessionId = null;
+ } else {
+ if (!oldSessionId.containsKey(sessionIdToInvalidate) && sessionIdToInvalidate.equals(sessionId)) {
+ oldSessionId.put(sessionIdToInvalidate, true);
+
+ logger.debug("Invalidate sessionId: {}", sessionIdToInvalidate);
+ sessionId = null;
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getRequestCount() {
+ return requestCount;
+ }
+
+ @Override
+ public int getErrorCount() {
+ return errorCount;
+ }
+
+ @Override
+ public SiemensHvacRequestListener.ErrorSource getErrorSource() {
+ return errorSource;
+ }
+
+ @Override
+ public void invalidate() {
+ sessionId = null;
+ sessionIdHttp = null;
+
+ synchronized (currentHandlerRegistry) {
+ currentHandlerRegistry.clear();
+ handlerInErrorRegistry.clear();
+ }
+ }
+
+ @Override
+ public void setTimeOut(int timeout) {
+ this.timeout = timeout;
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestHandler.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestHandler.java
new file mode 100644
index 00000000000..6c14de97aef
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestHandler.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.network;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+
+/**
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacRequestHandler {
+ private SiemensHvacConnector hvacConnector;
+
+ private int retryCount = 0;
+
+ @Nullable
+ private Request request = null;
+
+ @Nullable
+ private Response response = null;
+
+ @Nullable
+ private Result result = null;
+
+ private Instant startRequest;
+
+ /**
+ * Callback to execute on complete response
+ */
+ private final SiemensHvacCallback callback;
+
+ /**
+ * Constructor
+ *
+ * @param callback Callback which execute method has to be called.
+ */
+ public SiemensHvacRequestHandler(SiemensHvacCallback callback, SiemensHvacConnector hvacConnector) {
+ this.callback = callback;
+ this.hvacConnector = hvacConnector;
+ startRequest = Instant.now();
+ }
+
+ public SiemensHvacConnector getHvacConnector() {
+ return hvacConnector;
+ }
+
+ public SiemensHvacCallback getCallback() {
+ return callback;
+ }
+
+ public void incrementRetryCount() {
+ retryCount++;
+ }
+
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ @Nullable
+ public Response getResponse() {
+ return response;
+ }
+
+ @Nullable
+ public Request getRequest() {
+ return request;
+ }
+
+ @Nullable
+ public Result getResult() {
+ return result;
+ }
+
+ public void setResponse(@Nullable Response response) {
+ this.response = response;
+ }
+
+ public void setRequest(@Nullable Request request) {
+ this.request = request;
+ }
+
+ public void setResult(@Nullable Result result) {
+ this.result = result;
+ }
+
+ public long getElapsedTime() {
+ Instant finish = Instant.now();
+ return Duration.between(startRequest, finish).toSeconds();
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestListener.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestListener.java
new file mode 100644
index 00000000000..770eb43ada1
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestListener.java
@@ -0,0 +1,247 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.network;
+
+import java.io.EOFException;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Request.BeginListener;
+import org.eclipse.jetty.client.api.Request.QueuedListener;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Response.CompleteListener;
+import org.eclipse.jetty.client.api.Response.ContentListener;
+import org.eclipse.jetty.client.api.Response.FailureListener;
+import org.eclipse.jetty.client.api.Response.SuccessListener;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacRequestListener extends BufferingResponseListener
+ implements SuccessListener, FailureListener, ContentListener, CompleteListener, QueuedListener, BeginListener {
+
+ public enum ErrorSource {
+ ErrorBridge,
+ ErrorThings
+ }
+
+ private static int onSuccessCount = 0;
+ private static int onBeginCount = 0;
+ private static int onQueuedCount = 0;
+ private static int onCompleteCount = 0;
+ private static int onFailureCount = 0;
+
+ private final Logger logger = LoggerFactory.getLogger(SiemensHvacRequestListener.class);
+
+ private SiemensHvacRequestHandler requestHandler;
+ private SiemensHvacConnector hvacConnector;
+
+ /**
+ * Callback to execute on complete response
+ */
+ private final SiemensHvacCallback callback;
+
+ public static int getQueuedCount() {
+ return onQueuedCount;
+ }
+
+ public static int getStartedCount() {
+ return onBeginCount;
+ }
+
+ public static int getCompleteCount() {
+ return onCompleteCount;
+ }
+
+ public static int getFailureCount() {
+ return onFailureCount;
+ }
+
+ public static int getSuccessCount() {
+ return onSuccessCount;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param callback Callback which execute method has to be called.
+ */
+ public SiemensHvacRequestListener(SiemensHvacRequestHandler requestHandler) {
+ this.requestHandler = requestHandler;
+ this.hvacConnector = requestHandler.getHvacConnector();
+ this.callback = requestHandler.getCallback();
+ }
+
+ @Override
+ public void onSuccess(@Nullable Response response) {
+ onSuccessCount++;
+ requestHandler.setResponse(response);
+
+ if (response != null) {
+ logger.debug("{} response: {}", response.getRequest().getURI(), response.getStatus());
+ }
+ }
+
+ @Override
+ public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
+ onFailureCount++;
+ requestHandler.setResponse(response);
+
+ if (response != null && failure != null) {
+ Throwable cause = failure.getCause();
+ if (cause == null) {
+ cause = failure;
+ }
+
+ String msg = cause.getLocalizedMessage();
+
+ if (cause instanceof ConnectException e) {
+ logger.debug("ConnectException during request: {} {}", response.getRequest().getURI(), msg, e);
+ } else if (cause instanceof SocketException e) {
+ logger.debug("SocketException during request: {} {}", response.getRequest().getURI(), msg, e);
+ } else if (cause instanceof SocketTimeoutException e) {
+ logger.debug("SocketTimeoutException during request: {} {}", response.getRequest().getURI(), msg, e);
+ } else if (cause instanceof EOFException e) {
+ logger.debug("EOFException during request: {} {}", response.getRequest().getURI(), msg, e);
+ } else if (cause instanceof TimeoutException e) {
+ logger.debug("TimeoutException during request: {} {}", response.getRequest().getURI(), msg, e);
+ } else {
+ logger.debug("Response failed: {} {}", response.getRequest().getURI(), msg, failure);
+ }
+ }
+ }
+
+ @Override
+ public void onQueued(@Nullable Request request) {
+ onQueuedCount++;
+ requestHandler.setRequest(request);
+ }
+
+ @Override
+ public void onBegin(@Nullable Request request) {
+ onBeginCount++;
+ requestHandler.setRequest(request);
+ }
+
+ @Override
+ public void onComplete(@Nullable Result result) {
+ onCompleteCount++;
+ requestHandler.setResult(result);
+
+ if (result == null) {
+ return;
+ }
+
+ try {
+ String content = getContentAsString();
+ logger.trace("response complete: {}", content);
+ boolean mayRetry = true;
+
+ if (result.getResponse().getStatus() != 200) {
+ logger.debug("Error requesting gateway, non success code: {}", result.getResponse().getStatus());
+ hvacConnector.onError(result.getRequest(), requestHandler, ErrorSource.ErrorBridge, mayRetry);
+ return;
+ }
+
+ if (content != null) {
+ if (content.indexOf("") >= 0) {
+ hvacConnector.onComplete(result.getRequest(), requestHandler);
+ callback.execute(result.getRequest().getURI(), result.getResponse().getStatus(), content);
+ } else {
+ JsonObject resultObj = null;
+ try {
+ Gson gson = hvacConnector.getGson();
+ resultObj = gson.fromJson(content, JsonObject.class);
+ } catch (JsonSyntaxException ex) {
+ logger.debug("error(1): {}", ex.toString());
+ }
+
+ if (resultObj != null && resultObj.has("Result")) {
+ JsonObject subResultObj = resultObj.getAsJsonObject("Result");
+
+ if (subResultObj.has("Success")) {
+ boolean resultVal = subResultObj.get("Success").getAsBoolean();
+ JsonObject error = subResultObj.getAsJsonObject("Error");
+ String errorMsg = "";
+ if (error != null) {
+ errorMsg = error.get("Txt").getAsString();
+ }
+
+ if (errorMsg.indexOf("session") >= 0) {
+ String query = result.getRequest().getURI().getQuery();
+ String sessionId = SiemensHvacConnectorImpl.extractSessionId(query);
+
+ hvacConnector.resetSessionId(sessionId, false);
+ hvacConnector.resetSessionId(sessionId, true);
+ mayRetry = false;
+ }
+
+ if (resultVal) {
+ hvacConnector.onComplete(result.getRequest(), requestHandler);
+ callback.execute(result.getRequest().getURI(), result.getResponse().getStatus(),
+ resultObj);
+
+ return;
+ } else if (("datatype not supported").equals(errorMsg)) {
+ hvacConnector.onComplete(result.getRequest(), requestHandler);
+ callback.execute(result.getRequest().getURI(), result.getResponse().getStatus(),
+ resultObj);
+ return;
+ } else if (("read failed").equals(errorMsg)) {
+ logger.debug("error(2): {}", subResultObj);
+ hvacConnector.onError(result.getRequest(), requestHandler, ErrorSource.ErrorThings,
+ mayRetry);
+ } else {
+ logger.debug("error(3): {}", subResultObj);
+ hvacConnector.onError(result.getRequest(), requestHandler, ErrorSource.ErrorBridge,
+ mayRetry);
+ return;
+ }
+ } else {
+ logger.debug("error(4): invalid response from gateway, missing subResultObj:Success entry");
+ hvacConnector.onError(result.getRequest(), requestHandler, ErrorSource.ErrorBridge,
+ mayRetry);
+ return;
+ }
+
+ } else {
+ logger.debug("error(5): invalid response from gateway, missing Result entry");
+ hvacConnector.onError(result.getRequest(), requestHandler, ErrorSource.ErrorBridge, mayRetry);
+ return;
+ }
+ }
+ } else {
+ logger.debug("error: content == null");
+ hvacConnector.onError(result.getRequest(), requestHandler, ErrorSource.ErrorBridge, mayRetry);
+ return;
+ }
+ } catch (SiemensHvacException ex) {
+ logger.debug("An error occurred", ex);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProvider.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProvider.java
new file mode 100644
index 00000000000..a6489bcc134
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProvider.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.type.ChannelGroupType;
+import org.openhab.core.thing.type.ChannelGroupTypeProvider;
+import org.openhab.core.thing.type.ChannelGroupTypeUID;
+
+/**
+ * Extends the ChannelGroupTypeProvider to manually add a ChannelGroupType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacChannelGroupTypeProvider extends ChannelGroupTypeProvider {
+
+ /**
+ * Adds the ChannelGroupType to this provider.
+ */
+ void addChannelGroupType(ChannelGroupType channelGroupType);
+
+ /**
+ * Use this method to lookup a ChannelGroupType which was generated by the siemensHvac binding.
+ */
+ @Nullable
+ ChannelGroupType getInternalChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID);
+
+ void invalidate();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProviderImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProviderImpl.java
new file mode 100644
index 00000000000..364a631199e
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProviderImpl.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.type.ChannelGroupType;
+import org.openhab.core.thing.type.ChannelGroupTypeProvider;
+import org.openhab.core.thing.type.ChannelGroupTypeUID;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Provides all ChannelGroupTypes from all SiemensHvac bridges.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { SiemensHvacChannelGroupTypeProvider.class, ChannelGroupTypeProvider.class })
+public class SiemensHvacChannelGroupTypeProviderImpl implements SiemensHvacChannelGroupTypeProvider {
+
+ private final Map channelGroupTypesByUID = new HashMap<>();
+
+ @Override
+ public @Nullable ChannelGroupType getInternalChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID) {
+ return channelGroupTypesByUID.get(channelGroupTypeUID);
+ }
+
+ @Override
+ public void addChannelGroupType(ChannelGroupType channelGroupType) {
+ channelGroupTypesByUID.put(channelGroupType.getUID(), channelGroupType);
+ }
+
+ @Override
+ public @Nullable ChannelGroupType getChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID,
+ @Nullable Locale locale) {
+ return channelGroupTypesByUID.get(channelGroupTypeUID);
+ }
+
+ /**
+ *
+ * @see ChannelTypeRegistr#getChannelGroupTypes(Locale)
+ *
+ */
+ @Override
+ public Collection getChannelGroupTypes(@Nullable Locale locale) {
+ Collection result = new ArrayList<>();
+ for (ChannelGroupTypeUID uid : channelGroupTypesByUID.keySet()) {
+ ChannelGroupType groupType = channelGroupTypesByUID.get(uid);
+ if (groupType != null) {
+ result.add(groupType);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void invalidate() {
+ channelGroupTypesByUID.clear();
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProvider.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProvider.java
new file mode 100644
index 00000000000..6f5e16e9dc4
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProvider.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeProvider;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * Extends the ChannelTypeProvider to manually add a ChannelType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacChannelTypeProvider extends ChannelTypeProvider {
+
+ /**
+ * Adds the ChannelType to this provider.
+ */
+ void addChannelType(ChannelType channelType);
+
+ /**
+ * Use this method to lookup a ChannelType which was generated by the siemensHvac binding.
+ *
+ * @param channelTypeUID
+ * @return ChannelType that was added to SiemensHvacChannelTypeProvider, identified by its
+ * config-description-uri
+ * null if no ChannelType with the given UID was added
+ * before
+ */
+ @Nullable
+ ChannelType getInternalChannelType(@Nullable ChannelTypeUID channelTypeUID);
+
+ void invalidate();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProviderImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProviderImpl.java
new file mode 100644
index 00000000000..234bd4acfe9
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProviderImpl.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeProvider;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Provides all ChannelTypes from SiemensHvac bridges.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { SiemensHvacChannelTypeProvider.class, ChannelTypeProvider.class })
+public class SiemensHvacChannelTypeProviderImpl implements SiemensHvacChannelTypeProvider {
+ private final Map channelTypesByUID = new HashMap<>();
+
+ public SiemensHvacChannelTypeProviderImpl() {
+ }
+
+ @Override
+ public void addChannelType(ChannelType channelType) {
+ channelTypesByUID.put(channelType.getUID(), channelType);
+ }
+
+ @Override
+ public Collection getChannelTypes(@Nullable Locale locale) {
+ Collection result = new ArrayList<>();
+
+ for (ChannelTypeUID uid : channelTypesByUID.keySet()) {
+ ChannelType tp = channelTypesByUID.get(uid);
+ if (tp != null) {
+ result.add(tp);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @see ChannelTypeRegistr#getChannelType(ChannelTypeUID, Locale)
+ */
+ @Override
+ public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
+ return channelTypesByUID.get(channelTypeUID);
+ }
+
+ @Override
+ public @Nullable ChannelType getInternalChannelType(@Nullable ChannelTypeUID channelTypeUID) {
+ return channelTypesByUID.get(channelTypeUID);
+ }
+
+ @Override
+ public void invalidate() {
+ channelTypesByUID.clear();
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProvider.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProvider.java
new file mode 100644
index 00000000000..a16ec5f92d4
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProvider.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import java.net.URI;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.ConfigDescription;
+import org.openhab.core.config.core.ConfigDescriptionProvider;
+
+/**
+ * Extends the ConfigDescriptionProvider to manually add a ConfigDescription.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacConfigDescriptionProvider extends ConfigDescriptionProvider {
+
+ /**
+ * Adds the ConfigDescription to this provider.
+ */
+ void addConfigDescription(ConfigDescription configDescription);
+
+ /**
+ * Provides a {@link ConfigDescription} for the given URI.
+ *
+ * @param uri uri of the config description
+ * @param locale locale
+ *
+ * @return config description or null if no config description could be found
+ */
+ @Override
+ @Nullable
+ ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale);
+
+ /**
+ * Use this method to lookup a ConfigDescription which was generated by the
+ * siemenshvac binding.
+ *
+ */
+ @Nullable
+ ConfigDescription getInternalConfigDescription(URI uri);
+
+ void invalidate();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProviderImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProviderImpl.java
new file mode 100644
index 00000000000..79cfb5a31eb
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProviderImpl.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.ConfigDescription;
+import org.openhab.core.config.core.ConfigDescriptionProvider;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { SiemensHvacConfigDescriptionProvider.class, ConfigDescriptionProvider.class })
+public class SiemensHvacConfigDescriptionProviderImpl implements SiemensHvacConfigDescriptionProvider {
+ private Map configDescriptionsByURI = new HashMap<>();
+
+ @Override
+ public Collection getConfigDescriptions(@Nullable Locale locale) {
+ Collection result = new ArrayList<>();
+ for (URI configDescriptionURI : configDescriptionsByURI.keySet()) {
+ ConfigDescription desc = configDescriptionsByURI.get(configDescriptionURI);
+ if (desc != null) {
+ result.add(desc);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public @Nullable ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale) {
+ return configDescriptionsByURI.get(uri);
+ }
+
+ @Nullable
+ @Override
+ public ConfigDescription getInternalConfigDescription(URI uri) {
+ return configDescriptionsByURI.get(uri);
+ }
+
+ @Override
+ public void addConfigDescription(ConfigDescription configDescription) {
+ configDescriptionsByURI.put(configDescription.getUID(), configDescription);
+ }
+
+ @Override
+ public void invalidate() {
+ configDescriptionsByURI.clear();
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacException.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacException.java
new file mode 100644
index 00000000000..080fc350487
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacException.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * An exception that occurred while operating the binding
+ *
+ * @author Laurent Arnal - Initial contribution
+ *
+ */
+
+@NonNullByDefault
+public class SiemensHvacException extends Exception {
+ private static final long serialVersionUID = -3398100220952729816L;
+
+ public SiemensHvacException(String message, Exception e) {
+ super(message, e);
+ }
+
+ public SiemensHvacException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProvider.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProvider.java
new file mode 100644
index 00000000000..5fac0e0e78a
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProvider.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.ThingTypeProvider;
+import org.openhab.core.thing.type.ThingType;
+
+/**
+ * Extends the ThingTypeProvider to manually add a ThingType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacThingTypeProvider extends ThingTypeProvider {
+
+ /**
+ * Adds the ThingType to this provider.
+ */
+ void addThingType(ThingType thingType);
+
+ /**
+ * Use this method to lookup a ThingType which was generated by the
+ * binding. Other than {@link #getThingType(ThingTypeUID)}
+ * of this provider, it will return also those {@link ThingType}s which are
+ * excluded by {@link ThingTypeExcluder}
+ *
+ * @param thingTypeUID
+ * @return ThingType that was added to SiemensHvacThingTypeProvider, identified
+ * by its thingTypeUID
+ * null if no ThingType with the given thingTypeUID was added
+ * before
+ */
+ @Nullable
+ ThingType getInternalThingType(ThingTypeUID thingTypeUID);
+
+ void invalidate();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProviderImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProviderImpl.java
new file mode 100644
index 00000000000..b8895e74efc
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProviderImpl.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.ThingTypeProvider;
+import org.openhab.core.thing.type.ThingType;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Provides all ThingTypes from SiemensHvac bridges.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { SiemensHvacThingTypeProvider.class, ThingTypeProvider.class }, immediate = true)
+public class SiemensHvacThingTypeProviderImpl implements SiemensHvacThingTypeProvider {
+
+ private Map thingTypesByUID = new HashMap<>();
+
+ public SiemensHvacThingTypeProviderImpl() {
+ }
+
+ @Override
+ public void addThingType(ThingType thingType) {
+ thingTypesByUID.put(thingType.getUID(), thingType);
+ }
+
+ @Override
+ public @Nullable ThingType getInternalThingType(ThingTypeUID thingTypeUID) {
+ return thingTypesByUID.get(thingTypeUID);
+ }
+
+ @Override
+ public Collection getThingTypes(@Nullable Locale locale) {
+ Map copy = new HashMap<>(thingTypesByUID);
+ return copy.values();
+ }
+
+ @Override
+ public @Nullable ThingType getThingType(ThingTypeUID thingTypeUID, @Nullable Locale locale) {
+ return thingTypesByUID.get(thingTypeUID);
+ }
+
+ @Override
+ public void invalidate() {
+ thingTypesByUID.clear();
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/UidUtils.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/UidUtils.java
new file mode 100644
index 00000000000..c7c994c42e2
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/UidUtils.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import java.text.Normalizer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.siemenshvac.internal.constants.SiemensHvacBindingConstants;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterFactory;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterTypeException;
+import org.openhab.binding.siemenshvac.internal.converter.TypeConverter;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDevice;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataMenu;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelGroupTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * Utility class for generating some UIDs.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class UidUtils {
+ /**
+ * The methods remove specific local character (like 'é'/'ê','â') so we have a correctly formated UID from a
+ * localize item label
+ *
+ * @param label
+ * @return the label without invalid character
+ */
+ public static String sanetizeId(String label) {
+ String result = label;
+
+ if (!Normalizer.isNormalized(label, Normalizer.Form.NFKD)) {
+ result = Normalizer.normalize(label, Normalizer.Form.NFKD);
+ result = result.replaceAll("\\p{M}", "");
+ }
+
+ result = result.replaceAll("[^a-zA-Z0-9_]", "-").toLowerCase();
+
+ return result;
+ }
+
+ /**
+ * Generates the ThingTypeUID for the given device. If it's a Homegear device, add a prefix because a Homegear
+ * device has more datapoints.
+ */
+ public static ThingTypeUID generateThingTypeUID(SiemensHvacMetadataDevice device) {
+ String type = sanetizeId(device.getType());
+ return new ThingTypeUID(SiemensHvacBindingConstants.BINDING_ID, type);
+ }
+
+ /**
+ * get a more user friendly description from English short descriptor
+ *
+ * @param descriptor
+ * @return
+ */
+ private static String normalizeDescriptor(String descriptor) {
+ String result = descriptor.trim();
+
+ if (result.indexOf("CC") >= 0 || result.indexOf("HC") >= 0) {
+ for (int idx = 0; idx < 4; idx++) {
+ result = result.replace("CC" + idx, "CC");
+ result = result.replace("HC" + idx, "HC");
+ }
+ }
+
+ result = result.toLowerCase();
+
+ if (result.indexOf("history") >= 0) {
+ for (int idx = 0; idx < 20; idx++) {
+ result = result.replace("history " + idx, "history");
+ }
+ }
+
+ result = result.replace(" mon", "");
+ result = result.replace(" tue", "");
+ result = result.replace(" wed", "");
+ result = result.replace(" thu", "");
+ result = result.replace(" fri", "");
+ result = result.replace(" sat", "");
+ result = result.replace(" sun", "");
+ result = result.replace(" mo", "");
+ result = result.replace(" tu", "");
+ result = result.replace(" we", "");
+ result = result.replace(" th", "");
+ result = result.replace(" fr", "");
+ result = result.replace(" sa", "");
+ result = result.replace(" su", "");
+
+ if (result.indexOf("holidays") >= 0) {
+ if (result.indexOf("firstd") >= 0) {
+ result = "holidays-hc-firstd";
+ }
+ if (result.indexOf("lastd") >= 0) {
+ result = "holidays-hc-lastd";
+ }
+ }
+
+ result = result.replace("---", "-");
+ result = result.replace("--", "-");
+ result = result.replace('\'', '-');
+ result = result.replace('/', '-');
+ result = result.replace(' ', '-');
+ result = result.replace("+", "-");
+
+ result = result.replace("standard-tsp-hc", "time-switch-program-standard");
+ result = result.replace("standard-tsp-4", "time-switch-program-standard");
+ result = result.replace("tsp-3", "time-switch-program-day");
+ result = result.replace("tsp-4", "time-switch-program-day");
+ result = result.replace("setpointtemp", "setpoint-temp-");
+ result = result.replace("rmtmp", "roomtemp");
+ result = result.replace("roomtempfrostprot", "room-temp-frostprot-");
+ result = result.replace("-setp", "-setpoint");
+ result = result.replace("optg", "operating-");
+ result = result.replace("-comf", "-comfort");
+ result = result.replace("-red", "-reduce");
+ result = result.replace("setp-", "-setpoint");
+ result = result.replace("roomtemp-", "room-temp-");
+ result = result.replace("-setpointhc", "-setpoint-hc");
+ result = result.replace("setphc", "-setpoint-hc");
+
+ return result;
+ }
+
+ /**
+ * Generates the ChannelTypeUID for the given datapoint with deviceType, channelNumber and datapointName.
+ */
+ public static ChannelTypeUID generateChannelTypeUID(SiemensHvacMetadataDataPoint dpt) throws SiemensHvacException {
+ String type = dpt.getDptType();
+ String shortDesc = dpt.getShortDescEn();
+ String result = normalizeDescriptor(shortDesc);
+
+ try {
+ TypeConverter tp = ConverterFactory.getConverter(type);
+ if (!tp.hasVariant()) {
+ result = tp.getChannelType(dpt);
+ }
+ } catch (ConverterTypeException ex) {
+ throw new SiemensHvacException(String.format("Can't find converter for type: %s", type), ex);
+ }
+
+ return new ChannelTypeUID(SiemensHvacBindingConstants.BINDING_ID, result);
+ }
+
+ /**
+ * Generates the ChannelTypeUID for the given datapoint with deviceType and channelNumber.
+ */
+ public static ChannelGroupTypeUID generateChannelGroupTypeUID(SiemensHvacMetadataMenu menu) {
+ return new ChannelGroupTypeUID(SiemensHvacBindingConstants.BINDING_ID, String.valueOf(menu.getId()));
+ }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 00000000000..e0f15531231
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,24 @@
+
+
+ binding
+ SiemensHvac Binding
+ This is the binding for SiemensHvac.
+ local
+
+
+ upnp
+
+
+ manufacturer
+ Siemens.*
+
+
+ modelName
+ Web Server OZW.*
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/i18n/siemenshvac.properties b/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/i18n/siemenshvac.properties
new file mode 100644
index 00000000000..3703557f28d
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/i18n/siemenshvac.properties
@@ -0,0 +1,26 @@
+# add-on
+
+addon.siemenshvac.name = SiemensHvac Binding
+addon.siemenshvac.description = This is the binding for SiemensHvac.
+
+# thing types
+
+thing-type.siemenshvac.ozw.label = OZW IP Gateway
+thing-type.siemenshvac.ozw.description = This is a OZW IP interface
+
+# thing types config
+
+thing-type.config.siemenshvac.ozw.baseUrl.label = Base URL
+thing-type.config.siemenshvac.ozw.baseUrl.description = The URL of the Siemens Hvac IP gateway. Must be in format http://hostname/ or https://hostname/. Don't forget the trailing '/'
+thing-type.config.siemenshvac.ozw.userName.label = User Name
+thing-type.config.siemenshvac.ozw.userName.description = User name of the Siemens Hvac gateway
+thing-type.config.siemenshvac.ozw.userPassword.label = User Password
+thing-type.config.siemenshvac.ozw.userPassword.description = User password of the Siemens Hvac gateway
+
+# offline message
+
+offline.baseurl-mandatory = baseUrl is mandatory on configuration.
+offline.error-gateway-init = Error occurred during gateway initialization [{0}]
+offline.config-not-init = Config not initialize during reading metadata, aborting.
+offline.user-not-find = Cannot find user during reading metadata, aborting.
+offline.waiting-bridge-initialization = Waiting bridge initialization, reading metadata in background
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/thing/ozw.xml b/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/thing/ozw.xml
new file mode 100644
index 00000000000..7b43f25f1aa
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/thing/ozw.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ OZW IP Gateway
+ This is a OZW IP interface
+
+
+
+ Base URL
+ url
+ The URL of the Siemens Hvac IP gateway. Must be in format http://hostname/ or https://hostname/. Don't
+ forget the trailing '/'
+ true
+
+
+ User name of the Siemens Hvac gateway
+ false
+ User Name
+ Administrator
+
+
+ password
+ User password of the Siemens Hvac gateway
+ false
+ User Password
+ password
+
+
+
+
diff --git a/bundles/org.openhab.binding.siemenshvac/src/test/java/org/openhab/binding/siemenshvac/internal/type/UidUtilsTest.java b/bundles/org.openhab.binding.siemenshvac/src/test/java/org/openhab/binding/siemenshvac/internal/type/UidUtilsTest.java
new file mode 100644
index 00000000000..b94428d4fe0
--- /dev/null
+++ b/bundles/org.openhab.binding.siemenshvac/src/test/java/org/openhab/binding/siemenshvac/internal/type/UidUtilsTest.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.type;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class UidUtilsTest {
+
+ @Test
+ public void testSanetizeId() throws Exception {
+ assertEquals(UidUtils.sanetizeId("Début heure été"), "debut-heure-ete");
+ assertEquals(UidUtils.sanetizeId("App.Ambiance 1"), "app-ambiance-1");
+ assertEquals(UidUtils.sanetizeId("Appareil d'ambiance P"), "appareil-d-ambiance-p");
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index b51354c2c24..feae03ff21f 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -362,6 +362,7 @@
org.openhab.binding.serialbutton
org.openhab.binding.shelly
org.openhab.binding.silvercrestwifisocket
+ org.openhab.binding.siemenshvac
org.openhab.binding.siemensrds
org.openhab.binding.sinope
org.openhab.binding.sleepiq