<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>
--- /dev/null
+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
--- /dev/null
+# SiemensHVAC Binding
+
+This binding provides support for the Siemens HVAC controller ecosystem, and the Web Gateway interface OZW672.
+A typical system is composed of:
+
+
+
+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.
+
+
+
+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" }
+
+```
--- /dev/null
+<?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>
--- /dev/null
+/*
+ * 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>
+ * {
+ * @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>
+ * {
+ * @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>
+ * {
+ * @code
+ * Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
+ * }
+ * </pre>
+ *
+ * Like {@code GsonBuilder}, this API supports chaining:
+ *
+ * <pre>
+ * {
+ * @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);
+ }
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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() {
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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();
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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()));
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+# 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
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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");
+ }
+}
<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>