]> git.basschouten.com Git - openhab-addons.git/commitdiff
[siemenshvac] Initial contribution (#14263)
authorlo92fr <laurent@clae.net>
Sat, 29 Jun 2024 10:25:00 +0000 (12:25 +0200)
committerGitHub <noreply@github.com>
Sat, 29 Jun 2024 10:25:00 +0000 (12:25 +0200)
Signed-off-by: Laurent ARNAL <laurent@clae.net>
58 files changed:
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.siemenshvac/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/README.md [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/doc/Albatros.jpg [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/doc/Diagram.png [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/3rdparty/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/constants/SiemensHvacBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterException.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterTypeException.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/TypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/AbstractTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/CalendarTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/CheckboxTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/DateTimeTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/EnumTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/NumericTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/RadioTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/SchedulerTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/StringTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/TimeOfDayTypeConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/discovery/SiemenesHvacDiscoveryParticipant.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/discovery/SiemensHvacDeviceDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/factory/SiemensHvacHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacBridgeConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacBridgeThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacHandlerImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadata.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataDataPoint.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataLanguage.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataMenu.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataPointChild.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataRegistry.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataRegistryImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataUser.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacConnectorImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/network/SiemensHvacRequestListener.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelGroupTypeProviderImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacChannelTypeProviderImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacConfigDescriptionProviderImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacException.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/SiemensHvacThingTypeProviderImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/type/UidUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/i18n/siemenshvac.properties [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/main/resources/OH-INF/thing/ozw.xml [new file with mode: 0644]
bundles/org.openhab.binding.siemenshvac/src/test/java/org/openhab/binding/siemenshvac/internal/type/UidUtilsTest.java [new file with mode: 0644]
bundles/pom.xml

index 631c9f7a82f75c7eea06f6409c42a951ac682b61..bc27b7d2f8f5e7da67dea23e810a10861ebde615 100644 (file)
       <artifactId>org.openhab.binding.shelly</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.siemenshvac</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.siemensrds</artifactId>
diff --git a/bundles/org.openhab.binding.siemenshvac/NOTICE b/bundles/org.openhab.binding.siemenshvac/NOTICE
new file mode 100644 (file)
index 0000000..451e1f0
--- /dev/null
@@ -0,0 +1,20 @@
+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
+
+== Third-party Content
+
+RuntimeTypeAdapterFactory
+* License: Apache License, Version 2.0 
+* Project: https://github.com/google/gson
+* Source:  https://github.com/google/gson/blob/main/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
diff --git a/bundles/org.openhab.binding.siemenshvac/README.md b/bundles/org.openhab.binding.siemenshvac/README.md
new file mode 100644 (file)
index 0000000..43bb340
--- /dev/null
@@ -0,0 +1,98 @@
+# SiemensHVAC Binding
+
+This binding provides support for the Siemens HVAC controller ecosystem, and the Web Gateway interface OZW672.
+A typical system is composed of:
+         
+![Diagram](doc/Diagram.png)                 
+There's a lot of different HVAC controllers depending on model in lot of different PAC constructors.
+Siemens RVS41.813/327 inside a Atlantic Hybrid Duo was used for the development, and is fully supported and tested.
+
+Siemens have a complete set of controller references under the name "Siemens Albatros".
+Here is a picture of such device.
+You can also find this device in other types of heating systems: boiler or solar based.
+
+![](doc/Albatros.jpg)
+
+You will find some information about the OZW672.01 gateway on the Siemens web site: 
+
+[OZW 672 Page](https://hit.sbt.siemens.com/RWD/app.aspx?rc=FR&lang=fr&module=Catalog&action=ShowProduct&key=BPZ:OZW672.01)
+
+With this binding, you will be able to:
+
+- Consult the different parameters of your system, like temperature, current heating mode, water temperature, and many more.
+- Modify the functioning mode of your device: temperature set point, heating mode, and others.
+
+The OZW672 gateway supports many different languages (about 16).
+The binding should work with all language choices, but is currently tested more thoroughly with French and English as configured language.  
+If you use another language, and find some issues, you can report them on the openHAB forum.
+
+## Discovery
+
+Discovery of Gateway can be done using UPnP.
+Just switch off/on your gateway to make it annonce itself on the network.
+The gateway should appear in the Inbox a few minutes after.
+Be aware what you will have to modify the password in Gateway parameters just after the discovery to make it work properly.
+Be also aware that first initialization is a little long because the binding needs to read all the metadata from the device.
+
+Currently test was done with the OZW672.x series.
+No test was conducted using the OZW772.x series, the code will currently not handle initialization of an OZW772 gateway.
+You can request support in the community forum, if you have the gateway model and want it to be supported.
+
+Discovery of HVAC device inside your PAC (controller of type RVS...) have to be done through the Scan button inside the binding.
+Go to the Thing page, click on the "+" button, select the SiemensHVAC binding, and then click Scan.
+Your device should appear on the page after a few seconds.
+Only test in real conditions with RVS41.813/327 have been done, but it should work with all other types as the API interface is standard.
+
+## Bridge Configuration
+
+Parameter       | Required       | Default        | Description
+----------------|----------------|----------------|------------------
+baseUrl         | yes            |                | The address of the OZW672 devices
+userName        | yes            | Administrator  | The user name to log into the OZW672
+userPass        | yes            |                | The user password to log into the OZW672
+
+## Channels
+
+Channels are auto-discovered, you will find them on the RVS things.
+They are organized the same way as the LCD screen of your PAC device, by top level menu functionality, and sub-functionalities.
+Each channel is strongly typed, so for example, for heating mode, openHAB will provide you with a list of choices supported by the device.
+
+Channel                         | Description                                                                                       | Type                          | Unit     | Security Access Level   |  ReadOnly | Advanced   
+--------------------------------|---------------------------------------------------------------------------------------------------|-------------------------------|----------|-------------------------|-----------|----------
+1724#1725-optgmode-hc1          | Set Operating mode heat circuit 1 (`Automatic`, `Comfort`, `Reduced`, `Protection`)               | operating-mode-hc             |          |                         |  R/W      | true
+1724#1726-roomtemp-comfsetp-hc1 | Romm temperature comfort setpoint HC1                                                             | room-temp-comfort-setpoint-hc |          |                         |  R/W      | true
+   
+## Full Example
+
+Things file `.things`
+
+```java
+Bridge siemenshvac:ozw:ozw672_FF00F445 "Ozw672" [ baseUrl="https://192.168.254.42/", userName="Administrator", userPassword="mypass"  ] 
+{
+    Thing rvs41-813-327 00770000756A "RVS41.813/327"  [  ]
+    {
+        Type room-temp-comfort-setpoint-hc : testChannelTemperature                  "TestChannelTemperature"  [ id="1726" ]
+        Type operating-mode-hc : testChannelCC1                                      "TestChannelCC1"          [ id="1725" ] 
+    }
+}    
+```
+
+
+Items file `.items`
+
+```java
+Contact             Boiler_State_Pump_HWSb      "HWS Pump State [%s]"                   { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2237#2259-ppechargeecs"              } 
+Number              Boiler_State_HWS            "HWS State [%s]"                        { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2032#2035-etat-ecs"                  }
+Number:Temperature  Flow_Temperature_Real       "Flow Temparature Real [%.1f °C]"       { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2237#2248-valreelletempdep-cc1"      }   
+Number:Temperature  Flow_Temperature_Setpoint   "Flow Temperature Setpoint [%.1f °C]"   { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2237#2249-constdepresultcc1"         }   
+Number              Hour_fct_HWS                "HWS Hour function"                     { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2237#2263-heuresfoncpompeecs"        }   
+Number              Nb_Start_HWS                "HWS Number of start [%.1f]"            { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2237#2266-comptdemarresel-ecs"       }
+Number:Temperature  Thermostat_Temperature      "Thermostat tempeature [%.1f °C]"       { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:2237#2246-tambact-cc1"               }
+Number:Temperature  Thermostat_Setpoint         "Thermostat setpoint [%.1f °C]"         { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:1724#1726-consconfort-ta-cc1"        }
+Number              Heat_Mode                   "Heat mode [%s]"                        { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:1724#1725-regime-cc1"                }
+
+Number:Temperature  Thermostat_Setpoint_bis     "Temperature [%.1f °C]"                 { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:testChannelTemperature "             }   
+Number              Heat_Mode_bis               "Heat mode [%s]"                        { channel = "siemenshvac:rvs41-813-327:ozw672_FF00F445:00770000756A:testChannelCC1"                      }
+
+``` 
diff --git a/bundles/org.openhab.binding.siemenshvac/doc/Albatros.jpg b/bundles/org.openhab.binding.siemenshvac/doc/Albatros.jpg
new file mode 100644 (file)
index 0000000..4fc7444
Binary files /dev/null and b/bundles/org.openhab.binding.siemenshvac/doc/Albatros.jpg differ
diff --git a/bundles/org.openhab.binding.siemenshvac/doc/Diagram.png b/bundles/org.openhab.binding.siemenshvac/doc/Diagram.png
new file mode 100644 (file)
index 0000000..300cc2f
Binary files /dev/null and b/bundles/org.openhab.binding.siemenshvac/doc/Diagram.png differ
diff --git a/bundles/org.openhab.binding.siemenshvac/pom.xml b/bundles/org.openhab.binding.siemenshvac/pom.xml
new file mode 100644 (file)
index 0000000..cae4551
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.siemenshvac</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: SiemensHvac Binding</name>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <sources>
+                <source>src/3rdparty/java</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/bundles/org.openhab.binding.siemenshvac/src/3rdparty/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/bundles/org.openhab.binding.siemenshvac/src/3rdparty/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
new file mode 100644 (file)
index 0000000..a716848
--- /dev/null
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copied from
+ * https://raw.githubusercontent.com/google/gson/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
+ * and repackaged here with additional content from
+ * com.google.gson.internal.{Streams,TypeAdapters,LazilyParsedNumber}
+ * to avoid using the internal package.
+ */
+package com.google.gson.typeadapters;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.ObjectStreamException;
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.stream.MalformedJsonException;
+
+/**
+ * Adapts values whose runtime type may differ from their declaration type. This
+ * is necessary when a field's type is not the same type that GSON should create
+ * when deserializing that field. For example, consider these types:
+ *
+ * <pre>
+ * {
+ *     &#64;code
+ *     abstract class Shape {
+ *         int x;
+ *         int y;
+ *     }
+ *     class Circle extends Shape { 
+ *         int radius;
+ *     }
+ *     class Rectangle extends Shape {
+ *         int width;
+ *         int height;
+ *     }
+ *     class Diamond extends Shape {
+ *         int width;
+ *         int height;
+ *     }
+ *     class Drawing {
+ *         Shape bottomShape;
+ *         Shape topShape;
+ *     }
+ * }
+ * </pre>
+ * <p>
+ * Without additional type information, the serialized JSON is ambiguous. Is
+ * the bottom shape in this drawing a rectangle or a diamond?
+ *
+ * <pre>
+ *    {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * </pre>
+ *
+ * This class addresses this problem by adding type information to the
+ * serialized JSON and honoring that type information when the JSON is
+ * deserialized:
+ *
+ * <pre>
+ *    {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * </pre>
+ *
+ * Both the type field name ({@code "type"}) and the type labels ({@code
+ * "Rectangle"}) are configurable.
+ *
+ * <h3>Registering Types</h3>
+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
+ * name to the {@link #of} factory method. If you don't supply an explicit type
+ * field name, {@code "type"} will be used.
+ *
+ * <pre>
+ * {
+ *     &#64;code
+ *     RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * </pre>
+ *
+ * Next register all of your subtypes. Every subtype must be explicitly
+ * registered. This protects your application from injection attacks. If you
+ * don't supply an explicit type label, the type's simple name will be used.
+ *
+ * <pre>
+ *    {@code
+ * shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapter.registerSubtype(Circle.class, "Circle");
+ * shapeAdapter.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * </pre>
+ *
+ * Finally, register the type adapter factory in your application's GSON builder:
+ *
+ * <pre>
+ * {
+ *     &#64;code
+ *     Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
+ * }
+ * </pre>
+ *
+ * Like {@code GsonBuilder}, this API supports chaining:
+ *
+ * <pre>
+ * {
+ *     &#64;code
+ *     RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *             .registerSubtype(Rectangle.class).registerSubtype(Circle.class).registerSubtype(Diamond.class);
+ * }
+ * </pre>
+ */
+/**
+ * @author Jesse Wilson (swankjesse) - Initial contribution
+ */
+@NonNullByDefault
+public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
+    private final Class<?> baseType;
+    private final String typeFieldName;
+    private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
+    private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
+
+    private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
+        this.baseType = baseType;
+        this.typeFieldName = typeFieldName;
+    }
+
+    /**
+     * Creates a new runtime type adapter using for {@code baseType} using {@code
+     * typeFieldName} as the type field name. Type field names are case sensitive.
+     */
+    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
+        return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName);
+    }
+
+    /**
+     * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
+     * the type field name.
+     */
+    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
+        return new RuntimeTypeAdapterFactory<T>(baseType, "type");
+    }
+
+    /**
+     * Registers {@code type} identified by {@code label}. Labels are case
+     * sensitive.
+     *
+     * @throws IllegalArgumentException if either {@code type} or {@code label}
+     *             have already been registered on this type adapter.
+     */
+    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
+        if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
+            throw new IllegalArgumentException("types and labels must be unique");
+        }
+        labelToSubtype.put(label, type);
+        subtypeToLabel.put(type, label);
+        return this;
+    }
+
+    /**
+     * Registers {@code type} identified by its {@link Class#getSimpleName simple
+     * name}. Labels are case sensitive.
+     *
+     * @throws IllegalArgumentException if either {@code type} or its simple name
+     *             have already been registered on this type adapter.
+     */
+    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
+        return registerSubtype(type, type.getSimpleName());
+    }
+
+    @Override
+    public @Nullable <R> TypeAdapter<R> create(@Nullable Gson gson, @Nullable TypeToken<R> type) {
+        if (type == null || type.getRawType() != baseType) {
+            return null;
+        }
+        if (gson == null) {
+            return null;
+        }
+
+        final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<String, TypeAdapter<?>>();
+        final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<Class<?>, TypeAdapter<?>>();
+        for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
+            TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
+            labelToDelegate.put(entry.getKey(), delegate);
+            subtypeToDelegate.put(entry.getValue(), delegate);
+        }
+
+        return new TypeAdapter<R>() {
+            @Override
+            public @Nullable R read(JsonReader in) throws IOException {
+                JsonElement jsonElement = RuntimeTypeAdapterFactory.parse(in);
+                if (jsonElement != null) {
+                    JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
+                    if (labelJsonElement == null) {
+                        throw new JsonParseException("cannot deserialize " + baseType
+                                + " because it does not define a field named " + typeFieldName);
+                    }
+                    String label = labelJsonElement.getAsString();
+                    @SuppressWarnings("unchecked") // registration requires that subtype extends T
+                    TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
+                    if (delegate == null) {
+                        throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + label
+                                + "; did you forget to register a subtype?");
+                    }
+                    return delegate.fromJsonTree(jsonElement);
+                } else {
+                    throw new JsonParseException("cannot deserialize " + baseType + " because jsonElement is null");
+                }
+            }
+
+            @Override
+            public void write(JsonWriter out, @Nullable R value) throws IOException {
+                if (value == null) {
+                    return;
+                }
+
+                Class<?> srcType = value.getClass();
+                String label = subtypeToLabel.get(srcType);
+                @SuppressWarnings("unchecked") // registration requires that subtype extends T
+                TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
+                if (delegate == null) {
+                    throw new JsonParseException(
+                            "cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
+                }
+                JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
+                if (jsonObject.has(typeFieldName)) {
+                    throw new JsonParseException("cannot serialize " + srcType.getName()
+                            + " because it already defines a field named " + typeFieldName);
+                }
+                JsonObject clone = new JsonObject();
+                clone.add(typeFieldName, new JsonPrimitive(label));
+                for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
+                    clone.add(e.getKey(), e.getValue());
+                }
+                RuntimeTypeAdapterFactory.write(clone, out);
+            }
+        }.nullSafe();
+    }
+
+    /**
+     * Takes a reader in any state and returns the next value as a JsonElement.
+     */
+    private static @Nullable JsonElement parse(JsonReader reader) throws JsonParseException {
+        boolean isEmpty = true;
+        try {
+            reader.peek();
+            isEmpty = false;
+            return RuntimeTypeAdapterFactory.JSON_ELEMENT.read(reader);
+        } catch (EOFException e) {
+            /*
+             * For compatibility with JSON 1.5 and earlier, we return a JsonNull for
+             * empty documents instead of throwing.
+             */
+            if (isEmpty) {
+                return JsonNull.INSTANCE;
+            }
+            // The stream ended prematurely so it is likely a syntax error.
+            throw new JsonSyntaxException(e);
+        } catch (MalformedJsonException e) {
+            throw new JsonSyntaxException(e);
+        } catch (IOException e) {
+            throw new JsonIOException(e);
+        } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(e);
+        }
+    }
+
+    /**
+     * Writes the JSON element to the writer, recursively.
+     */
+    private static void write(JsonElement element, JsonWriter writer) throws IOException {
+        RuntimeTypeAdapterFactory.JSON_ELEMENT.write(writer, element);
+    }
+
+    private static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() {
+        @Override
+        public @Nullable JsonElement read(JsonReader in) throws IOException {
+            switch (in.peek()) {
+                case STRING:
+                    return new JsonPrimitive(in.nextString());
+                case NUMBER:
+                    String number = in.nextString();
+                    return new JsonPrimitive(new LazilyParsedNumber(number));
+                case BOOLEAN:
+                    return new JsonPrimitive(in.nextBoolean());
+                case NULL:
+                    in.nextNull();
+                    return JsonNull.INSTANCE;
+                case BEGIN_ARRAY:
+                    JsonArray array = new JsonArray();
+                    in.beginArray();
+                    while (in.hasNext()) {
+                        array.add(read(in));
+                    }
+                    in.endArray();
+                    return array;
+                case BEGIN_OBJECT:
+                    JsonObject object = new JsonObject();
+                    in.beginObject();
+                    while (in.hasNext()) {
+                        object.add(in.nextName(), read(in));
+                    }
+                    in.endObject();
+                    return object;
+                case END_DOCUMENT:
+                case NAME:
+                case END_OBJECT:
+                case END_ARRAY:
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        @Override
+        public void write(JsonWriter out, @Nullable JsonElement value) throws IOException {
+            if (value == null || value.isJsonNull()) {
+                out.nullValue();
+            } else if (value.isJsonPrimitive()) {
+                JsonPrimitive primitive = value.getAsJsonPrimitive();
+                if (primitive.isNumber()) {
+                    out.value(primitive.getAsNumber());
+                } else if (primitive.isBoolean()) {
+                    out.value(primitive.getAsBoolean());
+                } else {
+                    out.value(primitive.getAsString());
+                }
+
+            } else if (value.isJsonArray()) {
+                out.beginArray();
+                for (JsonElement e : value.getAsJsonArray()) {
+                    write(out, e);
+                }
+                out.endArray();
+
+            } else if (value.isJsonObject()) {
+                out.beginObject();
+                for (Map.Entry<String, JsonElement> e : value.getAsJsonObject().entrySet()) {
+                    out.name(e.getKey());
+                    write(out, e.getValue());
+                }
+                out.endObject();
+
+            } else {
+                throw new IllegalArgumentException("Couldn't write " + value.getClass());
+            }
+        }
+    };
+
+    /**
+     * This class holds a number value that is lazily converted to a specific number type
+     *
+     * @author Inderjeet Singh
+     */
+    public static final class LazilyParsedNumber extends Number {
+        private final String value;
+
+        public LazilyParsedNumber(String value) {
+            this.value = value;
+        }
+
+        @Override
+        public int intValue() {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                try {
+                    return (int) Long.parseLong(value);
+                } catch (NumberFormatException nfe) {
+                    return new BigDecimal(value).intValue();
+                }
+            }
+        }
+
+        @Override
+        public long longValue() {
+            try {
+                return Long.parseLong(value);
+            } catch (NumberFormatException e) {
+                return new BigDecimal(value).longValue();
+            }
+        }
+
+        @Override
+        public float floatValue() {
+            return Float.parseFloat(value);
+        }
+
+        @Override
+        public double doubleValue() {
+            return Double.parseDouble(value);
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+
+        /**
+         * If somebody is unlucky enough to have to serialize one of these, serialize
+         * it as a BigDecimal so that they won't need Gson on the other side to
+         * deserialize it.
+         */
+        private Object writeReplace() throws ObjectStreamException {
+            return new BigDecimal(value);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/feature/feature.xml b/bundles/org.openhab.binding.siemenshvac/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..a253c87
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+       Copyright (c) 2010-2020 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
+
+-->
+<features name="org.openhab.binding.siemenshvac-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-siemenshvac" description="SiemensHvac Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <feature>openhab-transport-upnp</feature>
+               <feature>openhab-transport-mdns</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.siemenshvac/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/constants/SiemensHvacBindingConstants.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/constants/SiemensHvacBindingConstants.java
new file mode 100644 (file)
index 0000000..c4b6ace
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * 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.constants;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link SiemensHvacBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacBindingConstants {
+
+    public static final String BINDING_ID = "siemenshvac";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_OZW = new ThingTypeUID(BINDING_ID, "ozw");
+
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(SiemensHvacBindingConstants.THING_TYPE_OZW);
+
+    public static final String IP_ADDRESS = "ipAddress";
+    public static final String BASE_URL = "baseUrl";
+
+    public static final String PROPERTY_VENDOR_NAME = "Siemens";
+
+    public static final String CONFIG_DESCRIPTION_URI_THING_PREFIX = "thing-type";
+
+    public static final String DPT_TYPE_ENUM = "Enumeration";
+    public static final String DPT_TYPE_NUMERIC = "Numeric";
+    public static final String DPT_TYPE_RADIO = "RadioButton";
+    public static final String DPT_TYPE_DATE_TIME = "DateTime";
+    public static final String DPT_TYPE_TIMEOFDAY = "TimeOfDay";
+    public static final String DPT_TYPE_STRING = "String";
+
+    public static final String DPT_TYPE_CHECKBOX = "CheckBox";
+    public static final String DPT_TYPE_SCHEDULER = "Scheduler";
+    public static final String DPT_TYPE_CALENDAR = "Calendar";
+
+    public static final String CATEGORY_THING_HVAC = "HVAC";
+
+    public static final String CATEGORY_CHANNEL_NUMBER = "Number";
+    public static final String CATEGORY_CHANNEL_SWITCH = "Switch";
+    public static final String CATEGORY_CHANNEL_TEMP = "Temperature";
+    public static final String CATEGORY_CHANNEL_TIME = "Time";
+
+    public static final String CATEGORY_CHANNEL_CONTROL_HEATING = "Heating";
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterException.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterException.java
new file mode 100644 (file)
index 0000000..d9b73d8
--- /dev/null
@@ -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.converter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception if something goes wrong when converting values between openHAB and the binding.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class ConverterException extends Exception {
+    private static final long serialVersionUID = 42567425458545L;
+
+    public ConverterException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterFactory.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterFactory.java
new file mode 100644 (file)
index 0000000..757c8ee
--- /dev/null
@@ -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.converter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.siemenshvac.internal.constants.SiemensHvacBindingConstants;
+import org.openhab.binding.siemenshvac.internal.converter.type.CalendarTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.CheckboxTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.DateTimeTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.EnumTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.NumericTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.RadioTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.SchedulerTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.StringTypeConverter;
+import org.openhab.binding.siemenshvac.internal.converter.type.TimeOfDayTypeConverter;
+import org.openhab.core.i18n.TimeZoneProvider;
+
+/**
+ * A factory for creating converters based on the itemType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+
+@NonNullByDefault
+public class ConverterFactory {
+    private static Map<String, TypeConverter> converterCache = new HashMap<>();
+
+    public static void registerConverter(TimeZoneProvider timeZoneProvider) {
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_DATE_TIME, new DateTimeTypeConverter(timeZoneProvider));
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_ENUM, new EnumTypeConverter());
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_NUMERIC, new NumericTypeConverter());
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_RADIO, new RadioTypeConverter());
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_STRING, new StringTypeConverter());
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_TIMEOFDAY, new TimeOfDayTypeConverter(timeZoneProvider));
+
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_CHECKBOX, new CheckboxTypeConverter());
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_SCHEDULER, new SchedulerTypeConverter());
+        registerConverter(SiemensHvacBindingConstants.DPT_TYPE_CALENDAR, new CalendarTypeConverter());
+    }
+
+    public static void registerConverter(String key, TypeConverter tp) {
+        converterCache.put(key, tp);
+    }
+
+    /**
+     * Returns the converter for an itemType.
+     */
+    public static TypeConverter getConverter(String itemType) throws ConverterTypeException {
+        TypeConverter converter = converterCache.get(itemType);
+        if (converter == null) {
+            throw new ConverterTypeException("Can't find a converter for type '" + itemType + "'");
+        }
+
+        return converter;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterTypeException.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/ConverterTypeException.java
new file mode 100644 (file)
index 0000000..3c3c6ba
--- /dev/null
@@ -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.converter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception if converting between two types is not possible due wrong item type or command.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class ConverterTypeException extends ConverterException {
+    private static final long serialVersionUID = 2546248551752214152L;
+
+    public ConverterTypeException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/TypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/TypeConverter.java
new file mode 100644 (file)
index 0000000..fbaf5ef
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * 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.converter;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Converter interface for converting between openHAB states/commands and siemensHvac values.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface TypeConverter {
+
+    /**
+     * Converts an openHAB type to a SiemensHVac value.
+     */
+    @Nullable
+    Object convertToBinding(Type type, ChannelType tp) throws ConverterException;
+
+    /**
+     * Converts a siemensHvac value to an openHAB type.
+     */
+    State convertFromBinding(JsonObject dp, ChannelType tp, Locale locale) throws ConverterException;
+
+    /**
+     * get underlying channel type to construct channel type UID
+     *
+     */
+    String getChannelType(SiemensHvacMetadataDataPoint dpt);
+
+    /**
+     * get underlying item type on openhab side for this SiemensHvac type
+     *
+     */
+    String getItemType(SiemensHvacMetadataDataPoint dpt);
+
+    /**
+     * tell if this type have different subvariant or not
+     *
+     */
+    boolean hasVariant();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/AbstractTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/AbstractTypeConverter.java
new file mode 100644 (file)
index 0000000..69b4485
--- /dev/null
@@ -0,0 +1,145 @@
+/**
+ * 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.converter.type;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+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.core.thing.type.ChannelType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.Type;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * Base class for all Converters with common methods.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractTypeConverter implements TypeConverter {
+    private final Logger logger = LoggerFactory.getLogger(AbstractTypeConverter.class);
+
+    @Override
+    public @Nullable Object convertToBinding(Type type, ChannelType tp) throws ConverterException {
+        if (type == UnDefType.NULL) {
+            return null;
+        } else if (type.getClass().isEnum()) {
+            return commandToBinding((Command) type, tp);
+        } else if (!toBindingValidation(type)) {
+            String errorMessage = String.format("Can't convert type %s with value '%s' to %s value",
+                    type.getClass().getSimpleName(), type.toString(), this.getClass().getSimpleName());
+            throw new ConverterTypeException(errorMessage);
+        }
+
+        return toBinding(type, tp);
+    }
+
+    @Override
+    public State convertFromBinding(JsonObject dp, ChannelType tp, Locale locale) throws ConverterException {
+        String type = null;
+        String unit = "";
+        JsonElement value = null;
+
+        if (dp.has("Type")) {
+            type = dp.get("Type").getAsString().trim();
+        }
+        if (dp.has("Value")) {
+            value = dp.get("Value");
+        }
+        if (dp.has("EnumValue")) {
+            value = dp.get("EnumValue");
+        }
+
+        if (dp.has("Unit")) {
+            unit = dp.get("Unit").getAsString().trim();
+        }
+
+        if (value == null) {
+            return UnDefType.NULL;
+        }
+
+        if (type == null) {
+            logger.debug("siemensHvac:ReadDP:null type {}", dp);
+            return UnDefType.NULL;
+        }
+
+        if (!fromBindingValidation(value, unit, type)) {
+            logger.debug("Can't convert {} value '{}' with {} for '{}'", type, value, this.getClass().getSimpleName(),
+                    dp);
+            return UnDefType.NULL;
+        }
+
+        return fromBinding(value, unit, type, tp, locale);
+    }
+
+    /**
+     * Converts an openHAB command to a SiemensHvacValue value.
+     */
+    protected @Nullable Object commandToBinding(Command command, ChannelType tp) throws ConverterException {
+        throw new ConverterException("Unsupported command " + command.getClass().getSimpleName() + " for "
+                + this.getClass().getSimpleName());
+    }
+
+    /**
+     * Returns true, if the conversion from openHAB to the binding is possible.
+     */
+    protected abstract boolean toBindingValidation(Type type);
+
+    /**
+     * Converts the type to a datapoint value.
+     */
+    protected abstract @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException;
+
+    /**
+     * Returns true, if the conversion from the binding to openHAB is possible.
+     */
+    protected abstract boolean fromBindingValidation(JsonElement value, String unit, String type);
+
+    /**
+     * Converts the datapoint value to an openHAB type.
+     */
+    protected abstract State fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException;
+
+    /**
+     * get underlying channel type to construct channel type UID
+     *
+     */
+    @Override
+    public abstract String getChannelType(SiemensHvacMetadataDataPoint dpt);
+
+    /**
+     * get underlying item type on openhab side for this SiemensHvac type
+     *
+     */
+    @Override
+    public abstract String getItemType(SiemensHvacMetadataDataPoint dpt);
+
+    /**
+     * tell if this type have different subvariant or not
+     *
+     */
+    @Override
+    public abstract boolean hasVariant();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/CalendarTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/CalendarTypeConverter.java
new file mode 100644 (file)
index 0000000..b6576f7
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * 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.converter.type;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class CalendarTypeConverter extends AbstractTypeConverter {
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof DateTimeType dateTime) {
+            valUpdate = dateTime.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected DecimalType fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        throw new ConverterException("NIY");
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "datetime";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.DATETIME;
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/CheckboxTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/CheckboxTypeConverter.java
new file mode 100644 (file)
index 0000000..f6537b4
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * 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.converter.type;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class CheckboxTypeConverter extends AbstractTypeConverter {
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        throw new ConverterException("NIY");
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected DecimalType fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        throw new ConverterException("NIY");
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "contact";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.CONTACT;
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/DateTimeTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/DateTimeTypeConverter.java
new file mode 100644 (file)
index 0000000..9d6ae69
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * 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.converter.type;
+
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class DateTimeTypeConverter extends AbstractTypeConverter {
+
+    private final TimeZoneProvider timeZoneProvider;
+
+    public DateTimeTypeConverter(final TimeZoneProvider timeZoneProvider) {
+        this.timeZoneProvider = timeZoneProvider;
+    }
+
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof DateTimeType dateTime) {
+            valUpdate = dateTime.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected DateTimeType fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        if ("----".equals(value.getAsString())) {
+            return new DateTimeType(ZonedDateTime.now(this.timeZoneProvider.getTimeZone()));
+        } else {
+            String[] formats = { "EEEE, d. LLLL yyyy HH:mm[:ss]", "d. LLLL yyyy HH:mm[:ss]", "d. LLLL yyyy",
+                    "d. LLLL" };
+
+            for (int i = 0; i < formats.length; i++) {
+                try {
+                    DateTimeFormatterBuilder formatterBuilder = new DateTimeFormatterBuilder().parseCaseInsensitive()
+                            .appendPattern(formats[i]).parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
+                            .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
+                            .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
+
+                    if (i == 3) {
+                        formatterBuilder = formatterBuilder.parseDefaulting(ChronoField.YEAR,
+                                ZonedDateTime.now(this.timeZoneProvider.getTimeZone()).getYear());
+                    }
+
+                    LocalDateTime parsedDate = LocalDateTime.parse(value.getAsString(),
+                            formatterBuilder.toFormatter(locale));
+
+                    ZonedDateTime zdt = parsedDate.atZone(this.timeZoneProvider.getTimeZone());
+
+                    return new DateTimeType(zdt);
+                } catch (DateTimeParseException ex) {
+                    // Silently ignore, we are proceeding to next format.
+                }
+            }
+        }
+
+        throw new ConverterException("Can't parse the date for:" + value);
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "datetime";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.DATETIME;
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/EnumTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/EnumTypeConverter.java
new file mode 100644 (file)
index 0000000..b23bfb1
--- /dev/null
@@ -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.converter.type;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class EnumTypeConverter extends AbstractTypeConverter {
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof DecimalType decimalValue) {
+            valUpdate = decimalValue.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected DecimalType fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        return new DecimalType(value.getAsInt());
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "number";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.NUMBER;
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return true;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/NumericTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/NumericTypeConverter.java
new file mode 100644 (file)
index 0000000..0b9c895
--- /dev/null
@@ -0,0 +1,250 @@
+/**
+ * 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.converter.type;
+
+import java.util.Locale;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.ElectricPotential;
+import javax.measure.quantity.Temperature;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class NumericTypeConverter extends AbstractTypeConverter {
+
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof QuantityType quantityType) {
+            Number num = (quantityType);
+            valUpdate = num.doubleValue();
+        } else if (type instanceof DecimalType decimalValue) {
+            valUpdate = decimalValue.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected State fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        if ("----".equals(value.getAsString())) {
+            return new DecimalType(0);
+        } else {
+            double dValue = value.getAsDouble();
+
+            String itemType = tp.getItemType();
+
+            if (itemType != null) {
+                if ("Number:Temperature".equals(itemType)) {
+                    Unit<Temperature> targetUnit = null;
+
+                    if ("°C".equals(unit)) {
+                        targetUnit = SIUnits.CELSIUS;
+                    } else if ("°F".equals(unit)) {
+                        targetUnit = ImperialUnits.FAHRENHEIT;
+                    }
+
+                    if (targetUnit != null) {
+                        return new QuantityType<>(dValue, targetUnit);
+                    }
+                } else if ("Number:ElectricPotential".equals(itemType)) {
+                    Unit<ElectricPotential> targetUnit = null;
+
+                    if ("V".equals(unit)) {
+                        targetUnit = Units.VOLT;
+                    }
+
+                    if (targetUnit != null) {
+                        return new QuantityType<>(dValue, targetUnit);
+                    }
+                } else if ("Number:Time".equals(itemType)) {
+                    Unit<Time> targetUnit = null;
+
+                    switch (unit) {
+                        case "s":
+                        case "sek":
+                            targetUnit = Units.SECOND;
+                            break;
+                        case "m":
+                        case "min":
+                        case "perc":
+                        case "dak":
+                        case "мин":
+                            targetUnit = Units.MINUTE;
+                            break;
+                        case "h":
+                        case "sa":
+                            targetUnit = Units.HOUR;
+                            break;
+                        case "Months":
+                        case "Monate":
+                        case "Mois":
+                        case "Mesi":
+                        case "Maanden":
+                        case "mies.":
+                        case "Měsíce":
+                        case "hónap":
+                        case "Meses":
+                        case "mdr.":
+                        case "Månader":
+                        case "kk":
+                        case "месяцы":
+                        case "Aylar":
+                        case "mesiac":
+                            targetUnit = Units.MONTH;
+                            break;
+                        case "d":
+                        case "Jours":
+                        case "giorni":
+                        case "Dny":
+                        case "nap":
+                        case "dage":
+                        case "dag":
+                        case "vrk":
+                        case "д":
+                            targetUnit = Units.DAY;
+                            break;
+                    }
+
+                    if (targetUnit != null) {
+                        return new QuantityType<>(dValue, targetUnit);
+                    }
+                } else if ("Number:Dimensionless".equals(itemType)) {
+                    Unit<Dimensionless> targetUnit = null;
+
+                    if ("%".equals(unit)) {
+                        targetUnit = Units.PERCENT;
+                    }
+
+                    if (targetUnit != null) {
+                        return new QuantityType<>(dValue, targetUnit);
+                    }
+                } else if ("Number".equals(itemType)) {
+                    return new DecimalType(dValue);
+                }
+            } else {
+                return new DecimalType(dValue);
+            }
+
+            return new DecimalType(dValue);
+        }
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "number";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        String unit = dpt.getDptUnit();
+
+        if (unit == null) {
+            return CoreItemFactory.NUMBER;
+        }
+
+        if ("".equals(unit)) {
+            return CoreItemFactory.NUMBER;
+        }
+
+        switch (unit) {
+            case "°F":
+            case "°C":
+                return CoreItemFactory.NUMBER + ":Temperature";
+            case "°F*min":
+            case "°C*min":
+            case "°Cmin":
+            case "°Cdak":
+                return CoreItemFactory.NUMBER;
+            case "V":
+                return CoreItemFactory.NUMBER + ":ElectricPotential";
+            case "%":
+                return CoreItemFactory.NUMBER + ":Dimensionless";
+            case "d":
+            case "Jours":
+            case "giorni":
+            case "Dny":
+            case "nap":
+            case "dage":
+            case "dag":
+            case "vrk":
+            case "д":
+            case "h":
+            case "sa":
+            case "m":
+            case "s":
+            case "сек":
+            case "min":
+            case "perc":
+            case "мин":
+            case "dak":
+            case "Months":
+            case "Monate":
+            case "Mois":
+            case "Mesi":
+            case "Maanden":
+            case "mies.":
+            case "Měsíce":
+            case "hónap":
+            case "Meses":
+            case "mdr.":
+            case "Månader":
+            case "kk":
+            case "месяцы":
+            case "Aylar":
+            case "mesiac":
+                return CoreItemFactory.NUMBER + ":Time";
+            default:
+                return CoreItemFactory.NUMBER;
+        }
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return true;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/RadioTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/RadioTypeConverter.java
new file mode 100644 (file)
index 0000000..d27a65b
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * 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.converter.type;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateDescription;
+import org.openhab.core.types.StateOption;
+import org.openhab.core.types.Type;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class RadioTypeConverter extends AbstractTypeConverter {
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof DecimalType decimalValue) {
+            valUpdate = decimalValue.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected @Nullable Object commandToBinding(Command command, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (command instanceof DecimalType decimalValue) {
+            valUpdate = decimalValue.toString();
+        } else if (command instanceof OnOffType onOffValue) {
+            if (onOffValue.equals(OnOffType.OFF)) {
+                valUpdate = 0;
+            } else if (onOffValue.equals(OnOffType.ON)) {
+                valUpdate = 1;
+            }
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected State fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        State updateVal = UnDefType.UNDEF;
+        String valueSt = value.getAsString();
+
+        StateDescription sd = tp.getState();
+
+        if (sd != null) {
+            List<StateOption> options = sd.getOptions();
+            StateOption offOpt = options.get(0);
+            StateOption onOpt = options.get(1);
+
+            if (valueSt.equals(onOpt.getLabel())) {
+                updateVal = new DecimalType(1);
+            } else if (valueSt.equals(offOpt.getLabel())) {
+                updateVal = new DecimalType(0);
+            }
+        }
+
+        return updateVal;
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        if (dpt.getWriteAccess()) {
+            return "switch";
+        } else {
+            return "contact";
+        }
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        if (dpt.getWriteAccess()) {
+            return CoreItemFactory.SWITCH;
+        } else {
+            return CoreItemFactory.CONTACT;
+        }
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return true;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/SchedulerTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/SchedulerTypeConverter.java
new file mode 100644 (file)
index 0000000..764a84f
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * 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.converter.type;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SchedulerTypeConverter extends AbstractTypeConverter {
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof DateTimeType dateTime) {
+            valUpdate = dateTime.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected DecimalType fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        throw new ConverterException("NIY");
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "datetime";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.DATETIME;
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/StringTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/StringTypeConverter.java
new file mode 100644 (file)
index 0000000..d8c5940
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * 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.converter.type;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class StringTypeConverter extends AbstractTypeConverter {
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof PercentType percentValue) {
+            valUpdate = percentValue.toString();
+        } else if (type instanceof DecimalType decimalValue) {
+            valUpdate = decimalValue.toString();
+        } else if (type instanceof StringType stringValue) {
+            valUpdate = stringValue.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected StringType fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        return new StringType(value.getAsString());
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "string";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.STRING;
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/TimeOfDayTypeConverter.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/converter/type/TimeOfDayTypeConverter.java
new file mode 100644 (file)
index 0000000..6d02dca
--- /dev/null
@@ -0,0 +1,121 @@
+/**
+ * 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.converter.type;
+
+import java.time.ZonedDateTime;
+import java.util.Locale;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDataPoint;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.Type;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Converts between a SiemensHvac datapoint value and an openHAB DecimalType.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class TimeOfDayTypeConverter extends AbstractTypeConverter {
+    private final TimeZoneProvider timeZoneProvider;
+
+    public TimeOfDayTypeConverter(final TimeZoneProvider timeZoneProvider) {
+        this.timeZoneProvider = timeZoneProvider;
+    }
+
+    @Override
+    protected boolean toBindingValidation(Type type) {
+        return true;
+    }
+
+    @Override
+    protected @Nullable Object toBinding(Type type, ChannelType tp) throws ConverterException {
+        Object valUpdate = null;
+
+        if (type instanceof DateTimeType dateTime) {
+            valUpdate = dateTime.toString();
+        }
+
+        return valUpdate;
+    }
+
+    @Override
+    protected boolean fromBindingValidation(JsonElement value, String unit, String type) {
+        return true;
+    }
+
+    @Override
+    protected State fromBinding(JsonElement value, String unit, String type, ChannelType tp, Locale locale)
+            throws ConverterException {
+        if ("----".equals(value.getAsString())) {
+            return new DateTimeType(ZonedDateTime.now(this.timeZoneProvider.getTimeZone()));
+        } else {
+
+            if (unit.equals("h:m")) {
+                String st = value.getAsString();
+                String[] parts = st.split(":");
+                int h = Integer.parseInt(parts[0]);
+                int m = Integer.parseInt(parts[1]);
+
+                Unit<Time> targetUnit = Units.MINUTE;
+                return new QuantityType<>(h * 60 + m, targetUnit);
+
+            } else if (unit.equals("m:s")) {
+                String st = value.getAsString();
+                String[] parts = st.split(":");
+                int m = Integer.parseInt(parts[0]);
+                int s = Integer.parseInt(parts[1]);
+
+                Unit<Time> targetUnit = Units.SECOND;
+                return new QuantityType<>(m * 60 + s, targetUnit);
+
+            } else if (unit.equals("h")) {
+                int val = Integer.parseInt(value.getAsString());
+
+                Unit<Time> targetUnit = Units.HOUR;
+                return new QuantityType<>(val, targetUnit);
+
+            } else {
+                throw new ConverterException("unsupported unit type:" + unit);
+            }
+        }
+    }
+
+    @Override
+    public String getChannelType(SiemensHvacMetadataDataPoint dpt) {
+        return "number";
+    }
+
+    @Override
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) {
+        return CoreItemFactory.NUMBER + ":Time";
+    }
+
+    @Override
+    public boolean hasVariant() {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/discovery/SiemenesHvacDiscoveryParticipant.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/discovery/SiemenesHvacDiscoveryParticipant.java
new file mode 100644 (file)
index 0000000..7af6711
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * 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.discovery;
+
+import static org.openhab.core.thing.Thing.PROPERTY_SERIAL_NUMBER;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.jupnp.model.meta.DeviceDetails;
+import org.jupnp.model.meta.ModelDetails;
+import org.jupnp.model.meta.RemoteDevice;
+import org.openhab.binding.siemenshvac.internal.constants.SiemensHvacBindingConstants;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
+import org.openhab.core.config.discovery.upnp.internal.UpnpDiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+
+/**
+ * The {@link SiemensHvacDiscoveryParticipant} is responsible for discovering new and
+ * removed siemensHvac bridges. It uses the central {@link UpnpDiscoveryService}.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "discovery.siemenshvac", immediate = true)
+public class SiemenesHvacDiscoveryParticipant implements UpnpDiscoveryParticipant {
+
+    @Activate
+    public void activate(@Nullable Map<String, Object> configProperties) {
+    }
+
+    @Modified
+    public void modified(@Nullable Map<String, Object> configProperties) {
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
+        return SiemensHvacBindingConstants.SUPPORTED_THING_TYPES;
+    }
+
+    @Override
+    public @Nullable DiscoveryResult createResult(RemoteDevice device) {
+        ThingUID uid = getThingUID(device);
+        if (uid != null) {
+            Map<String, Object> properties = new HashMap<>();
+            String ipAddress = device.getDetails().getPresentationURI().getHost();
+            properties.put(SiemensHvacBindingConstants.BASE_URL, "https://" + ipAddress + "/");
+
+            String label = "";
+
+            if (uid.getAsString().contains("ozw672")) {
+                label = "OZW672 IP Gateway";
+            } else if (uid.getAsString().contains("ozw772")) {
+                label = "OZW772 IP Gateway";
+            }
+
+            String serialNumber = device.getDetails().getSerialNumber();
+            DiscoveryResult result;
+            if (serialNumber != null && !serialNumber.isBlank()) {
+                properties.put(PROPERTY_SERIAL_NUMBER, serialNumber.toLowerCase());
+
+                result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label)
+                        .withRepresentationProperty(PROPERTY_SERIAL_NUMBER).build();
+            } else {
+                result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).build();
+            }
+            return result;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public @Nullable ThingUID getThingUID(RemoteDevice device) {
+        DeviceDetails details = device.getDetails();
+        if (details != null) {
+            ModelDetails modelDetails = details.getModelDetails();
+            String serialNumber = details.getSerialNumber();
+            if (modelDetails != null && serialNumber != null && !serialNumber.isBlank()) {
+                String modelName = modelDetails.getModelName();
+                if (modelName != null) {
+                    if (modelName.startsWith("Web Server OZW672")) {
+                        return new ThingUID(SiemensHvacBindingConstants.THING_TYPE_OZW, "ozw672-" + serialNumber);
+                    } else if (modelName.startsWith("Web Server OZW772")) {
+                        return new ThingUID(SiemensHvacBindingConstants.THING_TYPE_OZW, "ozw772-" + serialNumber);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/discovery/SiemensHvacDeviceDiscoveryService.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/discovery/SiemensHvacDeviceDiscoveryService.java
new file mode 100644 (file)
index 0000000..d310653
--- /dev/null
@@ -0,0 +1,162 @@
+/**
+ * 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.discovery;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.constants.SiemensHvacBindingConstants;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeThingHandler;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataDevice;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataRegistry;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+import org.openhab.binding.siemenshvac.internal.type.UidUtils;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SiemensHvacDeviceDiscoveryService} tracks for Siemens Hvac device connected to the bus.
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacDeviceDiscoveryService extends AbstractDiscoveryService
+        implements DiscoveryService, ThingHandlerService {
+
+    private final Logger logger = LoggerFactory.getLogger(SiemensHvacDeviceDiscoveryService.class);
+
+    private @Nullable SiemensHvacMetadataRegistry metadataRegistry;
+    private @Nullable SiemensHvacBridgeThingHandler siemensHvacBridgeHandler;
+
+    private static final int SEARCH_TIME = 10;
+
+    public SiemensHvacDeviceDiscoveryService() {
+        super(SiemensHvacBindingConstants.SUPPORTED_THING_TYPES, SEARCH_TIME);
+    }
+
+    @Reference
+    public void setSiemensHvacMetadataRegistry(@Nullable SiemensHvacMetadataRegistry metadataRegistry) {
+        this.metadataRegistry = metadataRegistry;
+    }
+
+    public void unsetSiemensHvacMetadataRegistry(SiemensHvacMetadataRegistry metadataRegistry) {
+        this.metadataRegistry = null;
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+    }
+
+    private @Nullable ThingUID getThingUID(ThingTypeUID thingTypeUID, String serial) {
+        final SiemensHvacBridgeThingHandler lcSiemensHvacBridgeHandler = siemensHvacBridgeHandler;
+        if (lcSiemensHvacBridgeHandler != null) {
+            ThingUID localBridgeUID = lcSiemensHvacBridgeHandler.getThing().getUID();
+            return new ThingUID(thingTypeUID, localBridgeUID, serial);
+        }
+        return null;
+    }
+
+    @Override
+    public void startScan() {
+        final SiemensHvacMetadataRegistry lcMetadataRegistry = metadataRegistry;
+        final SiemensHvacBridgeThingHandler lcSiemensHvacBridgeHandler = siemensHvacBridgeHandler;
+        logger.debug("call startScan()");
+
+        if (lcMetadataRegistry != null) {
+            try {
+                lcMetadataRegistry.readMeta();
+            } catch (SiemensHvacException ex) {
+                logger.debug("Exception occurred during execution: {}", ex.getMessage(), ex);
+                return;
+            }
+
+            ArrayList<SiemensHvacMetadataDevice> devices = lcMetadataRegistry.getDevices();
+
+            if (devices == null) {
+                return;
+            }
+
+            for (SiemensHvacMetadataDevice device : devices) {
+
+                String name = device.getName();
+                String type = device.getType();
+                String addr = device.getAddr();
+                String serialNr = device.getSerialNr();
+
+                logger.debug("Find devices: {} / {} / {} / {}", name, type, addr, serialNr);
+
+                String typeSn = UidUtils.sanetizeId(type);
+                ThingTypeUID thingTypeUID = new ThingTypeUID(SiemensHvacBindingConstants.BINDING_ID, typeSn);
+
+                ThingUID thingUID = getThingUID(thingTypeUID, serialNr);
+
+                if (lcSiemensHvacBridgeHandler != null) {
+                    ThingUID bridgeUID = lcSiemensHvacBridgeHandler.getThing().getUID();
+
+                    if (thingUID != null) {
+                        Map<String, Object> properties = new HashMap<>(4);
+                        properties.put(Thing.PROPERTY_MODEL_ID, name);
+                        properties.put("type", type);
+                        properties.put("addr", addr);
+                        properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNr);
+
+                        DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                .withProperties(properties).withBridge(bridgeUID).withLabel(name).build();
+
+                        thingDiscovered(discoveryResult);
+                    }
+                }
+
+            }
+        }
+    }
+
+    @Override
+    protected synchronized void stopScan() {
+        super.stopScan();
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof SiemensHvacBridgeThingHandler siemensHvacBridgeHandler) {
+            this.siemensHvacBridgeHandler = siemensHvacBridgeHandler;
+            this.siemensHvacBridgeHandler.registerDiscoveryListener(this);
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return siemensHvacBridgeHandler;
+    }
+
+    @Override
+    public void deactivate() {
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/factory/SiemensHvacHandlerFactory.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/factory/SiemensHvacHandlerFactory.java
new file mode 100644 (file)
index 0000000..d5ae1a8
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * 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.factory;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.constants.SiemensHvacBindingConstants;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterFactory;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacBridgeThingHandler;
+import org.openhab.binding.siemenshvac.internal.handler.SiemensHvacHandlerImpl;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataRegistry;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link SiemensHvacHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Laurent ARNAL - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.siemenshvac")
+public class SiemensHvacHandlerFactory extends BaseThingHandlerFactory {
+
+    private final NetworkAddressService networkAddressService;
+    private final HttpClientFactory httpClientFactory;
+    private final SiemensHvacMetadataRegistry metaDataRegistry;
+    private final ChannelTypeRegistry channelTypeRegistry;
+    private final TranslationProvider translationProvider;
+
+    @Activate
+    public SiemensHvacHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+            final @Reference SiemensHvacMetadataRegistry metaDataRegistry,
+            final @Reference NetworkAddressService networkAddressService,
+            final @Reference ChannelTypeRegistry channelTypeRegistry,
+            final @Reference TimeZoneProvider timeZoneProvider,
+            final @Reference TranslationProvider translationProvider) {
+        this.httpClientFactory = httpClientFactory;
+        this.metaDataRegistry = metaDataRegistry;
+        this.networkAddressService = networkAddressService;
+        this.channelTypeRegistry = channelTypeRegistry;
+        this.translationProvider = translationProvider;
+
+        ConverterFactory.registerConverter(timeZoneProvider);
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SiemensHvacBindingConstants.BINDING_ID.equals(thingTypeUID.getBindingId());
+    }
+
+    @Override
+    public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
+            @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
+        if (SiemensHvacBindingConstants.THING_TYPE_OZW.equals(thingTypeUID)) {
+            return super.createThing(thingTypeUID, configuration, thingUID, null);
+        } else if (SiemensHvacBindingConstants.BINDING_ID.equals(thingTypeUID.getBindingId())) {
+            return super.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
+        }
+        throw new IllegalArgumentException(
+                "The thing type " + thingTypeUID + " is not supported by the SiemensHvac binding.");
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        if (thing.getThingTypeUID().equals(SiemensHvacBindingConstants.THING_TYPE_OZW)) {
+            return new SiemensHvacBridgeThingHandler((Bridge) thing, networkAddressService, httpClientFactory,
+                    metaDataRegistry, translationProvider);
+        } else if (SiemensHvacBindingConstants.BINDING_ID.equals(thing.getThingTypeUID().getBindingId())) {
+            SiemensHvacHandlerImpl handler = new SiemensHvacHandlerImpl(thing,
+                    metaDataRegistry.getSiemensHvacConnector(), metaDataRegistry, channelTypeRegistry);
+            return handler;
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacBridgeConfig.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacBridgeConfig.java
new file mode 100644 (file)
index 0000000..2025f17
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.siemenshvac.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacBridgeConfig {
+
+    public String baseUrl = "";
+    public String userName = "Administrator";
+    public String userPassword = "password";
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacBridgeThingHandler.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacBridgeThingHandler.java
new file mode 100644 (file)
index 0000000..323a765
--- /dev/null
@@ -0,0 +1,213 @@
+/**
+ * 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.handler;
+
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.discovery.SiemensHvacDeviceDiscoveryService;
+import org.openhab.binding.siemenshvac.internal.metadata.SiemensHvacMetadataRegistry;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacConnector;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+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.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SiemensHvacBridgeBaseThingHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Laurent Arnal - Initial contribution and API
+ */
+@NonNullByDefault
+public class SiemensHvacBridgeThingHandler extends BaseBridgeHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(SiemensHvacBridgeThingHandler.class);
+    private @Nullable SiemensHvacDeviceDiscoveryService discoveryService;
+    private final @Nullable HttpClientFactory httpClientFactory;
+    private final SiemensHvacMetadataRegistry metaDataRegistry;
+    private @Nullable SiemensHvacBridgeConfig config;
+    private final TranslationProvider translationProvider;
+
+    public SiemensHvacBridgeThingHandler(Bridge bridge, @Nullable NetworkAddressService networkAddressService,
+            @Nullable HttpClientFactory httpClientFactory, SiemensHvacMetadataRegistry metaDataRegistry,
+            TranslationProvider translationProvider) {
+        super(bridge);
+        SiemensHvacConnector lcConnector = null;
+        this.httpClientFactory = httpClientFactory;
+        this.metaDataRegistry = metaDataRegistry;
+        this.translationProvider = translationProvider;
+
+        lcConnector = this.metaDataRegistry.getSiemensHvacConnector();
+        if (lcConnector != null) {
+            lcConnector.setSiemensHvacBridgeBaseThingHandler(this);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    @Override
+    public void dispose() {
+        metaDataRegistry.invalidate();
+    }
+
+    @Override
+    public void initialize() {
+        SiemensHvacBridgeConfig lcConfig = getConfigAs(SiemensHvacBridgeConfig.class);
+        String baseUrl = null;
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Initialize() bridge: {}", getBuildDate());
+        }
+
+        baseUrl = lcConfig.baseUrl;
+
+        if (baseUrl.isEmpty()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+                    "@text/offline.error-gateway-init");
+            return;
+        }
+
+        if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
+            baseUrl = "http://" + baseUrl;
+        }
+
+        if (!baseUrl.endsWith("/")) {
+            baseUrl = baseUrl + "/";
+        }
+
+        config = lcConfig;
+
+        updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/offline.waiting-bridge-initialization");
+
+        // Will read metadata in background to not block initialize for a long period !
+        scheduler.schedule(this::initializeCode, 1, TimeUnit.SECONDS);
+    }
+
+    protected String getBuildDate() {
+        try {
+            ClassLoader cl = getClass().getClassLoader();
+            if (cl != null) {
+                URL res = cl.getResource(getClass().getCanonicalName().replace('.', '/') + ".class");
+                if (res != null) {
+                    URLConnection cnx = res.openConnection();
+                    LocalDate dt = LocalDate.ofEpochDay(cnx.getLastModified());
+                    DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
+                    return df.format(dt);
+                }
+            }
+
+        } catch (Exception ex) {
+        }
+        return "unknown";
+    }
+
+    public static String getStackTrace(final Throwable throwable) {
+        StringBuffer sb = new StringBuffer();
+
+        Throwable current = throwable;
+        while (current != null) {
+            sb.append(current.getLocalizedMessage());
+            sb.append(",\r\n");
+
+            Throwable cause = throwable.getCause();
+            if (cause != null) {
+                if (!cause.equals(throwable)) {
+                    current = current.getCause();
+                } else {
+                    current = null;
+                }
+            } else {
+                current = null;
+            }
+        }
+        return sb.toString();
+    }
+
+    private void initializeCode() {
+        try {
+            metaDataRegistry.readMeta();
+            updateStatus(ThingStatus.ONLINE);
+        } catch (SiemensHvacException ex) {
+            Locale local = metaDataRegistry.getUserLocale();
+            BundleContext bundleContext = FrameworkUtil.getBundle(this.getClass()).getBundleContext();
+            String text = translationProvider.getText(bundleContext.getBundle(), "offline.error-gateway-init",
+                    "DefaultValue", local);
+
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    MessageFormat.format(text, ex.getMessage()));
+        }
+    }
+
+    public @Nullable SiemensHvacBridgeConfig getBridgeConfiguration() {
+        return config;
+    }
+
+    @Override
+    public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
+        super.updateStatus(status, statusDetail, description);
+    }
+
+    public boolean registerDiscoveryListener(SiemensHvacDeviceDiscoveryService listener) {
+        SiemensHvacDeviceDiscoveryService lcDiscoveryService = discoveryService;
+        if (lcDiscoveryService == null) {
+            lcDiscoveryService = listener;
+            lcDiscoveryService.setSiemensHvacMetadataRegistry(metaDataRegistry);
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean unregisterDiscoveryListener() {
+        SiemensHvacDeviceDiscoveryService lcDiscoveryService = discoveryService;
+        if (lcDiscoveryService != null) {
+            discoveryService = null;
+            return true;
+        }
+
+        return false;
+    }
+
+    public @Nullable HttpClientFactory getHttpClientFactory() {
+        return this.httpClientFactory;
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(SiemensHvacDeviceDiscoveryService.class);
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacHandlerImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/handler/SiemensHvacHandlerImpl.java
new file mode 100644 (file)
index 0000000..4c1c4dd
--- /dev/null
@@ -0,0 +1,451 @@
+/**
+ * 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.handler;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.converter.ConverterException;
+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.SiemensHvacMetadataRegistry;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacCallback;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacConnector;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacRequestListener.ErrorSource;
+import org.openhab.core.library.types.DecimalType;
+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.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateDescription;
+import org.openhab.core.types.Type;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link SiemensHvacHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Laurent ARNAL - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacHandlerImpl extends BaseThingHandler {
+
+    private Lock lockObj = new ReentrantLock();
+
+    private final Logger logger = LoggerFactory.getLogger(SiemensHvacHandlerImpl.class);
+
+    private @Nullable ScheduledFuture<?> pollingJob = null;
+
+    private final @Nullable SiemensHvacConnector hvacConnector;
+    private final @Nullable SiemensHvacMetadataRegistry metaDataRegistry;
+    private final ChannelTypeRegistry channelTypeRegistry;
+
+    private long lastWrite = 0;
+
+    public SiemensHvacHandlerImpl(Thing thing, @Nullable SiemensHvacConnector hvacConnector,
+            @Nullable SiemensHvacMetadataRegistry metaDataRegistry, ChannelTypeRegistry channelTypeRegistry) {
+        super(thing);
+
+        this.hvacConnector = hvacConnector;
+        this.metaDataRegistry = metaDataRegistry;
+        this.channelTypeRegistry = channelTypeRegistry;
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+    }
+
+    @Override
+    public void initialize() {
+        updateStatus(ThingStatus.UNKNOWN);
+        pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, 5, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void dispose() {
+        ScheduledFuture<?> lcPollingJob = pollingJob;
+        if (lcPollingJob != null) {
+            lcPollingJob.cancel(true);
+            pollingJob = null;
+        }
+    }
+
+    private void pollingCode() {
+        Bridge lcBridge = getBridge();
+
+        if (lcBridge == null) {
+            return;
+        }
+
+        if (lcBridge.getStatus() == ThingStatus.OFFLINE) {
+            if (!ThingStatusDetail.COMMUNICATION_ERROR.equals(lcBridge.getStatusInfo().getStatusDetail())) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+                return;
+            }
+        }
+
+        if (lcBridge.getStatus() != ThingStatus.ONLINE) {
+            if (!ThingStatusDetail.COMMUNICATION_ERROR.equals(lcBridge.getStatusInfo().getStatusDetail())) {
+                logger.debug("Bridge is not ready, don't enter polling for now!");
+                return;
+            }
+        }
+
+        long start = System.currentTimeMillis();
+        var chList = this.getThing().getChannels();
+
+        SiemensHvacConnector lcHvacConnector = hvacConnector;
+        if (lcHvacConnector != null) {
+            int previousRequestCount = lcHvacConnector.getRequestCount();
+            int previousErrorCount = lcHvacConnector.getErrorCount();
+
+            logger.debug("readChannels:");
+            for (Channel channel : chList) {
+                readChannel(channel);
+            }
+
+            logger.debug("WaitAllPendingRequest:Start waiting()");
+            lcHvacConnector.waitAllPendingRequest();
+            long end = System.currentTimeMillis();
+            logger.debug("WaitAllPendingRequest:All request done(): {}", (end - start) / 1000.00);
+
+            int newRequestCount = lcHvacConnector.getRequestCount();
+            int newErrorCount = lcHvacConnector.getErrorCount();
+
+            int requestCount = newRequestCount - previousRequestCount;
+            int errorCount = newErrorCount - previousErrorCount;
+
+            double errorRate = (double) errorCount / requestCount * 100.00;
+
+            if (errorRate > 50) {
+                SiemensHvacBridgeThingHandler bridgeHandler = (SiemensHvacBridgeThingHandler) lcBridge.getHandler();
+
+                if (lcHvacConnector.getErrorSource() == ErrorSource.ErrorBridge) {
+                    if (bridgeHandler != null) {
+                        bridgeHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                                String.format("Communication ErrorRate to gateway is too high: %f", errorRate));
+                    }
+                } else if (lcHvacConnector.getErrorSource() == ErrorSource.ErrorThings) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                            String.format("Communication ErrorRate to thing is too high: %f", errorRate));
+                }
+            } else {
+                updateStatus(ThingStatus.ONLINE);
+
+                SiemensHvacBridgeThingHandler bridgeHandler = (SiemensHvacBridgeThingHandler) lcBridge.getHandler();
+
+                // Automatically recover from communication errors if errorRate is ok.
+                if (bridgeHandler != null) {
+                    if (ThingStatusDetail.COMMUNICATION_ERROR
+                            .equals(bridgeHandler.getThing().getStatusInfo().getStatusDetail())) {
+                        bridgeHandler.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "");
+                    }
+                }
+            }
+
+            lcHvacConnector.displayRequestStats();
+        }
+    }
+
+    private void readChannel(Channel channel) {
+        ThingHandlerCallback cb = this.getCallback();
+        boolean isLink = false;
+
+        if (cb != null) {
+            isLink = cb.isChannelLinked(channel.getUID());
+        }
+
+        if (!isLink) {
+            return;
+        }
+
+        logger.debug("readChannel: {}", channel.getDescription());
+
+        ChannelType tp = channelTypeRegistry.getChannelType(channel.getChannelTypeUID());
+
+        if (tp == null) {
+            return;
+        }
+
+        String id = channel.getProperties().get("id");
+        String uid = channel.getUID().getId();
+        String type = tp.getItemType();
+
+        if (id == null) {
+            id = (String) channel.getConfiguration().getProperties().get("id");
+        }
+
+        if (id == null) {
+            logger.debug("pollingCode: Id is null {} ", channel);
+            return;
+        }
+        if (type == null) {
+            logger.debug("pollingCode: type is null {} ", channel);
+            return;
+        }
+
+        readDp(id, uid, tp, type, true);
+    }
+
+    public void decodeReadDp(@Nullable JsonObject response, @Nullable String uid, @Nullable String dp, ChannelType tp,
+            @Nullable String type) {
+        SiemensHvacMetadataRegistry lcMetaDataRegistry = metaDataRegistry;
+        if (lcMetaDataRegistry == null) {
+            return;
+        }
+
+        if (response != null && response.has("Data")) {
+            JsonObject subResult = (JsonObject) response.get("Data");
+
+            String updateKey = "" + uid;
+
+            String typer = null;
+
+            if (subResult.has("Type")) {
+                typer = subResult.get("Type").getAsString().trim();
+            }
+
+            try {
+                if (typer != null) {
+                    TypeConverter converter = ConverterFactory.getConverter(typer);
+
+                    Locale local = lcMetaDataRegistry.getUserLocale();
+                    if (local == null) {
+                        local = Locale.getDefault();
+                    }
+
+                    State state = converter.convertFromBinding(subResult, tp, local);
+                    updateState(updateKey, state);
+                }
+            } catch (ConverterTypeException ex) {
+                logger.warn("{}, for uid : {}, please check the item type", ex.getMessage(), uid);
+            } catch (ConverterException ex) {
+                logger.warn("{}, for uid: {}, please check the item type", ex.getMessage(), uid);
+            }
+
+        }
+    }
+
+    private void readDp(String dp, String uid, ChannelType tp, String type, boolean async) {
+        SiemensHvacConnector lcHvacConnector = hvacConnector;
+
+        if ("-1".equals(dp)) {
+            return;
+        }
+
+        try {
+            lockObj.lock();
+
+            logger.trace("Start read: {}", dp);
+            String request = "api/menutree/read_datapoint.json?Id=" + dp;
+
+            logger.debug("siemensHvac:ReadDp:DoRequest(): {}", request);
+
+            if (async) {
+                if (lcHvacConnector != null) {
+                    lcHvacConnector.doRequest(request, new SiemensHvacCallback() {
+
+                        @Override
+                        public void execute(java.net.URI uri, int status, @Nullable Object response) {
+                            // prevent async read if we just write so we have no overlaps
+                            long now = System.currentTimeMillis();
+                            if (now - lastWrite < 5000) {
+                                return;
+                            }
+
+                            logger.trace("End read: {}", dp);
+
+                            if (response instanceof JsonObject jsonResponse) {
+                                decodeReadDp(jsonResponse, uid, dp, tp, type);
+                            }
+                        }
+                    });
+                }
+            } else {
+                if (lcHvacConnector != null) {
+                    JsonObject js = lcHvacConnector.doRequest(request);
+                    decodeReadDp(js, uid, dp, tp, type);
+                }
+            }
+        } finally {
+            logger.trace("End read: {}", dp);
+            lockObj.unlock();
+        }
+    }
+
+    private void writeDp(String dp, Type dpVal, ChannelType tp, String type) {
+        SiemensHvacConnector lcHvacConnector = hvacConnector;
+
+        if (lcHvacConnector != null) {
+            lcHvacConnector.displayRequestStats();
+        }
+
+        if ("-1".equals(dp)) {
+            return;
+        }
+
+        try {
+            lockObj.lock();
+            logger.trace("Start write: {}", dp);
+            lastWrite = System.currentTimeMillis();
+
+            Object valUpdate = "0";
+
+            try {
+                TypeConverter converter = ConverterFactory.getConverter(type);
+
+                valUpdate = converter.convertToBinding(dpVal, tp);
+                if (valUpdate != null) {
+                    String request = String.format("api/menutree/write_datapoint.json?Id=%s&Value=%s&Type=%s", dp,
+                            valUpdate, type);
+
+                    if (lcHvacConnector != null) {
+                        logger.trace("Write request for: {} ", valUpdate);
+                        JsonObject response = lcHvacConnector.doRequest(request);
+
+                        logger.trace("Write request response: {} ", response);
+                    }
+
+                } else {
+                    logger.debug("Failed to get converted state from datapoint '{}'", dp);
+                }
+            } catch (ConverterTypeException ex) {
+                logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
+            } catch (ConverterException ex) {
+                logger.warn("{}, please check the item type and the commands in your scripts", ex.getMessage());
+            }
+        } finally {
+            logger.debug("End write: {}", dp);
+            lockObj.unlock();
+        }
+    }
+
+    private Command applyState(ChannelType tp, Command command) {
+        StateDescription sd = tp.getState();
+        Command result = command;
+
+        if (sd != null) {
+            BigDecimal maxb = sd.getMaximum();
+            BigDecimal minb = sd.getMinimum();
+            BigDecimal step = sd.getStep();
+            boolean doMods = false;
+
+            if (command instanceof DecimalType decimalCommand) {
+                double v1 = decimalCommand.doubleValue();
+
+                if (step != null) {
+                    doMods = true;
+                    int divider = 1;
+
+                    if (step.doubleValue() == 0.5) {
+                        divider = 2;
+                    } else if (step.doubleValue() == 0.1) {
+                        divider = 10;
+                    } else if (step.doubleValue() == 0.02) {
+                        divider = 50;
+                    } else if (step.doubleValue() == 0.01) {
+                        divider = 100;
+                    }
+                    v1 = v1 * divider;
+                    v1 = Math.floor(v1);
+                    v1 = v1 / divider;
+                }
+
+                if (minb != null && v1 < minb.floatValue()) {
+                    doMods = true;
+                    v1 = minb.floatValue();
+                }
+                if (maxb != null && v1 > maxb.floatValue()) {
+                    doMods = true;
+                    v1 = maxb.floatValue();
+                }
+
+                if (doMods) {
+                    result = new DecimalType(v1);
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        SiemensHvacMetadataRegistry lcMetaDataRegistry = metaDataRegistry;
+        logger.debug("handleCommand");
+        if (command instanceof RefreshType) {
+            var channel = this.getThing().getChannel(channelUID);
+            if (channel != null) {
+                readChannel(channel);
+            }
+        } else {
+            Channel channel = getThing().getChannel(channelUID);
+            if (channel == null) {
+                return;
+            }
+
+            Command commandVar = command;
+            ChannelType tp = channelTypeRegistry.getChannelType(channel.getChannelTypeUID());
+
+            if (tp == null) {
+                return;
+            }
+
+            String type = tp.getItemType();
+            String dptType = "";
+            String id = channel.getProperties().get("id");
+            SiemensHvacMetadataDataPoint md = null;
+
+            if (id == null) {
+                id = (String) channel.getConfiguration().getProperties().get("id");
+            }
+
+            if (lcMetaDataRegistry != null) {
+                md = (SiemensHvacMetadataDataPoint) lcMetaDataRegistry.getDptMap(id);
+                if (md != null) {
+                    id = "" + md.getId();
+                    dptType = md.getDptType();
+                }
+            }
+
+            if (command instanceof State commandState) {
+                commandVar = applyState(tp, commandVar);
+                this.updateState(channelUID, commandState);
+            }
+
+            if (id != null && type != null) {
+                writeDp(id, commandVar, tp, dptType);
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadata.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadata.java
new file mode 100644 (file)
index 0000000..d0597da
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * 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;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacMetadata {
+    private int id = -1;
+    private int subId = -1;
+    private int groupId = -1;
+    private int catId = -1;
+    private String shortDescEn = "";
+    private String longDescEn = "";
+    private String shortDesc = "";
+    private String longDesc = "";
+    @Nullable
+    private transient SiemensHvacMetadata parent;
+
+    public SiemensHvacMetadata() {
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int Id) {
+        this.id = Id;
+    }
+
+    public int getSubId() {
+        return subId;
+    }
+
+    public void setSubId(int subId) {
+        this.subId = subId;
+    }
+
+    public int getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(int groupId) {
+        this.groupId = groupId;
+    }
+
+    public int getCatId() {
+        return catId;
+    }
+
+    public void setCatId(int catId) {
+        this.catId = catId;
+    }
+
+    public String getShortDescEn() {
+        return shortDescEn;
+    }
+
+    public void setShortDescEn(String shortDesc) {
+        this.shortDescEn = shortDesc;
+    }
+
+    public String getLongDescEn() {
+        return longDescEn;
+    }
+
+    public void setLongDescEn(String longDesc) {
+        this.longDescEn = longDesc;
+    }
+
+    public String getShortDesc() {
+        return shortDesc;
+    }
+
+    public void setShortDesc(String shortDesc) {
+        this.shortDesc = shortDesc;
+    }
+
+    public String getLongDesc() {
+        return longDesc;
+    }
+
+    public void setLongDesc(String longDesc) {
+        this.longDesc = longDesc;
+    }
+
+    public @Nullable SiemensHvacMetadata getParent() {
+        return parent;
+    }
+
+    public void setParent(@Nullable SiemensHvacMetadata parent) {
+        this.parent = parent;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataDataPoint.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataDataPoint.java
new file mode 100644 (file)
index 0000000..51b4a8e
--- /dev/null
@@ -0,0 +1,236 @@
+/**
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+import javax.validation.constraints.NotNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.constants.SiemensHvacBindingConstants;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacMetadataDataPoint extends SiemensHvacMetadata {
+    private String dptId = "-1";
+    private String dptType = "";
+    private @Nullable String dptUnit = null;
+    private @Nullable String min = null;
+    private @Nullable String max = null;
+    private @Nullable String resolution = null;
+    private @Nullable String fieldWitdh = null;
+    private @Nullable String decimalDigits = null;
+    private boolean detailsResolved = false;
+    private @Nullable String dialogType = null;
+    private @Nullable String maxLength = null;
+    private @Nullable String address = null;
+    private int dptSubKey = -1;
+    private boolean writeAccess = false;
+
+    private @NotNull List<SiemensHvacMetadataPointChild> child = List.of();
+
+    public SiemensHvacMetadataDataPoint() {
+        child = new ArrayList<SiemensHvacMetadataPointChild>();
+    }
+
+    public String getDptType() {
+        return dptType;
+    }
+
+    public void setDptType(String dptType) {
+        this.dptType = dptType;
+    }
+
+    public List<SiemensHvacMetadataPointChild> getChild() {
+        return child;
+    }
+
+    public void setChild(List<SiemensHvacMetadataPointChild> child) {
+        this.child = child;
+    }
+
+    public String getDptId() {
+        return dptId;
+    }
+
+    public void setDptId(String dptId) {
+        this.dptId = dptId;
+    }
+
+    public int getDptSubKey() {
+        return dptSubKey;
+    }
+
+    public void setDptSubKey(int dptSubKey) {
+        this.dptSubKey = dptSubKey;
+    }
+
+    public @Nullable String getAddress() {
+        return address;
+    }
+
+    public void setWriteAccess(boolean writeAccess) {
+        this.writeAccess = writeAccess;
+    }
+
+    public boolean getWriteAccess() {
+        return writeAccess;
+    }
+
+    public void setAddress(String address) {
+        this.address = address;
+    }
+
+    public @Nullable String getDptUnit() {
+        return dptUnit;
+    }
+
+    public void setDptUnit(String dptUnit) {
+        this.dptUnit = dptUnit;
+    }
+
+    public @Nullable String getMaxLength() {
+        return maxLength;
+    }
+
+    public void setMaxLength(String maxLength) {
+        this.maxLength = maxLength;
+    }
+
+    public @Nullable String getDialogType() {
+        return dialogType;
+    }
+
+    public void setDialogType(String dialogType) {
+        this.dialogType = dialogType;
+    }
+
+    public @Nullable String getMin() {
+        return min;
+    }
+
+    public void setMin(String min) {
+        this.min = min;
+    }
+
+    public @Nullable String getMax() {
+        return max;
+    }
+
+    public void setMax(String max) {
+        this.max = max;
+    }
+
+    public @Nullable String getResolution() {
+        return resolution;
+    }
+
+    public void setResolution(String resolution) {
+        this.resolution = resolution;
+    }
+
+    public @Nullable String getFieldWitdh() {
+        return fieldWitdh;
+    }
+
+    public void setFieldWitdh(String fieldWitdh) {
+        this.fieldWitdh = fieldWitdh;
+    }
+
+    public @Nullable String getDecimalDigits() {
+        return decimalDigits;
+    }
+
+    public void setDecimalDigits(String decimalDigits) {
+        this.decimalDigits = decimalDigits;
+    }
+
+    public Boolean getDetailsResolved() {
+        return detailsResolved;
+    }
+
+    public void setDetailsResolved(Boolean detailsResolved) {
+        this.detailsResolved = detailsResolved;
+    }
+
+    public void resolveDptDetails(JsonObject result) {
+        JsonObject subResultObj = result.getAsJsonObject("Result");
+        JsonObject desc = result.getAsJsonObject("Description");
+
+        if (subResultObj.has("Success")) {
+            JsonObject error = subResultObj.getAsJsonObject("Error");
+            String errorMsg = "";
+            if (error != null) {
+                errorMsg = error.get("Txt").getAsString();
+            }
+
+            if (("datatype not supported").equals(errorMsg)) {
+                detailsResolved = true;
+                return;
+            }
+
+        }
+
+        if (desc != null) {
+            this.dptType = desc.get("Type").getAsString();
+
+            if (SiemensHvacBindingConstants.DPT_TYPE_ENUM.equals(dptType)) {
+                JsonArray enums = desc.getAsJsonArray("Enums");
+
+                for (Object obj : enums) {
+                    JsonObject entry = (JsonObject) obj;
+
+                    SiemensHvacMetadataPointChild ch = new SiemensHvacMetadataPointChild();
+                    ch.setText(entry.get("Text").getAsString());
+                    ch.setValue(entry.get("Value").getAsString());
+                    ch.setIsActive(entry.get("IsCurrentValue").getAsString());
+                    child.add(ch);
+                }
+            } else if (SiemensHvacBindingConstants.DPT_TYPE_NUMERIC.equals(dptType)) {
+                this.dptUnit = desc.get("Unit").getAsString();
+                this.min = desc.get("Min").getAsString();
+                this.max = desc.get("Max").getAsString();
+                this.resolution = desc.get("Resolution").getAsString();
+                this.fieldWitdh = desc.get("FieldWitdh").getAsString();
+                this.decimalDigits = desc.get("DecimalDigits").getAsString();
+            } else if (SiemensHvacBindingConstants.DPT_TYPE_STRING.equals(dptType)) {
+                this.dialogType = desc.get("DialogType").getAsString();
+                this.maxLength = desc.get("MaxLength").getAsString();
+            } else if (SiemensHvacBindingConstants.DPT_TYPE_RADIO.equals(dptType)) {
+                JsonArray buttons = desc.getAsJsonArray("Buttons");
+
+                child = new ArrayList<SiemensHvacMetadataPointChild>();
+
+                for (Object obj : buttons) {
+                    JsonObject button = (JsonObject) obj;
+
+                    SiemensHvacMetadataPointChild ch = new SiemensHvacMetadataPointChild();
+                    ch.setOpt0(button.get("TextOpt0").getAsString());
+                    ch.setOpt1(button.get("TextOpt1").getAsString());
+                    ch.setIsActive(button.get("IsActive").getAsString());
+                    child.add(ch);
+                }
+            }
+
+            detailsResolved = true;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataDevice.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataDevice.java
new file mode 100644 (file)
index 0000000..c9df927
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * 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;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacMetadataDevice {
+
+    private String name = "";
+
+    private String addr = "";
+
+    private String type = "unknown";
+
+    private String serialNr = "";
+
+    private @Nullable String treeDate;
+
+    private @Nullable String treeTime;
+
+    private boolean treeGenerated;
+    private int treeId;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getAddr() {
+        return addr;
+    }
+
+    public void setAddr(String addr) {
+        this.addr = addr;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getSerialNr() {
+        return serialNr;
+    }
+
+    public void setSerialNr(String serialNr) {
+        this.serialNr = serialNr;
+    }
+
+    public @Nullable String getTreeDate() {
+        return treeDate;
+    }
+
+    public void setTreeDate(String treeDate) {
+        this.treeDate = treeDate;
+    }
+
+    public @Nullable String getTreeTime() {
+        return treeTime;
+    }
+
+    public void setTreeTime(String treeTime) {
+        this.treeTime = treeTime;
+    }
+
+    public boolean getTreeGenerated() {
+        return treeGenerated;
+    }
+
+    public void setTreeGenerated(boolean treeGenerated) {
+        this.treeGenerated = treeGenerated;
+    }
+
+    public int getTreeId() {
+        return treeId;
+    }
+
+    public void setTreeId(int treeId) {
+        this.treeId = treeId;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataLanguage.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataLanguage.java
new file mode 100644 (file)
index 0000000..56153f8
--- /dev/null
@@ -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 SiemensHvacMetadataLanguage {
+    private String name;
+    private int id;
+    private String language;
+    private int languageId;
+
+    public SiemensHvacMetadataLanguage() {
+        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/metadata/SiemensHvacMetadataMenu.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataMenu.java
new file mode 100644 (file)
index 0000000..4b48b63
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * 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 java.util.HashMap;
+import java.util.LinkedHashMap;
+
+import javax.validation.constraints.NotNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacMetadataMenu extends SiemensHvacMetadata {
+    private LinkedHashMap<Integer, SiemensHvacMetadata> childList;
+
+    public SiemensHvacMetadataMenu() {
+        childList = new LinkedHashMap<Integer, SiemensHvacMetadata>();
+    }
+
+    public void addChild(SiemensHvacMetadata information) {
+        childList.put(information.getId(), information);
+    }
+
+    public HashMap<Integer, SiemensHvacMetadata> getChilds() {
+        return this.childList;
+    }
+
+    public boolean hasChild(int Id) {
+        return this.childList.containsKey(Id);
+    }
+
+    public @NotNull SiemensHvacMetadata getChild(int Id) {
+        return this.childList.get(Id);
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataPointChild.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataPointChild.java
new file mode 100644 (file)
index 0000000..7c44930
--- /dev/null
@@ -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.metadata;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public class SiemensHvacMetadataPointChild {
+
+    private String text = "";
+    private String value = "";
+    private String opt0 = "";
+    private String opt1 = "";
+    private String isActive = "";
+
+    public String getText() {
+        return this.text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+
+    public String getValue() {
+        return this.value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public String getOpt0() {
+        return this.opt0;
+    }
+
+    public void setOpt0(String opt0) {
+        this.opt0 = opt0;
+    }
+
+    public String getOpt1() {
+        return this.opt1;
+    }
+
+    public void setOpt1(String opt1) {
+        this.opt1 = opt1;
+    }
+
+    public String getIsActive() {
+        return this.isActive;
+    }
+
+    public void setIsActive(String isActive) {
+        this.isActive = isActive;
+    }
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataRegistry.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataRegistry.java
new file mode 100644 (file)
index 0000000..0d3a2c3
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * 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 java.util.ArrayList;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacConnector;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacChannelTypeProvider;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+public interface SiemensHvacMetadataRegistry {
+
+    /**
+     * Initializes the type generator.
+     */
+    void initialize();
+
+    void readMeta() throws SiemensHvacException;
+
+    @Nullable
+    SiemensHvacMetadataMenu getRoot();
+
+    @Nullable
+    ArrayList<SiemensHvacMetadataDevice> getDevices();
+
+    @Nullable
+    SiemensHvacMetadata getDptMap(@Nullable String key);
+
+    @Nullable
+    SiemensHvacChannelTypeProvider getChannelTypeProvider();
+
+    @Nullable
+    SiemensHvacConnector getSiemensHvacConnector();
+
+    void invalidate();
+
+    @Nullable
+    SiemensHvacMetadataUser getUser();
+
+    @Nullable
+    Locale getUserLocale();
+}
diff --git a/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataRegistryImpl.java b/bundles/org.openhab.binding.siemenshvac/src/main/java/org/openhab/binding/siemenshvac/internal/metadata/SiemensHvacMetadataRegistryImpl.java
new file mode 100644 (file)
index 0000000..95880cc
--- /dev/null
@@ -0,0 +1,1301 @@
+/**
+ * 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 java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+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.handler.SiemensHvacBridgeConfig;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacCallback;
+import org.openhab.binding.siemenshvac.internal.network.SiemensHvacConnector;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacChannelGroupTypeProvider;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacChannelTypeProvider;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacConfigDescriptionProvider;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacException;
+import org.openhab.binding.siemenshvac.internal.type.SiemensHvacThingTypeProvider;
+import org.openhab.binding.siemenshvac.internal.type.UidUtils;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.config.core.ConfigDescriptionBuilder;
+import org.openhab.core.config.core.ConfigDescriptionParameter;
+import org.openhab.core.config.core.ConfigDescriptionParameterGroup;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelDefinition;
+import org.openhab.core.thing.type.ChannelDefinitionBuilder;
+import org.openhab.core.thing.type.ChannelGroupDefinition;
+import org.openhab.core.thing.type.ChannelGroupType;
+import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
+import org.openhab.core.thing.type.ChannelGroupTypeUID;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.thing.type.StateChannelTypeBuilder;
+import org.openhab.core.thing.type.ThingType;
+import org.openhab.core.thing.type.ThingTypeBuilder;
+import org.openhab.core.types.StateDescriptionFragmentBuilder;
+import org.openhab.core.types.StateOption;
+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.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ *
+ * @author Laurent Arnal - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class SiemensHvacMetadataRegistryImpl implements SiemensHvacMetadataRegistry {
+
+    private final Logger logger = LoggerFactory.getLogger(SiemensHvacMetadataRegistryImpl.class);
+
+    // A map contains data point config read from Api and/or WebPages
+    private Map<String, SiemensHvacMetadata> dptMap = new Hashtable<String, SiemensHvacMetadata>();
+    private @Nullable SiemensHvacMetadata root = null;
+    private @Nullable ArrayList<SiemensHvacMetadataDevice> devices = null;
+
+    private static final String JSON_DIR = OpenHAB.getUserDataFolder() + File.separatorChar + "siemenshvac";
+
+    private @Nullable SiemensHvacThingTypeProvider thingTypeProvider;
+    private @Nullable SiemensHvacChannelTypeProvider channelTypeProvider;
+    private @Nullable SiemensHvacChannelGroupTypeProvider channelGroupTypeProvider;
+    private @Nullable SiemensHvacConfigDescriptionProvider configDescriptionProvider;
+    private @Nullable SiemensHvacConnector hvacConnector;
+    private @Nullable SiemensHvacMetadataUser user;
+    private @Nullable Locale userLocale;
+
+    private final HashMap<String, SiemensHvacMetadataUser> userList;
+
+    public SiemensHvacMetadataRegistryImpl() {
+        userList = new HashMap<String, SiemensHvacMetadataUser>();
+    }
+
+    @Reference
+    protected void setSiemensHvacConnector(SiemensHvacConnector hvacConnector) {
+        this.hvacConnector = hvacConnector;
+    }
+
+    protected void unsetSiemensHvacConnector(SiemensHvacConnector hvacConnector) {
+        this.hvacConnector = null;
+    }
+
+    @Reference
+    protected void setThingTypeProvider(SiemensHvacThingTypeProvider thingTypeProvider) {
+        this.thingTypeProvider = thingTypeProvider;
+    }
+
+    protected void unsetThingTypeProvider(SiemensHvacThingTypeProvider thingTypeProvider) {
+        this.thingTypeProvider = null;
+    }
+
+    @Reference
+    protected void setChannelTypeProvider(SiemensHvacChannelTypeProvider channelTypeProvider) {
+        this.channelTypeProvider = channelTypeProvider;
+    }
+
+    protected void unsetChannelTypeProvider(SiemensHvacChannelTypeProvider channelTypeProvider) {
+        this.channelTypeProvider = null;
+    }
+
+    //
+    @Reference
+    protected void setChannelGroupTypeProvider(SiemensHvacChannelGroupTypeProvider channelGroupTypeProvider) {
+        this.channelGroupTypeProvider = channelGroupTypeProvider;
+    }
+
+    protected void unsetChannelGroupTypeProvider(SiemensHvacChannelGroupTypeProvider channelGroupTypeProvider) {
+        this.channelGroupTypeProvider = null;
+    }
+
+    @Reference
+    protected void setConfigDescriptionProvider(SiemensHvacConfigDescriptionProvider configDescriptionProvider) {
+        this.configDescriptionProvider = configDescriptionProvider;
+    }
+
+    protected void unsetConfigDescriptionProvider(SiemensHvacConfigDescriptionProvider configDescriptionProvider) {
+        this.configDescriptionProvider = null;
+    }
+
+    @Override
+    public @Nullable SiemensHvacConnector getSiemensHvacConnector() {
+        return this.hvacConnector;
+    }
+
+    @Override
+    public @Nullable SiemensHvacChannelTypeProvider getChannelTypeProvider() {
+        return this.channelTypeProvider;
+    }
+
+    @Override
+    public @Nullable ArrayList<SiemensHvacMetadataDevice> getDevices() {
+        return devices;
+    }
+
+    /**
+     * Initializes the type generator.
+     */
+    @Override
+    @Activate
+    public void initialize() {
+    }
+
+    public void initDptMap(@Nullable SiemensHvacMetadata node) {
+        if (node == null) {
+            return;
+        }
+
+        if (node.getClass() == SiemensHvacMetadataMenu.class) {
+            SiemensHvacMetadataMenu mInformation = (SiemensHvacMetadataMenu) node;
+
+            for (SiemensHvacMetadata child : mInformation.getChilds().values()) {
+                initDptMap(child);
+            }
+        }
+
+        if (!node.getLongDesc().isEmpty()) {
+            dptMap.put("byName" + node.getLongDesc(), node);
+        }
+        if (!node.getShortDesc().isEmpty()) {
+            dptMap.put("byName" + node.getShortDesc(), node);
+        }
+
+        dptMap.put("byId" + node.getId(), node);
+        dptMap.put("bySubId" + node.getSubId(), node);
+
+        if (node.getClass() == SiemensHvacMetadataDataPoint.class) {
+            SiemensHvacMetadataDataPoint dpi = (SiemensHvacMetadataDataPoint) node;
+            dptMap.put("byDptId" + dpi.getDptId(), node);
+        }
+    }
+
+    class ResolveCount {
+        private int resolveCount = 0;
+
+        public ResolveCount(int count) {
+            resolveCount = count;
+        }
+
+        public void decreaseResolveCount() {
+            resolveCount--;
+        }
+
+        public int getResolveCount() {
+            return resolveCount;
+        }
+    }
+
+    public void resolveDetails(int unresolveCountP) {
+        ResolveCount rv = new ResolveCount(unresolveCountP);
+
+        for (String key : dptMap.keySet()) {
+            if (key.indexOf("byId") < 0) {
+                continue;
+            }
+
+            SiemensHvacMetadata node = dptMap.get(key);
+            if (node != null) {
+                if (node.getClass() == SiemensHvacMetadataDataPoint.class) {
+                    SiemensHvacMetadataDataPoint dpi = (SiemensHvacMetadataDataPoint) node;
+                    if (!dpi.getDetailsResolved()) {
+                        resolveDptDetails(dpi, rv);
+                    }
+                }
+            }
+        }
+    }
+
+    public int unresolveCount() {
+        int count = 0;
+        for (String key : dptMap.keySet()) {
+            if (key.indexOf("byId") < 0) {
+                continue;
+            }
+
+            SiemensHvacMetadata node = dptMap.get(key);
+            if (node != null) {
+                if (node instanceof SiemensHvacMetadataDataPoint dpi) {
+                    if (!dpi.getDetailsResolved()) {
+                        count++;
+                    }
+                }
+            }
+
+        }
+
+        return count;
+    }
+
+    @Override
+    public @Nullable SiemensHvacMetadataMenu getRoot() {
+        return (SiemensHvacMetadataMenu) root;
+    }
+
+    @Override
+    public void readMeta() throws SiemensHvacException {
+        ArrayList<SiemensHvacMetadataDevice> lcDevices = devices;
+        SiemensHvacConnector lcHvacConnector = hvacConnector;
+
+        if (root != null) {
+            return;
+        }
+
+        if (lcHvacConnector == null) {
+            logger.debug("SiemensHvacMetadataRegistryImpl:ReadMeta(): lHvacConnector not initialize.");
+            return;
+        }
+
+        readUserInfo();
+
+        SiemensHvacBridgeConfig config = lcHvacConnector.getBridgeConfiguration();
+        if (config == null) {
+            throw new SiemensHvacException("@offline.config-not-init");
+        }
+
+        SiemensHvacMetadataUser lcUser = null;
+
+        String userName = config.userName;
+        if (userList.containsKey(userName)) {
+            lcUser = userList.get(userName);
+        }
+
+        if (lcUser == null) {
+            throw new SiemensHvacException("@offline.user-not-find");
+        }
+
+        Map<String, Locale> map = new HashMap<String, Locale>();
+        map.put("English", Locale.forLanguageTag("en-UK"));
+        map.put("Deutsch", Locale.forLanguageTag("de-DE"));
+        map.put("Francais", Locale.forLanguageTag("fr-FR"));
+        map.put("Italiano", Locale.forLanguageTag("it-IT"));
+        map.put("Nederlands", Locale.forLanguageTag("nl-NL"));
+        map.put("Polski", Locale.forLanguageTag("pl-PL"));
+        map.put("Ceski", Locale.forLanguageTag("cs-CZ"));
+        map.put("Magyar", Locale.forLanguageTag("hu-HU"));
+        map.put("Espagnol", Locale.forLanguageTag("es-ES"));
+        map.put("Dansk", Locale.forLanguageTag("da-DK"));
+        map.put("Svenska", Locale.forLanguageTag("sv-SE"));
+        map.put("Suomi", Locale.forLanguageTag("fi-FI"));
+        map.put("Portugues", Locale.forLanguageTag("pt-PT"));
+        map.put("Russkij", Locale.forLanguageTag("ru-RU"));
+        map.put("Turkce", Locale.forLanguageTag("tr-TR"));
+        map.put("Slovensky", Locale.forLanguageTag("sl-SV"));
+
+        this.user = lcUser;
+        if (map.containsKey(lcUser.getLanguage())) {
+            this.userLocale = map.get(lcUser.getLanguage());
+        } else {
+            this.userLocale = Locale.getDefault();
+        }
+
+        logger.trace("siemensHvac:Initialization():Begin_0001");
+
+        File folder = new File(JSON_DIR);
+
+        if (!folder.exists()) {
+            logger.debug("Creating directory {}", folder);
+            folder.mkdirs();
+        }
+
+        logger.trace("siemensHvac:Initialization():ReadCache");
+        loadMetaDataFromCache();
+
+        // increase the timeout during this phase
+        // because we queued a lot of request
+        // and timeout start to run when request is queued (not executed)
+
+        Instant start = Instant.now();
+        lcHvacConnector.setTimeOut(600);
+
+        logger.trace("siemensHvac:Initialization():ReadDeviceList");
+        readDeviceList();
+
+        if (root == null) {
+            logger.trace("siemensHvac:Initialization():No cache information, root==null, reading metadata from device");
+
+            logger.trace("siemensHvac:Initialization():BeginReadMenu");
+            root = new SiemensHvacMetadataMenu();
+
+            changeLanguage(lcUser, 1);
+            readMetaData(root, -1, false);
+            lcHvacConnector.waitNoNewRequest();
+            lcHvacConnector.waitAllPendingRequest();
+
+            changeLanguage(lcUser, lcUser.getLanguageId());
+            readMetaData(root, -1, true);
+            lcHvacConnector.waitNoNewRequest();
+            lcHvacConnector.waitAllPendingRequest();
+
+            logger.trace("siemensHvac:Initialization():EndReadMenu");
+        }
+
+        if (root != null) {
+            logger.trace("siemensHvac:Initialization():BeginInitDptMap");
+            initDptMap(root);
+            logger.trace("siemensHvac:Initialization():EndInitDptMap");
+        }
+
+        int unresolveCount = unresolveCount();
+
+        while (unresolveCount > 0) {
+            logger.trace("siemensHvac:Initialization():BeginResolveDtpMap {}", unresolveCount);
+            resolveDetails(unresolveCount);
+            lcHvacConnector.waitAllPendingRequest();
+            unresolveCount = unresolveCount();
+        }
+
+        Instant end = Instant.now();
+        lcHvacConnector.setTimeOut(30);
+
+        long elapseTime = Duration.between(start, end).toSeconds();
+        logger.trace("siemensHvac:Initialization():ReadMetadata in {} s", elapseTime);
+
+        logger.trace("siemensHvac:Initialization():SaveCache");
+        saveMetaDataToCache();
+
+        logger.trace("siemensHvac:Initialization():InitThing");
+        getRoot();
+        lcDevices = devices;
+        if (lcDevices != null) {
+            for (SiemensHvacMetadataDevice device : lcDevices) {
+                generateThingsType(device);
+            }
+        }
+
+        logger.trace("siemensHvac:InitDptMap():end");
+    }
+
+    @Override
+    public @Nullable SiemensHvacMetadataUser getUser() {
+        return user;
+    }
+
+    @Override
+    public @Nullable Locale getUserLocale() {
+        return userLocale;
+    }
+
+    private void generateThingsType(SiemensHvacMetadataDevice device) {
+        SiemensHvacThingTypeProvider lcThingTypeProvider = thingTypeProvider;
+        logger.debug("Generate thing types for device: {} / {}", device.getName(), device.getSerialNr());
+        if (lcThingTypeProvider != null) {
+            ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
+            ThingType tt = null;
+
+            tt = lcThingTypeProvider.getInternalThingType(thingTypeUID);
+
+            if (tt == null) {
+                List<ChannelGroupType> groupTypes = new ArrayList<>();
+
+                int treeId = device.getTreeId();
+                if (dptMap.containsKey("byId" + treeId)) {
+                    SiemensHvacMetadataMenu menu = (SiemensHvacMetadataMenu) dptMap.get("byId" + treeId);
+
+                    if (menu != null) {
+                        var childs = menu.getChilds().values();
+                        for (SiemensHvacMetadata child : childs) {
+                            generateThingsType(child, groupTypes, menu);
+                        }
+
+                    }
+
+                }
+
+                tt = createThingType(device, groupTypes);
+                lcThingTypeProvider.addThingType(tt);
+            }
+        }
+    }
+
+    private void generateThingsType(SiemensHvacMetadata child, List<ChannelGroupType> groupTypes,
+            SiemensHvacMetadataMenu menu) {
+        SiemensHvacChannelTypeProvider lcChannelTypeProvider = channelTypeProvider;
+        SiemensHvacChannelGroupTypeProvider lcChannelGroupTypeProvider = channelGroupTypeProvider;
+
+        if (child instanceof SiemensHvacMetadataMenu subMenu) {
+            List<ChannelDefinition> channelDefinitions = new ArrayList<>();
+
+            for (SiemensHvacMetadata childDt : subMenu.getChilds().values()) {
+
+                try {
+                    if (childDt instanceof SiemensHvacMetadataMenu) {
+                        generateThingsType(childDt, groupTypes, menu);
+                    }
+                    if (childDt instanceof SiemensHvacMetadataDataPoint metadataDataPoint) {
+                        if (metadataDataPoint.getDptType().isEmpty()) {
+                            continue;
+                        }
+
+                        ChannelTypeUID channelTypeUID = UidUtils.generateChannelTypeUID(metadataDataPoint);
+
+                        ChannelType channelType = null;
+
+                        if (channelTypeProvider != null && lcChannelTypeProvider != null) {
+                            channelType = lcChannelTypeProvider.getInternalChannelType(channelTypeUID);
+                            if (channelType == null) {
+                                channelType = createChannelType(metadataDataPoint, channelTypeUID);
+                                lcChannelTypeProvider.addChannelType(channelType);
+                            }
+                        }
+
+                        Map<String, String> props = new Hashtable<String, String>();
+                        props.put("dptId", "" + metadataDataPoint.getDptId());
+                        props.put("id", "" + metadataDataPoint.getId());
+                        props.put("subId", "" + metadataDataPoint.getSubId());
+                        props.put("groupdId", "" + metadataDataPoint.getGroupId());
+
+                        String id = metadataDataPoint.getId() + "-"
+                                + UidUtils.sanetizeId(metadataDataPoint.getShortDesc());
+
+                        if (channelType != null) {
+                            ChannelDefinition channelDef = new ChannelDefinitionBuilder(id, channelType.getUID())
+                                    .withLabel(metadataDataPoint.getShortDesc())
+                                    .withDescription(metadataDataPoint.getLongDesc()).withProperties(props).build();
+
+                            channelDefinitions.add(channelDef);
+                        }
+                    }
+                } catch (SiemensHvacException ex) {
+                    logger.warn("Unable to create channel for: {}", childDt);
+                }
+            }
+
+            // generate group
+            ChannelGroupTypeUID groupTypeUID = UidUtils.generateChannelGroupTypeUID(subMenu);
+            ChannelGroupType groupType = null;
+
+            if (lcChannelGroupTypeProvider != null) {
+                groupType = lcChannelGroupTypeProvider.getInternalChannelGroupType(groupTypeUID);
+
+                if (groupType == null) {
+                    String groupLabel = subMenu.getShortDesc();
+                    groupType = ChannelGroupTypeBuilder.instance(groupTypeUID, groupLabel)
+                            .withChannelDefinitions(channelDefinitions).withCategory("")
+                            .withDescription(menu.getLongDesc()).build();
+                    lcChannelGroupTypeProvider.addChannelGroupType(groupType);
+                    groupTypes.add(groupType);
+                }
+            }
+
+        }
+    }
+
+    private ChannelType createChannelType(SiemensHvacMetadataDataPoint dpt, ChannelTypeUID channelTypeUID)
+            throws SiemensHvacException {
+        ChannelType channelType;
+
+        String itemType = getItemType(dpt);
+        String category = getCategory(dpt);
+        String label = itemType;
+        String description = "";
+
+        StateDescriptionFragmentBuilder stateFragment = StateDescriptionFragmentBuilder.create();
+
+        List<StateOption> options = new ArrayList<StateOption>();
+        if (dpt.getDptType().equals(SiemensHvacBindingConstants.DPT_TYPE_ENUM)) {
+            StringBuilder descBuilder = new StringBuilder();
+            descBuilder.append("Enum:");
+            List<SiemensHvacMetadataPointChild> childs = dpt.getChild();
+            int idx = 0;
+
+            for (SiemensHvacMetadataPointChild opt : childs) {
+                StateOption stOpt = new StateOption(opt.getValue(), opt.getText());
+                options.add(stOpt);
+                if (idx > 0) {
+                    descBuilder.append("_");
+                }
+
+                descBuilder.append(String.format("(%s:%s)", opt.getValue(), opt.getText()));
+                idx++;
+            }
+            description = descBuilder.toString();
+            label = channelTypeUID.getId();
+        } else if (dpt.getDptType().equals(SiemensHvacBindingConstants.DPT_TYPE_RADIO)) {
+            StringBuilder descBuilder = new StringBuilder();
+            descBuilder.append("Radio:");
+            SiemensHvacMetadataPointChild child = dpt.getChild().get(0);
+
+            if (dpt.getWriteAccess()) {
+                StateOption stOpt0 = new StateOption("OFF", child.getOpt0());
+                descBuilder.append(String.format("(%s:%s)", "OFF", child.getOpt0()));
+                options.add(stOpt0);
+
+                StateOption stOpt1 = new StateOption("ON", child.getOpt1());
+                descBuilder.append(String.format("(%s:%s)", "ON", child.getOpt1()));
+                options.add(stOpt1);
+            } else {
+                StateOption stOpt0 = new StateOption("CLOSED", child.getOpt0());
+                descBuilder.append(String.format("(%s:%s)", "OFF", child.getOpt0()));
+                options.add(stOpt0);
+
+                StateOption stOpt1 = new StateOption("OPEN", child.getOpt1());
+                descBuilder.append(String.format("(%s:%s)", "ON", child.getOpt1()));
+                options.add(stOpt1);
+            }
+
+            description = descBuilder.toString();
+            label = channelTypeUID.getId();
+        } else if (dpt.getDptType().equals(SiemensHvacBindingConstants.DPT_TYPE_NUMERIC)) {
+            BigDecimal min = new BigDecimal(dpt.getMin());
+            BigDecimal max = new BigDecimal(dpt.getMax());
+            BigDecimal step = new BigDecimal(dpt.getResolution());
+
+            stateFragment = stateFragment.withPattern(getStatePattern(dpt)).withMinimum(min).withMaximum(max)
+                    .withStep(step).withReadOnly(false);
+
+            description = channelTypeUID.toString();
+            label = channelTypeUID.getId();
+        } else {
+            stateFragment = stateFragment.withPattern(getStatePattern(dpt)).withReadOnly(!dpt.getWriteAccess());
+        }
+
+        if (!options.isEmpty()) {
+            stateFragment = stateFragment.withOptions(options);
+        }
+
+        boolean isAdvanced = false;
+        if (channelTypeUID.getId().contains("-y")) {
+            isAdvanced = true;
+        }
+        if (channelTypeUID.getId().contains("-k")) {
+            isAdvanced = true;
+        }
+        if (channelTypeUID.getId().contains("histo")) {
+            isAdvanced = true;
+        }
+        if (channelTypeUID.getId().contains("-qx")) {
+            isAdvanced = true;
+        }
+
+        final StateChannelTypeBuilder channelTypeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
+                .withStateDescriptionFragment(stateFragment.build());
+
+        channelType = channelTypeBuilder.isAdvanced(isAdvanced).withDescription(description).withCategory(category)
+                .build();
+
+        return channelType;
+    }
+
+    /**
+     * Creates the ThingType for the given device.
+     */
+    private ThingType createThingType(SiemensHvacMetadataDevice device, List<ChannelGroupType> groupTypes) {
+        SiemensHvacConfigDescriptionProvider lcConfigDescriptionProvider = configDescriptionProvider;
+        String name = device.getName();
+        String description = device.getName();
+
+        List<String> supportedBridgeTypeUids = new ArrayList<>();
+        supportedBridgeTypeUids.add(SiemensHvacBindingConstants.THING_TYPE_OZW.toString());
+        ThingTypeUID thingTypeUID = UidUtils.generateThingTypeUID(device);
+
+        Map<String, String> properties = new HashMap<>();
+        properties.put(Thing.PROPERTY_VENDOR, SiemensHvacBindingConstants.PROPERTY_VENDOR_NAME);
+        properties.put(Thing.PROPERTY_MODEL_ID, device.getType());
+
+        URI configDescriptionURI = getConfigDescriptionURI(device);
+        if (lcConfigDescriptionProvider != null
+                && lcConfigDescriptionProvider.getInternalConfigDescription(configDescriptionURI) == null) {
+            generateConfigDescription(device, groupTypes, configDescriptionURI);
+        }
+
+        List<ChannelGroupDefinition> groupDefinitions = new ArrayList<>();
+        for (ChannelGroupType groupType : groupTypes) {
+            String id = groupType.getUID().getId();
+            groupDefinitions.add(new ChannelGroupDefinition(id, groupType.getUID()));
+        }
+
+        return ThingTypeBuilder.instance(thingTypeUID, name).withSupportedBridgeTypeUIDs(supportedBridgeTypeUids)
+                .withDescription(description).withChannelGroupDefinitions(groupDefinitions).withProperties(properties)
+                .withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withConfigDescriptionURI(configDescriptionURI)
+                .withCategory(SiemensHvacBindingConstants.CATEGORY_THING_HVAC).build();
+    }
+
+    private URI getConfigDescriptionURI(SiemensHvacMetadataDevice device) {
+        return URI.create((String.format("%s:%s", SiemensHvacBindingConstants.CONFIG_DESCRIPTION_URI_THING_PREFIX,
+                UidUtils.generateThingTypeUID(device))));
+    }
+
+    private void generateConfigDescription(SiemensHvacMetadataDevice device, List<ChannelGroupType> groupTypes,
+            URI configDescriptionURI) {
+        SiemensHvacConfigDescriptionProvider lcConfigDescriptionProvider = configDescriptionProvider;
+        List<ConfigDescriptionParameter> parms = new ArrayList<>();
+        List<ConfigDescriptionParameterGroup> groups = new ArrayList<>();
+
+        if (lcConfigDescriptionProvider != null) {
+            lcConfigDescriptionProvider.addConfigDescription(ConfigDescriptionBuilder.create(configDescriptionURI)
+                    .withParameters(parms).withParameterGroups(groups).build());
+        }
+    }
+
+    public String getItemType(SiemensHvacMetadataDataPoint dpt) throws SiemensHvacException {
+        try {
+            TypeConverter tp = ConverterFactory.getConverter(dpt.getDptType());
+            return tp.getItemType(dpt);
+        } catch (ConverterTypeException ex) {
+            throw new SiemensHvacException(String.format("Can't find convertor for type: %s", dpt.getDptType()), ex);
+        }
+    }
+
+    /**
+     * Determines the category for the given Datapoint.
+     */
+    public static String getCategory(SiemensHvacMetadataDataPoint dp) {
+        String dpType = dp.getDptType();
+        String dptUnit = dp.getDptUnit();
+
+        if (dptUnit == null) {
+            return "";
+        } else if (dptUnit.contains("°C")) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_TEMP;
+        } else if (dptUnit.contains("°F")) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_TEMP;
+        } else if (dpType.contains(SiemensHvacBindingConstants.DPT_TYPE_DATE_TIME)) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_TIME;
+        } else if (dpType.contains(SiemensHvacBindingConstants.DPT_TYPE_TIMEOFDAY)) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_TIME;
+        } else if (dpType.contains(SiemensHvacBindingConstants.DPT_TYPE_ENUM)) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_SWITCH;
+        } else if (dpType.contains(SiemensHvacBindingConstants.DPT_TYPE_RADIO)) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_SWITCH;
+        } else if (dpType.contains(SiemensHvacBindingConstants.DPT_TYPE_NUMERIC)) {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_NUMBER;
+        } else {
+            return SiemensHvacBindingConstants.CATEGORY_CHANNEL_CONTROL_HEATING;
+        }
+    }
+
+    /**
+     * Returns the state pattern metadata string with unit for the given Datapoint.
+     */
+    public static String getStatePattern(SiemensHvacMetadataDataPoint dpt) {
+        String unit = dpt.getDptUnit();
+
+        if ("%".equals(unit)) {
+            return "%d %%";
+        }
+
+        int digits = 0;
+        if (dpt.getDptType().equals(SiemensHvacBindingConstants.DPT_TYPE_NUMERIC)) {
+            String digitSt = dpt.getDecimalDigits();
+            if (digitSt != null && !"".equals(digitSt)) {
+                digits = Integer.parseInt(digitSt);
+            }
+        }
+
+        if (unit != null && !unit.isEmpty()) {
+            return String.format("%s %s", "%." + digits + "f", "%unit%");
+        } else {
+            return String.format("%s", "%." + digits + "f");
+        }
+    }
+
+    public void readUserInfo() throws SiemensHvacException {
+        SiemensHvacConnector lcHvacConnector = hvacConnector;
+        String request = "main.app?section=settings&subsection=user";
+
+        if (lcHvacConnector != null) {
+            String response = lcHvacConnector.doBasicRequest(request);
+
+            if (response != null) {
+                String st = response;
+                st = st.replace("\n", "");
+
+                Pattern pattern1 = Pattern.compile("table class=\\\"user_table\\\".*?>(.*?)<\\/table>");
+                Matcher matcher1 = pattern1.matcher(st);
+
+                if (matcher1.find()) {
+                    String userTable = matcher1.group(1);
+
+                    Pattern pattern2 = Pattern.compile("<tr.*?>(.*?)<\\/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(.*?)>(.*?)<\\/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("<select name=\\\"language\\\".*>((.*|\\n)*?)</select>",
+                                    Pattern.MULTILINE);
+                            Matcher matcher5 = pattern5.matcher(response);
+
+                            if (matcher5.find()) {
+                                String optionsList = matcher5.group(1);
+
+                                Pattern pattern6 = java.util.regex.Pattern
+                                        .compile("<option value=\\\"([^ ]*)\\\"(.*)>(.*)</option>", 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<SiemensHvacMetadataDevice> lcDevices = devices;
+
+            lcDevices = new ArrayList<SiemensHvacMetadataDevice>();
+            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<String, SiemensHvacMetadataDataPoint> idMap = new Hashtable<String, SiemensHvacMetadataDataPoint>();
+
+            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 (file)
index 0000000..64647fd
--- /dev/null
@@ -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 (file)
index 0000000..32766a7
--- /dev/null
@@ -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 (file)
index 0000000..3b5583c
--- /dev/null
@@ -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 (file)
index 0000000..eaa5bc6
--- /dev/null
@@ -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<SiemensHvacRequestHandler, SiemensHvacRequestHandler> currentHandlerRegistry = new ConcurrentHashMap<>();
+    private Map<SiemensHvacRequestHandler, SiemensHvacRequestHandler> handlerInErrorRegistry = new ConcurrentHashMap<>();
+
+    private Map<String, Boolean> 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<String, Type> 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<SiemensHvacMetadata> adapter = RuntimeTypeAdapterFactory
+                .of(SiemensHvacMetadata.class);
+        adapter.registerSubtype(SiemensHvacMetadataMenu.class);
+        adapter.registerSubtype(SiemensHvacMetadataDataPoint.class);
+
+        gsonWithAdapter = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
+
+        this.updateCommand = new Hashtable<String, Type>();
+        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<HttpCookie> 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 (file)
index 0000000..6c14de9
--- /dev/null
@@ -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 (file)
index 0000000..770eb43
--- /dev/null
@@ -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("<!DOCTYPE html>") >= 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 (file)
index 0000000..a6489bc
--- /dev/null
@@ -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 (file)
index 0000000..364a631
--- /dev/null
@@ -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<ChannelGroupTypeUID, ChannelGroupType> 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<ChannelGroupType> getChannelGroupTypes(@Nullable Locale locale) {
+        Collection<ChannelGroupType> 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 (file)
index 0000000..6f5e16e
--- /dev/null
@@ -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<br>
+     *         <i>null</i> 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 (file)
index 0000000..234bd4a
--- /dev/null
@@ -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<ChannelTypeUID, ChannelType> channelTypesByUID = new HashMap<>();
+
+    public SiemensHvacChannelTypeProviderImpl() {
+    }
+
+    @Override
+    public void addChannelType(ChannelType channelType) {
+        channelTypesByUID.put(channelType.getUID(), channelType);
+    }
+
+    @Override
+    public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
+        Collection<ChannelType> 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 (file)
index 0000000..a16ec5f
--- /dev/null
@@ -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 (file)
index 0000000..79cfb5a
--- /dev/null
@@ -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<URI, ConfigDescription> configDescriptionsByURI = new HashMap<>();
+
+    @Override
+    public Collection<ConfigDescription> getConfigDescriptions(@Nullable Locale locale) {
+        Collection<ConfigDescription> 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 (file)
index 0000000..080fc35
--- /dev/null
@@ -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 (file)
index 0000000..5fac0e0
--- /dev/null
@@ -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<br>
+     *         <i>null</i> 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 (file)
index 0000000..b8895e7
--- /dev/null
@@ -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<ThingTypeUID, ThingType> 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<ThingType> getThingTypes(@Nullable Locale locale) {
+        Map<ThingTypeUID, ThingType> 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 (file)
index 0000000..c7c994c
--- /dev/null
@@ -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 (file)
index 0000000..e0f1553
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon xmlns:addon="https://openhab.org/schemas/addon/v1.0.0" id="siemenshvac"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+       <type>binding</type>
+       <name>SiemensHvac Binding</name>
+       <description>This is the binding for SiemensHvac.</description>
+       <connection>local</connection>
+       <discovery-methods>
+               <discovery-method>
+                       <service-type>upnp</service-type>
+                       <match-properties>
+                               <match-property>
+                                       <name>manufacturer</name>
+                                       <regex>Siemens.*</regex>
+                               </match-property>
+                               <match-property>
+                                       <name>modelName</name>
+                                       <regex>Web Server OZW.*</regex>
+                               </match-property>
+                       </match-properties>
+               </discovery-method>
+       </discovery-methods>
+</addon:addon>
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 (file)
index 0000000..3703557
--- /dev/null
@@ -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 (file)
index 0000000..7b43f25
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="siemenshvac"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Bridge Thing Type -->
+       <bridge-type id="ozw">
+               <label>OZW IP Gateway</label>
+               <description>This is a OZW IP interface</description>
+
+               <config-description>
+                       <parameter name="baseUrl" type="text" pattern="(http|https):\/\/(.+)\/">
+                               <label>Base URL</label>
+                               <context>url</context>
+                               <description>The URL of the Siemens Hvac IP gateway. Must be in format http://hostname/ or https://hostname/. Don't
+                                       forget the trailing '/'</description>
+                               <required>true</required>
+                       </parameter>
+                       <parameter name="userName" type="text">
+                               <description>User name of the Siemens Hvac gateway</description>
+                               <required>false</required>
+                               <label>User Name</label>
+                               <default>Administrator</default>
+                       </parameter>
+                       <parameter name="userPassword" type="text">
+                               <context>password</context>
+                               <description>User password of the Siemens Hvac gateway</description>
+                               <required>false</required>
+                               <label>User Password</label>
+                               <default>password</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+</thing:thing-descriptions>
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 (file)
index 0000000..b94428d
--- /dev/null
@@ -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");
+    }
+}
index b51354c2c24d87076d9ab1d56b0c6603407d44a0..feae03ff21f7e60ab78ce9ac6ed881ab93aecb56 100644 (file)
     <module>org.openhab.binding.serialbutton</module>
     <module>org.openhab.binding.shelly</module>
     <module>org.openhab.binding.silvercrestwifisocket</module>
+    <module>org.openhab.binding.siemenshvac</module>
     <module>org.openhab.binding.siemensrds</module>
     <module>org.openhab.binding.sinope</module>
     <module>org.openhab.binding.sleepiq</module>