]> git.basschouten.com Git - openhab-addons.git/commitdiff
[wolfsmartset] Initial contribution (#10751)
authorBo Biene <bo_biene@hotmail.com>
Sat, 25 Sep 2021 19:07:54 +0000 (21:07 +0200)
committerGitHub <noreply@github.com>
Sat, 25 Sep 2021 19:07:54 +0000 (21:07 +0200)
Signed-off-by: Bo Biene <openhab.github@biene.eu>
38 files changed:
CODEOWNERS
bundles/org.openhab.binding.wolfsmartset/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/README.md [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/WolfSmartsetBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/WolfSmartsetHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/api/WolfSmartsetApi.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/api/WolfSmartsetCloudException.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetAccountConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetSystemConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetUnitConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/discovery/WolfSmartsetAccountDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/discovery/WolfSmartsetSystemDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/CreateSession2DTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/CurrentMessageDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GatewayStateDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetGuiDescriptionForGatewayDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetParameterValuesDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetSystemListDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetSystemStateListDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/HistoryMessageDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/LoginResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/MenuItemDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/MenuItemTabViewDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ParameterDescriptorDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ReadFaultMessagesDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/SubMenuEntryDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/SubMenuEntryWithMenuItemTabView.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ValueDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetAccountBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetSystemBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetUnitThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index a9bb24764bb513b2e6ae3f0e80ab86735bdac035..1913885694c0ed79d23b1d1cf78b572f3359e9b6 100644 (file)
 /bundles/org.openhab.binding.windcentrale/ @marcelrv
 /bundles/org.openhab.binding.wlanthermo/ @CSchlipp
 /bundles/org.openhab.binding.wled/ @Skinah
+/bundles/org.openhab.binding.wolfsmartset/ @BoBiene
 /bundles/org.openhab.binding.xmltv/ @clinique
 /bundles/org.openhab.binding.xmppclient/ @pavel-gololobov
 /bundles/org.openhab.binding.yamahareceiver/ @davidgraeff @zarusz
diff --git a/bundles/org.openhab.binding.wolfsmartset/NOTICE b/bundles/org.openhab.binding.wolfsmartset/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.wolfsmartset/README.md b/bundles/org.openhab.binding.wolfsmartset/README.md
new file mode 100644 (file)
index 0000000..ffd0a43
--- /dev/null
@@ -0,0 +1,149 @@
+# Wolf Smartset Binding
+
+This binding communicates with the www.wolf-smartset.de API and provides values readonly. 
+Wolf systems are connected with official gateways (Wolf Link Home or Wolf Link Pro) https://www.wolf.eu/produkte/smarthome/ 
+
+## Supported Things
+
+- Account (``thing-type:wolfsmartset:account``)
+    * holding the credentials to connect to the wolf-smartset online portal.
+- System (``thing-type:wolfsmartset:system``)
+    *  represents one wolf system connected to the wolf-smartset online portal.
+- Unit (``thing-type:wolfsmartset:unit``)
+    * unit is a part of the system with values and parameter
+
+## Discovery
+
+- System things (bridge) are discovered after Account thing (bridge) is set up
+- Unit things are discovered after System things are set up
+
+## Thing Configuration
+
+### Account (bridge)
+
+The account thing holds the credentials to connect to the wolf-smartset online portal.
+
+| Parameter       | Type    | Defaut | Description                                                         |
+|-----------------|---------|----------|---------------------------------------------------------------------|
+| username | text | | username to authenticate to www.wolf-smartset.de |
+| password | text  | | password to authenticate to www.wolf-smartset.de |
+| refreshIntervalStructure | integer | 10 | Specifies the refresh interval to refresh the Structure in minutes |
+| refreshIntervalValues | integer | 15 | Specifies time in seconds to refresh values |
+| discoveryEnabled | boolean | true | disable the Thing discovery |
+
+### System (bridge)
+
+The system thing represents one wolf system connected via a WOLF Link home or a WOLF Link pro to the wolf-smartset online portal. 
+You have access to your own or to shared systems. 
+
+| Parameter       | Type    | Defaut | Description                                                         |
+|-----------------|---------|----------|---------------------------------------------------------------------|
+| systemId | integer | | System ID assigned to the system by WolfSmartset |
+
+### Unit
+
+A system is divided into different units. 
+In the wolf-smartset portal, the system has an "Expert" section, each submenu item within the "Expert" section has multiple tabs.
+Each of these tabs is treated as one unit.
+
+| Parameter       | Type    | Defaut | Description                                                         |
+|-----------------|---------|----------|---------------------------------------------------------------------|
+| unitId | integer | | The BundleId assigned to the unit by WolfSmartset |
+
+## Tested WOLF-Devices
+
+| WOLF Equipment    | openhab Version | Used gateway  |
+|-------------------|-----------------|---------------|
+| CSZ (CGB and SM1) | 3.1             | WOLF Link Pro |
+| CGB-2             | 3.1             | WOLF Link home|
+
+
+## Channels
+
+| channel  | type   | description                  |
+|----------|--------|------------------------------|
+| number  | Number | a generic number  |
+| contact  | Contact | a generic contact  |
+| temperature  | Number:Temperature | a generic temperature  |
+| string  | String | a generic String  |
+| datetime  | DateTime | a generic DateTime  |
+
+## Full Example
+
+### Things
+
+````
+Bridge wolfsmartset:account:account "Wolf Smartset Account" [ username="User", password="Password" ] {
+    Bridge system 32122305166 "WolfSmartset System CSZ" [ systemId="32122305166" ] {
+        Thing unitId uinit0 "CSZ Heizgerät" [ unitId="unit0" ] {
+        }
+    }
+}
+````
+_You need to use the corrosponding systemId and unitId returned by the discovery_
+
+### Items
+
+````
+"Number CSZHeizgerat_Raumtemperatur "Raumtemperatur" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900000"}
+Number CSZHeizgerat_Flamme "Flamme" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900001"}
+Number CSZHeizgerat_AnalogeFernbedienung "Analoge Fernbedienung" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900002"}
+Number CSZHeizgerat_Raumsolltemperatur "Raumsolltemperatur" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900003"}
+Number CSZHeizgerat_AusgangA1 "Ausgang A1" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900004"}
+String CSZHeizgerat_ZeitprogrammdirekterHeizkreis "Zeitprogramm direkter Heizkreis" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900005"}
+Number CSZHeizgerat_Ventil1 "Ventil 1" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900006"}
+Number CSZHeizgerat_Ventil2 "Ventil 2" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900007"}
+Number CSZHeizgerat_WiSoUmschaltung "Wi/So Umschaltung" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900008"}
+Number CSZHeizgerat_Tagtemperatur "Tagtemperatur" { channel="wolfsmartset:unit:account:32122305166:uinit0:1000900009"}
+Number CSZHeizgerat_PWMPumpe "PWM Pumpe" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000010"}
+Number CSZHeizgerat_Speichersolltemperatur "Speichersolltemperatur" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000011"}
+Number CSZHeizgerat_Heizkurve "Heizkurve" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000012"}
+Number CSZHeizgerat_Raumeinfluss "Raumeinfluss" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000013"}
+Number CSZHeizgerat_TWVorlauf "TW-Vorlauf" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000014"}
+Number CSZHeizgerat_Spartemperatur "Spartemperatur" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000015"}
+Number CSZHeizgerat_Geblase "Gebläse" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000016"}
+Number CSZHeizgerat_Vorlaufsolltemperatur "Vorlaufsolltemperatur" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000017"}
+Group CSZHeizgerat "CSZ Heizgerät" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000018"}
+Number CSZHeizgerat_ECOABS "ECO/ABS" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000019"}
+Number CSZHeizgerat_Netzbetriebstunden "Netzbetriebstunden" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000020"}
+Number CSZHeizgerat_TWAbgas "TW-Abgas" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000021"}
+Number CSZHeizgerat_HGStatus "HG Status" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000022"}
+Number CSZHeizgerat_EingangE1 "Eingang E1" { channel="wolfsmartset:unit:account:32122305166:uinit0:10009000023"}"
+````
+
+## Supported Heating-Devices
+
+All devices able to be connected to www.wolf-smartset.de
+
+### Related Documentation from WOLF
+
+https://www.wolf.eu/fileadmin/Wolf_Daten/Dokumente/FAQ/3065655_201711.pdf
+
+| Heating system                            | WOLF Link home        | WOLF Link pro      |
+|-------------------------------------------|-----------------------|--------------------|
+| Gas condensing boiler CGB-2, CGW-2, CGS-2 | ✅ | ✅ |
+| Oil condensing boiler TOB | ✅ | ✅ |
+| MGK-2 gas condensing boiler | ✅ | ✅ |
+| split air/water heat pump BWL-1S | ✅ | ✅ |
+| Oil condensing boiler COB |  | ✅ |
+| gas condensing boiler MGK |   | ✅ |
+| Gas condensing boilers CGB, CGW, CGS, FGB |   | ✅ |
+| Gas condensing boilers CGG-2, CGU-2 |   | ✅ |
+| Boiler controls R2, R3, R21 |   | ✅ |
+| Monobloc heat pumps BWW-1, BWL-1, BWS-1 |   | ✅ |
+| mixer module MM, MM-2 | ⬜ | ✅ |
+| cascade module KM, KM-2 | ⬜ | ✅ |
+| solar modules SM1, SM1-2, SM-2, SM2-2 | ⬜ | ✅ |
+| Comfort apartment ventilation CWL Excellent | ⬜ | ✅ |
+| Air handling units KG Top, CKL Pool``*`` |   | ✅ |
+| Air handling units CKL, CFL, CRL``*`` |   | ✅ |
+| Combined heat and power units | | ✅ |
+
+
+Note: 
+
+⬜ possible in connection with a WOLF Link home compatible heater,
+full functionality only for devices with current software version.
+
+``*`` Modbus interface required in the device,
+Special programming cannot be mapped.
diff --git a/bundles/org.openhab.binding.wolfsmartset/pom.xml b/bundles/org.openhab.binding.wolfsmartset/pom.xml
new file mode 100644 (file)
index 0000000..bfaa738
--- /dev/null
@@ -0,0 +1,17 @@
+<?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>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.wolfsmartset</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: WolfSmartset Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/feature/feature.xml b/bundles/org.openhab.binding.wolfsmartset/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..ba34a81
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.wolfsmartset-${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-wolfsmartset" description="Wolf Smartset Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.wolfsmartset/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/WolfSmartsetBindingConstants.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/WolfSmartsetBindingConstants.java
new file mode 100644 (file)
index 0000000..61765b9
--- /dev/null
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.SubMenuEntryWithMenuItemTabView;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link WolfSmartsetBindingConstants} class defines common constants that are
+ * used across the whole binding.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetBindingConstants {
+
+    public static final String BINDING_ID = "wolfsmartset";
+
+    // Account bridge
+    public static final String THING_TYPE_ACCOUNT = "account";
+    public static final ThingTypeUID UID_ACCOUNT_BRIDGE = new ThingTypeUID(BINDING_ID, THING_TYPE_ACCOUNT);
+    public static final Set<ThingTypeUID> SUPPORTED_ACCOUNT_BRIDGE_THING_TYPES_UIDS = Collections
+            .unmodifiableSet(Stream.of(UID_ACCOUNT_BRIDGE).collect(Collectors.toSet()));
+
+    // System bridge
+    public static final String THING_TYPE_SYSTEM = "system";
+    public static final ThingTypeUID UID_SYSTEM_BRIDGE = new ThingTypeUID(BINDING_ID, THING_TYPE_SYSTEM);
+    public static final Set<ThingTypeUID> SUPPORTED_SYSTEM_BRIDGE_THING_TYPES_UIDS = Collections
+            .unmodifiableSet(Stream.of(UID_SYSTEM_BRIDGE).collect(Collectors.toSet()));
+
+    // unit thing
+    public static final String THING_TYPE_UNIT = "unit";
+    public static final ThingTypeUID UID_UNIT_THING = new ThingTypeUID(BINDING_ID, THING_TYPE_UNIT);
+    public static final Set<ThingTypeUID> SUPPORTED_UNIT_THING_TYPES_UIDS = Collections
+            .unmodifiableSet(Stream.of(UID_UNIT_THING).collect(Collectors.toSet()));
+
+    // Collection of system and unit thing types
+    public static final Set<ThingTypeUID> SUPPORTED_SYSTEM_AND_UNIT_THING_TYPES_UIDS = Stream
+            .concat(SUPPORTED_SYSTEM_BRIDGE_THING_TYPES_UIDS.stream(), SUPPORTED_UNIT_THING_TYPES_UIDS.stream())
+            .collect(Collectors.toSet());
+
+    // Collection of all supported thing types
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
+            Stream.of(UID_ACCOUNT_BRIDGE, UID_SYSTEM_BRIDGE, UID_UNIT_THING).collect(Collectors.toSet()));
+
+    // System Properties
+    public static final String THING_PROPERTY_GATEWAY_ID = "GatewayId";
+    public static final String THING_PROPERTY_GATEWAY_USERNAME = "GatewayUsername";
+    public static final String THING_PROPERTY_INSTALLATION_DATE = "InstallationDate";
+    public static final String THING_PROPERTY_LOCATION = "Location";
+    public static final String THING_PROPERTY_OPERATOR_NAME = "OperatorName";
+    public static final String THING_PROPERTY_USERNAME_OWNER = "UserNameOwner";
+    public static final String THING_PROPERTY_ACCESSLEVEL = "AccessLevel";
+
+    public static final String CH_TEMPERATURE = "temperature";
+    public static final String CH_PRESSURE = "barometric-pressure";
+    public static final String CH_STRING = "string";
+    public static final String CH_CONTACT = "contact";
+    public static final String CH_NUMBER = "number";
+    public static final String CH_DATETIME = "datetime";
+
+    // Background discovery frequency
+    public static final int DISCOVERY_INTERVAL_SECONDS = 300;
+    public static final int DISCOVERY_INITIAL_DELAY_SECONDS = 10;
+
+    // System bridge and remote unit thing config parameters
+    public static final String CONFIG_SYSTEM_ID = "systemId";
+    public static final String CONFIG_UNIT_ID = "unitId";
+
+    public static final List<SubMenuEntryWithMenuItemTabView> EMPTY_UNITS = Collections
+            .<SubMenuEntryWithMenuItemTabView> emptyList();
+    public static final List<GetSystemListDTO> EMPTY_SYSTEMS = Collections.<GetSystemListDTO> emptyList();
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/WolfSmartsetHandlerFactory.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/WolfSmartsetHandlerFactory.java
new file mode 100644 (file)
index 0000000..5d892a9
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal;
+
+import static org.openhab.binding.wolfsmartset.internal.WolfSmartsetBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.wolfsmartset.internal.handler.WolfSmartsetAccountBridgeHandler;
+import org.openhab.binding.wolfsmartset.internal.handler.WolfSmartsetSystemBridgeHandler;
+import org.openhab.binding.wolfsmartset.internal.handler.WolfSmartsetUnitThingHandler;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link WolfSmartsetHandlerFactory} is responsible for creating thing handlers
+ * for the account bridge, system bridge, and unit thing.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.wolfsmartset", service = ThingHandlerFactory.class)
+public class WolfSmartsetHandlerFactory extends BaseThingHandlerFactory {
+    private final HttpClient httpClient;
+
+    @Activate
+    public WolfSmartsetHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (SUPPORTED_ACCOUNT_BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID)) {
+            return new WolfSmartsetAccountBridgeHandler((Bridge) thing, httpClient);
+        }
+        if (SUPPORTED_SYSTEM_BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID)) {
+            return new WolfSmartsetSystemBridgeHandler((Bridge) thing);
+        }
+        if (SUPPORTED_UNIT_THING_TYPES_UIDS.contains(thingTypeUID)) {
+            return new WolfSmartsetUnitThingHandler(thing);
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/api/WolfSmartsetApi.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/api/WolfSmartsetApi.java
new file mode 100644 (file)
index 0000000..6e85130
--- /dev/null
@@ -0,0 +1,610 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.api;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+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.HttpResponseException;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.wolfsmartset.internal.dto.CreateSession2DTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetGuiDescriptionForGatewayDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetParameterValuesDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemStateListDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.LoginResponseDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.ReadFaultMessagesDTO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link WolfSmartsetCloudConnector} class is used for connecting to the Wolf Smartset cloud service
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetApi {
+    private static final int MAX_QUEUE_SIZE = 1000; // maximum queue size
+    private static final int REQUEST_TIMEOUT_SECONDS = 10;
+
+    private static final DateTimeFormatter SESSION_TIME_STAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final String WOLF_API_URL = "https://www.wolf-smartset.com/portal/";
+
+    private Instant blockRequestsUntil = Instant.now();
+    private String username;
+    private String password;
+    private String serviceToken = "";
+    private @Nullable CreateSession2DTO session = null;
+    private int loginFailedCounter = 0;
+    private HttpClient httpClient;
+    private int delay = 500; // in ms
+    private final ScheduledExecutorService scheduler;
+    private final Gson gson = new GsonBuilder().serializeNulls().create();
+    private final LinkedBlockingQueue<RequestQueueEntry> requestQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
+    private @Nullable ScheduledFuture<?> processJob;
+
+    private final Logger logger = LoggerFactory.getLogger(WolfSmartsetApi.class);
+
+    public WolfSmartsetApi(String username, String password, HttpClient httpClient, ScheduledExecutorService scheduler)
+            throws WolfSmartsetCloudException {
+        this.username = username;
+        this.password = password;
+        this.httpClient = httpClient;
+        this.scheduler = scheduler;
+        if (!checkCredentials()) {
+            throw new WolfSmartsetCloudException("username or password can't be empty");
+        }
+    }
+
+    /**
+     * Validate Login to wolf smartset. Returns true if valid token is available, otherwise tries to authenticate with
+     * wolf smartset portal
+     */
+    public synchronized boolean login() {
+        if (!checkCredentials()) {
+            return false;
+        }
+        if (!serviceToken.isEmpty()) {
+            return true;
+        }
+        logger.debug("Wolf Smartset login with username {}", username);
+        try {
+            loginRequest();
+            loginFailedCounter = 0;
+            this.session = getCreateSession();
+            if (this.session != null) {
+                logger.debug("login successful, browserSessionId {}", session.getBrowserSessionId());
+                return true;
+            } else {
+                loginFailedCounter++;
+                this.session = null;
+                logger.trace("Login succeeded but failed to create session {}", loginFailedCounter);
+                return false;
+            }
+
+        } catch (WolfSmartsetCloudException e) {
+            logger.debug("Error logging on to Wolf Smartset ({}): {}", loginFailedCounter, e.getMessage());
+            loginFailedCounter++;
+            serviceToken = "";
+            loginFailedCounterCheck();
+            return false;
+        }
+    }
+
+    /**
+     * Request the systems available for the authenticated account
+     * 
+     * @return a list of the available systems
+     */
+    public List<GetSystemListDTO> getSystems() {
+        final String response = getSystemString();
+        List<GetSystemListDTO> devicesList = new ArrayList<>();
+        try {
+            GetSystemListDTO[] cdl = gson.fromJson(response, GetSystemListDTO[].class);
+            if (cdl != null) {
+                for (GetSystemListDTO system : cdl) {
+                    devicesList.add(system);
+                }
+            }
+        } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
+            loginFailedCounter++;
+            logger.warn("Error while parsing devices: {}", e.getMessage());
+        }
+        return devicesList;
+    }
+
+    /**
+     * Request the description of the given system
+     * 
+     * @param systemId the id of the system
+     * @param gatewayId the id of the gateway the system relates to
+     * @return dto describing the requested system
+     */
+    public @Nullable GetGuiDescriptionForGatewayDTO getSystemDescription(Integer systemId, Integer gatewayId) {
+        final String response = getSystemDescriptionString(systemId, gatewayId);
+        GetGuiDescriptionForGatewayDTO deviceDescription = null;
+        try {
+            deviceDescription = gson.fromJson(response, GetGuiDescriptionForGatewayDTO.class);
+        } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
+            loginFailedCounter++;
+            logger.warn("Error while parsing device descriptions: {}", e.getMessage());
+        }
+        return deviceDescription;
+    }
+
+    /**
+     * Request the system state of the given systems
+     * 
+     * @param systems a list of {@link GetSystemListDTO}
+     * @return the {@link GetSystemStateListDTO} descibing the state of the given {@link GetSystemListDTO} items
+     */
+    public @Nullable GetSystemStateListDTO @Nullable [] getSystemState(Collection<@Nullable GetSystemListDTO> systems) {
+        final String response = getSystemStateString(systems);
+        GetSystemStateListDTO[] systemState = null;
+        try {
+            systemState = gson.fromJson(response, GetSystemStateListDTO[].class);
+        } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
+            loginFailedCounter++;
+            logger.warn("Error while parsing device descriptions: {}", e.getMessage());
+        }
+        if (systemState != null && systemState.length >= 1) {
+            return systemState;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Request the fault messages of the given system
+     * 
+     * @param systemId the id of the system
+     * @param gatewayId the id of the gateway the system relates to
+     * @return {@link ReadFaultMessagesDTO} containing the faultmessages
+     */
+    public @Nullable ReadFaultMessagesDTO getFaultMessages(Integer systemId, Integer gatewayId) {
+        final String response = getFaultMessagesString(systemId, gatewayId);
+        ReadFaultMessagesDTO faultMessages = null;
+        try {
+            faultMessages = gson.fromJson(response, ReadFaultMessagesDTO.class);
+        } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
+            loginFailedCounter++;
+            logger.warn("Error while parsing faultmessages: {}", e.getMessage());
+        }
+        return faultMessages;
+    }
+
+    /**
+     * Request the current values for a unit associated with the given system.
+     * if lastAccess is not null, only value changes newer than the given timestamp are returned
+     * 
+     * @param systemId the id of the system
+     * @param gatewayId the id of the gateway the system relates to
+     * @param bundleId the id of the Unit
+     * @param valueIdList list of the values to request
+     * @param lastAccess timestamp of the last valid value request
+     * @return {@link GetParameterValuesDTO} containing the requested values
+     */
+    public @Nullable GetParameterValuesDTO getGetParameterValues(Integer systemId, Integer gatewayId, Long bundleId,
+            List<Long> valueIdList, @Nullable Instant lastAccess) {
+        final String response = getGetParameterValuesString(systemId, gatewayId, bundleId, valueIdList, lastAccess);
+        GetParameterValuesDTO parameterValues = null;
+        try {
+            parameterValues = gson.fromJson(response, GetParameterValuesDTO.class);
+        } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
+            loginFailedCounter++;
+            logger.warn("Error while parsing device parameter values: {}", e.getMessage());
+        }
+        return parameterValues;
+    }
+
+    public void stopRequestQueue() {
+        try {
+            stopProcessJob();
+            requestQueue.forEach(queueEntry -> queueEntry.future.completeExceptionally(new CancellationException()));
+        } catch (Exception e) {
+            logger.debug("Error stopping request queue background processing:{}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Set a new delay
+     *
+     * @param delay in ms between to requests
+     */
+    private void setDelay(int delay) {
+        if (delay < 0) {
+            throw new IllegalArgumentException("Delay needs to be larger or equal to zero");
+        }
+        this.delay = delay;
+        stopProcessJob();
+        if (delay != 0) {
+            processJob = scheduler.scheduleWithFixedDelay(() -> processQueue(), 0, delay, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private boolean checkCredentials() {
+        if (username.trim().isEmpty() || password.trim().isEmpty()) {
+            logger.debug("Wolf Smartset: username or password missing.");
+            return false;
+        }
+        return true;
+    }
+
+    private String getCreateSessionString() {
+        String resp = "";
+        try {
+            JsonObject json = new JsonObject();
+            json.addProperty("Timestamp", SESSION_TIME_STAMP.format(LocalDateTime.now()));
+            resp = requestPOST("api/portal/CreateSession2", json).get();
+            logger.trace("api/portal/CreateSession2 response: {}", resp);
+        } catch (InterruptedException | ExecutionException e) {
+            logger.warn("getSystemStateString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        } catch (WolfSmartsetCloudException e) {
+            logger.debug("getSystemStateString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        }
+
+        return resp;
+    }
+
+    private @Nullable CreateSession2DTO getCreateSession() {
+        final String response = getCreateSessionString();
+        CreateSession2DTO session = null;
+        try {
+            session = gson.fromJson(response, CreateSession2DTO.class);
+        } catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
+            loginFailedCounter++;
+            logger.warn("getCreateSession failed with {}: {}", e.getCause(), e.getMessage());
+        }
+        return session;
+    }
+
+    private String getSystemString() {
+        String resp = "";
+        try {
+            resp = requestGET("api/portal/GetSystemList").get();
+            logger.trace("api/portal/GetSystemList response: {}", resp);
+        } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
+            logger.warn("getSystemString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        }
+        return resp;
+    }
+
+    private String getSystemDescriptionString(Integer systemId, Integer gatewayId) {
+        String resp = "";
+        try {
+            Map<String, String> params = new HashMap<String, String>();
+            params.put("SystemId", systemId.toString());
+            params.put("GatewayId", gatewayId.toString());
+            resp = requestGET("api/portal/GetGuiDescriptionForGateway", params).get();
+            logger.trace("api/portal/GetGuiDescriptionForGateway response: {}", resp);
+        } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
+            logger.warn("getSystemDescriptionString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        }
+        return resp;
+    }
+
+    private String getSystemStateString(Collection<@Nullable GetSystemListDTO> systems) {
+        String resp = "";
+        try {
+            JsonArray jsonSystemList = new JsonArray();
+
+            for (@Nullable
+            GetSystemListDTO system : systems) {
+                if (system != null) {
+                    JsonObject jsonSystem = new JsonObject();
+                    jsonSystem.addProperty("SystemId", system.getId());
+                    jsonSystem.addProperty("GatewayId", system.getGatewayId());
+
+                    if (system.getSystemShareId() != null) {
+                        jsonSystem.addProperty("SystemShareId", system.getSystemShareId());
+                    }
+                    jsonSystemList.add(jsonSystem);
+                }
+            }
+
+            JsonObject json = new JsonObject();
+
+            json.add("SystemList", jsonSystemList);
+            resp = requestPOST("api/portal/GetSystemStateList", json).get();
+            logger.trace("api/portal/GetSystemStateList response: {}", resp);
+        } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
+            logger.warn("getSystemStateString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        }
+        return resp;
+    }
+
+    private String getFaultMessagesString(Integer systemId, Integer gatewayId) {
+        String resp = "";
+        try {
+            JsonObject json = new JsonObject();
+
+            json.addProperty("SystemId", systemId);
+            json.addProperty("GatewayId", gatewayId);
+            resp = requestPOST("api/portal/ReadFaultMessages", json).get();
+            logger.trace("api/portal/ReadFaultMessages response: {}", resp);
+        } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
+            logger.warn("getFaultMessagesString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        }
+        return resp;
+    }
+
+    private String getGetParameterValuesString(Integer systemId, Integer gatewayId, Long bundleId,
+            List<Long> valueIdList, @Nullable Instant lastAccess) {
+        String resp = "";
+        try {
+            JsonObject json = new JsonObject();
+            json.addProperty("SystemId", systemId);
+            json.addProperty("GatewayId", gatewayId);
+            json.addProperty("BundleId", bundleId);
+            json.addProperty("IsSubBundle", false);
+            json.add("ValueIdList", gson.toJsonTree(valueIdList));
+            if (lastAccess != null) {
+                json.addProperty("LastAccess", DateTimeFormatter.ISO_INSTANT.format(lastAccess));
+            } else {
+                json.addProperty("LastAccess", (String) null);
+            }
+            json.addProperty("GuiIdChanged", false);
+            if (session != null) {
+                json.addProperty("SessionId", session.getBrowserSessionId());
+            }
+            resp = requestPOST("api/portal/GetParameterValues", json).get();
+            logger.trace("api/portal/GetParameterValues response: {}", resp);
+        } catch (InterruptedException | ExecutionException | WolfSmartsetCloudException e) {
+            logger.warn("getGetParameterValuesString failed with {}: {}", e.getCause(), e.getMessage());
+            loginFailedCounter++;
+        }
+        return resp;
+    }
+
+    private CompletableFuture<String> requestGET(String url) throws WolfSmartsetCloudException {
+        return requestGET(url, new HashMap<String, String>());
+    }
+
+    private CompletableFuture<String> requestGET(String url, Map<String, String> params)
+            throws WolfSmartsetCloudException {
+        return rateLimtedRequest(() -> {
+            if (this.serviceToken.isEmpty()) {
+                throw new WolfSmartsetCloudException("Cannot execute request. service token missing");
+            }
+            loginFailedCounterCheck();
+
+            var requestUrl = WOLF_API_URL + url;
+            Request request = httpClient.newRequest(requestUrl).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+            // using HTTP GET with ContentType application/x-www-form-urlencoded like the iOS App does
+            request.header(HttpHeader.AUTHORIZATION, serviceToken);
+            request.method(HttpMethod.GET);
+            request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
+
+            for (Entry<String, String> entry : params.entrySet()) {
+                logger.debug("Send request param: {}={} to {}", entry.getKey(), entry.getValue().toString(), url);
+                request.param(entry.getKey(), entry.getValue());
+            }
+
+            return request;
+        });
+    }
+
+    private CompletableFuture<String> requestPOST(String url, JsonElement json) throws WolfSmartsetCloudException {
+        return rateLimtedRequest(() -> {
+            if (this.serviceToken.isEmpty()) {
+                throw new WolfSmartsetCloudException("Cannot execute request. service token missing");
+            }
+            loginFailedCounterCheck();
+
+            var request = createPOSTRequest(url, json);
+            request.header(HttpHeader.AUTHORIZATION, serviceToken);
+            return request;
+        });
+    }
+
+    private Request createPOSTRequest(String url, JsonElement json) {
+        var requestUrl = WOLF_API_URL + url;
+        Request request = httpClient.newRequest(requestUrl).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        request.header(HttpHeader.ACCEPT, "application/json");
+        request.header(HttpHeader.CONTENT_TYPE, "application/json");
+        request.method(HttpMethod.POST);
+
+        request.content(new StringContentProvider(json.toString()), "application/json");
+        return request;
+    }
+
+    private CompletableFuture<String> rateLimtedRequest(SupplyRequestFunctionalInterface buildRequest) {
+        // if no delay is set, return a completed CompletableFuture
+        CompletableFuture<String> future = new CompletableFuture<>();
+        RequestQueueEntry queueEntry = new RequestQueueEntry(buildRequest, future);
+
+        if (delay == 0) {
+            queueEntry.completeFuture((r) -> this.getResponse(r));
+        } else {
+            if (!requestQueue.offer(queueEntry)) {
+                future.completeExceptionally(new RejectedExecutionException("Maximum queue size exceeded."));
+            }
+        }
+        return future;
+    }
+
+    private void stopProcessJob() {
+        ScheduledFuture<?> processJob = this.processJob;
+        if (processJob != null) {
+            processJob.cancel(false);
+            this.processJob = null;
+        }
+    }
+
+    private void processQueue() {
+        // No new Requests until blockRequestsUntil, is set when recieved HttpStatus.TOO_MANY_REQUESTS_429
+        if (blockRequestsUntil.isBefore(Instant.now())) {
+            RequestQueueEntry queueEntry = requestQueue.poll();
+            if (queueEntry != null) {
+                queueEntry.completeFuture((r) -> this.getResponse(r));
+            }
+        }
+    }
+
+    @FunctionalInterface
+    interface SupplyRequestFunctionalInterface {
+        Request get() throws WolfSmartsetCloudException;
+    }
+
+    @FunctionalInterface
+    interface GetResponseFunctionalInterface {
+        String get(Request request) throws WolfSmartsetCloudException;
+    }
+
+    private String getResponse(Request request) throws WolfSmartsetCloudException {
+        try {
+            logger.debug("execute request {} {}", request.getMethod(), request.getURI());
+            final ContentResponse response = request.send();
+            if (response.getStatus() == HttpStatus.NOT_FOUND_404) {
+                throw new WolfSmartsetCloudException("Invalid request, not found " + request.getURI());
+            } else if (response.getStatus() == HttpStatus.TOO_MANY_REQUESTS_429) {
+                blockRequestsUntil = Instant.now().plusSeconds(30);
+                throw new WolfSmartsetCloudException("Error too many requests: " + response.getContentAsString());
+            } else if (response.getStatus() >= HttpStatus.BAD_REQUEST_400
+                    && response.getStatus() < HttpStatus.INTERNAL_SERVER_ERROR_500) {
+                this.serviceToken = "";
+                logger.debug("Status {} while executing request to {} :{}", response.getStatus(), request.getURI(),
+                        response.getContentAsString());
+            } else {
+                return response.getContentAsString();
+            }
+        } catch (HttpResponseException e) {
+            serviceToken = "";
+            logger.debug("Error while executing request to {} :{}", request.getURI(), e.getMessage());
+            loginFailedCounter++;
+        } catch (InterruptedException | TimeoutException | ExecutionException /* | IOException */ e) {
+            logger.debug("Error while executing request to {} :{}", request.getURI(), e.getMessage());
+            loginFailedCounter++;
+        }
+        return "";
+    }
+
+    void loginFailedCounterCheck() {
+        if (loginFailedCounter > 10) {
+            logger.debug("Repeated errors logging on to Wolf Smartset");
+            serviceToken = "";
+            loginFailedCounter = 0;
+        }
+    }
+
+    protected void loginRequest() throws WolfSmartsetCloudException {
+        try {
+            setDelay(delay);
+            logger.trace("Wolf Smartset Login");
+
+            String url = WOLF_API_URL + "connect/token";
+            Request request = httpClient.POST(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
+
+            // Building Request body exacly the way the iOS App did this
+            var encodedUser = URLEncoder.encode(username, StandardCharsets.UTF_8);
+            var encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8);
+            var authRequestBody = "grant_type=password&username=" + encodedUser + "&password=" + encodedPassword;
+
+            request.content(new StringContentProvider("application/x-www-form-urlencoded", authRequestBody,
+                    StandardCharsets.UTF_8));
+
+            final ContentResponse response;
+            response = request.send();
+
+            final String content = response.getContentAsString();
+            logger.trace("Wolf smartset Login response= {}", response);
+            logger.trace("Wolf smartset Login content= {}", content);
+
+            switch (response.getStatus()) {
+                case HttpStatus.FORBIDDEN_403:
+                    throw new WolfSmartsetCloudException(
+                            "Access denied. Did you set the correct password and/or username?");
+                case HttpStatus.OK_200:
+                    LoginResponseDTO jsonResp = gson.fromJson(content, LoginResponseDTO.class);
+                    if (jsonResp == null) {
+                        throw new WolfSmartsetCloudException("Error getting logon details: " + content);
+                    }
+
+                    serviceToken = jsonResp.getTokenType() + " " + jsonResp.getAccessToken();
+
+                    logger.trace("Wolf Smartset login scope = {}", jsonResp.getScope());
+                    logger.trace("Wolf Smartset login expiresIn = {}", jsonResp.getExpiresIn());
+                    logger.trace("Wolf Smartset login tokenType = {}", jsonResp.getTokenType());
+                    return;
+                default:
+                    logger.trace("request returned status '{}', reason: {}, content = {}", response.getStatus(),
+                            response.getReason(), response.getContentAsString());
+                    throw new WolfSmartsetCloudException(response.getStatus() + response.getReason());
+            }
+        } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
+            throw new WolfSmartsetCloudException("Cannot logon to Wolf Smartset cloud: " + e.getMessage(), e);
+        }
+    }
+
+    private static class RequestQueueEntry {
+        private SupplyRequestFunctionalInterface buildRequest;
+        private CompletableFuture<String> future;
+
+        public RequestQueueEntry(SupplyRequestFunctionalInterface buildRequest, CompletableFuture<String> future) {
+            this.buildRequest = buildRequest;
+            this.future = future;
+        }
+
+        public void completeFuture(GetResponseFunctionalInterface getResponse) {
+            try {
+                String response = getResponse.get(this.buildRequest.get());
+                future.complete(response);
+            } catch (WolfSmartsetCloudException e) {
+                future.completeExceptionally(e);
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/api/WolfSmartsetCloudException.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/api/WolfSmartsetCloudException.java
new file mode 100644 (file)
index 0000000..010f48f
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Will be thrown for cloud errors
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetCloudException extends Exception {
+    /**
+     * required variable to avoid IncorrectMultilineIndexException warning
+     */
+    private static final Long serialVersionUID = -1280858607995252321L;
+
+    public WolfSmartsetCloudException() {
+        super();
+    }
+
+    public WolfSmartsetCloudException(@Nullable String message) {
+        super(message);
+    }
+
+    public WolfSmartsetCloudException(@Nullable String message, @Nullable Exception e) {
+        super(message, e);
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetAccountConfiguration.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetAccountConfiguration.java
new file mode 100644 (file)
index 0000000..1f23a0a
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link WolfSmartsetAccountConfiguration} class contains fields mapping
+ * to the account thing configuration parameters.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetAccountConfiguration {
+
+    public @Nullable String username = "";
+    public @Nullable String password = "";
+    /**
+     * Time in seconds between information refresh
+     */
+    public @Nullable Integer refreshIntervalStructure;
+
+    /**
+     * Time in seconds to wait after successful update, command or action before refresh
+     */
+    public @Nullable Integer refreshIntervalValues;
+
+    /*
+     * Enable/disable automatic discovery
+     */
+    public @Nullable Boolean discoveryEnabled;
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetSystemConfiguration.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetSystemConfiguration.java
new file mode 100644 (file)
index 0000000..bd852af
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link WolfSmartsetSystemConfiguration} class contains fields mapping
+ * to the system thing configuration parameters.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetSystemConfiguration {
+
+    /**
+     * System ID assigned by WolfSmartset
+     */
+    public @Nullable String systemId;
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetUnitConfiguration.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/config/WolfSmartsetUnitConfiguration.java
new file mode 100644 (file)
index 0000000..3135a1f
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link WolfSmartsetUnitConfiguration} class contains fields mapping
+ * to the unit thing configuration parameters.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetUnitConfiguration {
+
+    /**
+     * Id of remote unit
+     */
+    public @Nullable String unitId;
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/discovery/WolfSmartsetAccountDiscoveryService.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/discovery/WolfSmartsetAccountDiscoveryService.java
new file mode 100644 (file)
index 0000000..41a5f31
--- /dev/null
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.discovery;
+
+import static org.openhab.binding.wolfsmartset.internal.WolfSmartsetBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
+import org.openhab.binding.wolfsmartset.internal.handler.WolfSmartsetAccountBridgeHandler;
+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.thing.ThingStatus;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WolfSmartsetAccountDiscoveryService} is responsible for discovering the WolfSmartset
+ * systems that are associated with the WolfSmartset Account
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetAccountDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+
+    private final Logger logger = LoggerFactory.getLogger(WolfSmartsetAccountDiscoveryService.class);
+
+    private @NonNullByDefault({}) WolfSmartsetAccountBridgeHandler bridgeHandler;
+
+    private @Nullable Future<?> discoveryJob;
+
+    public WolfSmartsetAccountDiscoveryService() {
+        super(SUPPORTED_SYSTEM_AND_UNIT_THING_TYPES_UIDS, 8, true);
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof WolfSmartsetAccountBridgeHandler) {
+            this.bridgeHandler = (WolfSmartsetAccountBridgeHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+
+    @Override
+    public void activate() {
+        super.activate(null);
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypes() {
+        return SUPPORTED_SYSTEM_AND_UNIT_THING_TYPES_UIDS;
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        logger.debug("WolfSmartsetDiscovery: Starting background discovery job");
+
+        Future<?> localDiscoveryJob = discoveryJob;
+        if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
+            discoveryJob = scheduler.scheduleWithFixedDelay(this::backgroundDiscover, DISCOVERY_INITIAL_DELAY_SECONDS,
+                    DISCOVERY_INTERVAL_SECONDS, TimeUnit.SECONDS);
+        }
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        logger.debug("WolfSmartsetDiscovery: Stopping background discovery job");
+        Future<?> localDiscoveryJob = discoveryJob;
+        if (localDiscoveryJob != null) {
+            localDiscoveryJob.cancel(true);
+            discoveryJob = null;
+        }
+    }
+
+    @Override
+    public void startScan() {
+        logger.debug("WolfSmartsetDiscovery: Starting discovery scan");
+        discover();
+    }
+
+    private void backgroundDiscover() {
+        if (!bridgeHandler.isBackgroundDiscoveryEnabled()) {
+            return;
+        }
+        discover();
+    }
+
+    private void discover() {
+        if (bridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
+            logger.debug("WolfSmartsetDiscovery: Skipping discovery because Account Bridge thing is not ONLINE");
+            return;
+        }
+        logger.debug("WolfSmartsetDiscovery: Discovering WolfSmartset devices");
+        discoverSystems();
+    }
+
+    private synchronized void discoverSystems() {
+        logger.debug("WolfSmartsetDiscovery: Discovering systems");
+        var registeredSytems = bridgeHandler.getRegisteredSystems();
+        if (registeredSytems != null) {
+            for (GetSystemListDTO system : registeredSytems) {
+                String name = system.getName();
+                String identifier = null;
+                if (system.getId() != null) {
+                    identifier = system.getId().toString();
+                }
+                if (identifier != null && name != null) {
+                    ThingUID thingUID = new ThingUID(UID_SYSTEM_BRIDGE, bridgeHandler.getThing().getUID(), identifier);
+                    thingDiscovered(createSystemDiscoveryResult(thingUID, identifier, name));
+                    logger.debug("WolfSmartsetDiscovery: System '{}' and name '{}' added with UID '{}'", identifier,
+                            name, thingUID);
+                }
+            }
+        }
+    }
+
+    private DiscoveryResult createSystemDiscoveryResult(ThingUID systemUID, String identifier, String name) {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(CONFIG_SYSTEM_ID, identifier);
+        return DiscoveryResultBuilder.create(systemUID).withProperties(properties)
+                .withRepresentationProperty(CONFIG_SYSTEM_ID).withBridge(bridgeHandler.getThing().getUID())
+                .withLabel(String.format("WolfSmartset System %s", name)).build();
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/discovery/WolfSmartsetSystemDiscoveryService.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/discovery/WolfSmartsetSystemDiscoveryService.java
new file mode 100644 (file)
index 0000000..dda2cb9
--- /dev/null
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.discovery;
+
+import static org.openhab.binding.wolfsmartset.internal.WolfSmartsetBindingConstants.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.SubMenuEntryWithMenuItemTabView;
+import org.openhab.binding.wolfsmartset.internal.handler.WolfSmartsetSystemBridgeHandler;
+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.thing.ThingStatus;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WolfSmartsetAccountDiscoveryService} is responsible for discovering the WolfSmartset Units
+ * that are associated with the WolfSmartset System
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetSystemDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+
+    private final Logger logger = LoggerFactory.getLogger(WolfSmartsetSystemDiscoveryService.class);
+
+    private @NonNullByDefault({}) WolfSmartsetSystemBridgeHandler bridgeHandler;
+
+    private @Nullable Future<?> discoveryJob;
+
+    public WolfSmartsetSystemDiscoveryService() {
+        super(SUPPORTED_SYSTEM_AND_UNIT_THING_TYPES_UIDS, 8, true);
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof WolfSmartsetSystemBridgeHandler) {
+            this.bridgeHandler = (WolfSmartsetSystemBridgeHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+
+    @Override
+    public void activate() {
+        super.activate(null);
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypes() {
+        return SUPPORTED_SYSTEM_AND_UNIT_THING_TYPES_UIDS;
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        logger.debug("WolfSmartsetSystemDiscovery: Starting background discovery job");
+        Future<?> localDiscoveryJob = discoveryJob;
+        if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
+            discoveryJob = scheduler.scheduleWithFixedDelay(() -> this.backgroundDiscover(),
+                    DISCOVERY_INITIAL_DELAY_SECONDS, DISCOVERY_INTERVAL_SECONDS, TimeUnit.SECONDS);
+        }
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        logger.debug("WolfSmartsetSystemDiscovery: Stopping background discovery job");
+        Future<?> localDiscoveryJob = discoveryJob;
+        if (localDiscoveryJob != null) {
+            localDiscoveryJob.cancel(true);
+            discoveryJob = null;
+        }
+    }
+
+    @Override
+    public void startScan() {
+        logger.debug("WolfSmartsetSystemDiscovery: Starting discovery scan");
+        discover();
+    }
+
+    private void backgroundDiscover() {
+        var accountBridgeHandler = bridgeHandler.getAccountBridgeHandler();
+        if (accountBridgeHandler == null || !accountBridgeHandler.isBackgroundDiscoveryEnabled()) {
+            return;
+        }
+        discover();
+    }
+
+    private void discover() {
+        if (bridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
+            logger.debug("WolfSmartsetSystemDiscovery: Skipping discovery because Account Bridge thing is not ONLINE");
+            return;
+        }
+        logger.debug("WolfSmartsetSystemDiscovery: Discovering WolfSmartset devices");
+        discoverUnits();
+    }
+
+    private synchronized void discoverUnits() {
+        if (this.bridgeHandler != null) {
+            String systemId = this.bridgeHandler.getSystemId();
+            var systemConfig = this.bridgeHandler.getSystemConfig();
+            if (systemConfig != null) {
+                logger.debug("WolfSmartsetSystemDiscovery: Discovering units for system '{}' (id {})",
+                        systemConfig.getName(), systemId);
+                for (var unit : this.bridgeHandler.getUnits()) {
+                    ThingUID bridgeUID = this.bridgeHandler.getThing().getUID();
+                    ThingUID unitUID = new ThingUID(UID_UNIT_THING, bridgeUID,
+                            unit.menuItemTabViewDTO.bundleId.toString());
+                    thingDiscovered(createUnitDiscoveryResult(unitUID, bridgeUID, systemConfig, unit));
+                    logger.debug(
+                            "WolfSmartsetSystemDiscovery: Unit for '{}' with id '{}' and name '{}' added with UID '{}'",
+                            systemId, unit.menuItemTabViewDTO.bundleId, unit.menuItemTabViewDTO.tabName, unitUID);
+                }
+            }
+        }
+    }
+
+    private DiscoveryResult createUnitDiscoveryResult(ThingUID unitUID, ThingUID bridgeUID,
+            GetSystemListDTO systemConfig, SubMenuEntryWithMenuItemTabView unit) {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(CONFIG_UNIT_ID, unit.menuItemTabViewDTO.bundleId.toString());
+        var tabName = unit.menuItemTabViewDTO.tabName;
+        var menuName = unit.subMenuEntryDTO.getName();
+        tabName = tabName.isEmpty() || tabName.equalsIgnoreCase("NULL") || menuName.equalsIgnoreCase(tabName) ? ""
+                : "-" + tabName;
+
+        return DiscoveryResultBuilder.create(unitUID).withProperties(properties)
+                .withRepresentationProperty(CONFIG_UNIT_ID).withBridge(bridgeUID)
+                .withLabel(String.format("%s %s%s", systemConfig.getName(), menuName, tabName)).build();
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/CreateSession2DTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/CreateSession2DTO.java
new file mode 100644 (file)
index 0000000..0d13b4b
--- /dev/null
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class CreateSession2DTO {
+
+    @SerializedName("CultureInfoCode")
+    @Expose
+    private String cultureInfoCode;
+    @SerializedName("IsPasswordReset")
+    @Expose
+    private Boolean isPasswordReset;
+    @SerializedName("IsProfessional")
+    @Expose
+    private Boolean isProfessional;
+    @SerializedName("IsProfessionalPasswordReset")
+    @Expose
+    private Boolean isProfessionalPasswordReset;
+    @SerializedName("BrowserSessionId")
+    @Expose
+    private Integer browserSessionId;
+
+    public String getCultureInfoCode() {
+        return cultureInfoCode;
+    }
+
+    public void setCultureInfoCode(String cultureInfoCode) {
+        this.cultureInfoCode = cultureInfoCode;
+    }
+
+    public Boolean getIsPasswordReset() {
+        return isPasswordReset;
+    }
+
+    public void setIsPasswordReset(Boolean isPasswordReset) {
+        this.isPasswordReset = isPasswordReset;
+    }
+
+    public Boolean getIsProfessional() {
+        return isProfessional;
+    }
+
+    public void setIsProfessional(Boolean isProfessional) {
+        this.isProfessional = isProfessional;
+    }
+
+    public Boolean getIsProfessionalPasswordReset() {
+        return isProfessionalPasswordReset;
+    }
+
+    public void setIsProfessionalPasswordReset(Boolean isProfessionalPasswordReset) {
+        this.isProfessionalPasswordReset = isProfessionalPasswordReset;
+    }
+
+    public Integer getBrowserSessionId() {
+        return browserSessionId;
+    }
+
+    public void setBrowserSessionId(Integer browserSessionId) {
+        this.browserSessionId = browserSessionId;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/CurrentMessageDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/CurrentMessageDTO.java
new file mode 100644 (file)
index 0000000..9868a3a
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class CurrentMessageDTO {
+
+    @SerializedName("Id")
+    @Expose
+    private Integer id;
+    @SerializedName("ErrorCode")
+    @Expose
+    private Integer errorCode;
+    @SerializedName("Description")
+    @Expose
+    private String description;
+    @SerializedName("OccurTimeLocal")
+    @Expose
+    private String occurTimeLocal;
+    @SerializedName("Active")
+    @Expose
+    private Boolean active;
+    @SerializedName("Index")
+    @Expose
+    private Integer index;
+    @SerializedName("Device")
+    @Expose
+    private String device;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getErrorCode() {
+        return errorCode;
+    }
+
+    public void setErrorCode(Integer errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getOccurTimeLocal() {
+        return occurTimeLocal;
+    }
+
+    public void setOccurTimeLocal(String occurTimeLocal) {
+        this.occurTimeLocal = occurTimeLocal;
+    }
+
+    public Boolean getActive() {
+        return active;
+    }
+
+    public void setActive(Boolean active) {
+        this.active = active;
+    }
+
+    public Integer getIndex() {
+        return index;
+    }
+
+    public void setIndex(Integer index) {
+        this.index = index;
+    }
+
+    public String getDevice() {
+        return device;
+    }
+
+    public void setDevice(String device) {
+        this.device = device;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GatewayStateDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GatewayStateDTO.java
new file mode 100644 (file)
index 0000000..f511644
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class GatewayStateDTO {
+
+    @SerializedName("GatewayId")
+    @Expose
+    private Integer gatewayId;
+    @SerializedName("IsOnline")
+    @Expose
+    private Boolean isOnline;
+    @SerializedName("GatewayOfflineCause")
+    @Expose
+    private Integer gatewayOfflineCause;
+    @SerializedName("IsLocked")
+    @Expose
+    private Boolean isLocked;
+    @SerializedName("IsDeleted")
+    @Expose
+    private Boolean isDeleted;
+    @SerializedName("IsBusy")
+    @Expose
+    private Boolean isBusy;
+    @SerializedName("ImageName")
+    @Expose
+    private String imageName;
+
+    public Integer getGatewayId() {
+        return gatewayId;
+    }
+
+    public void setGatewayId(Integer gatewayId) {
+        this.gatewayId = gatewayId;
+    }
+
+    public Boolean getIsOnline() {
+        return isOnline;
+    }
+
+    public void setIsOnline(Boolean isOnline) {
+        this.isOnline = isOnline;
+    }
+
+    public Integer getGatewayOfflineCause() {
+        return gatewayOfflineCause;
+    }
+
+    public void setGatewayOfflineCause(Integer gatewayOfflineCause) {
+        this.gatewayOfflineCause = gatewayOfflineCause;
+    }
+
+    public Boolean getIsLocked() {
+        return isLocked;
+    }
+
+    public void setIsLocked(Boolean isLocked) {
+        this.isLocked = isLocked;
+    }
+
+    public Boolean getIsDeleted() {
+        return isDeleted;
+    }
+
+    public void setIsDeleted(Boolean isDeleted) {
+        this.isDeleted = isDeleted;
+    }
+
+    public Boolean getIsBusy() {
+        return isBusy;
+    }
+
+    public void setIsBusy(Boolean isBusy) {
+        this.isBusy = isBusy;
+    }
+
+    public String getImageName() {
+        return imageName;
+    }
+
+    public void setImageName(String imageName) {
+        this.imageName = imageName;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetGuiDescriptionForGatewayDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetGuiDescriptionForGatewayDTO.java
new file mode 100644 (file)
index 0000000..1c5ce12
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class GetGuiDescriptionForGatewayDTO {
+
+    @SerializedName("MenuItems")
+    @Expose
+    private List<MenuItemDTO> menuItems = null;
+    @SerializedName("DynFaultMessageDevices")
+    @Expose
+    private List<Object> dynFaultMessageDevices = null;
+    @SerializedName("SystemHasWRSClassicDevices")
+    @Expose
+    private Boolean systemHasWRSClassicDevices;
+
+    public List<MenuItemDTO> getMenuItems() {
+        return menuItems;
+    }
+
+    public void setMenuItems(List<MenuItemDTO> menuItems) {
+        this.menuItems = menuItems;
+    }
+
+    public List<Object> getDynFaultMessageDevices() {
+        return dynFaultMessageDevices;
+    }
+
+    public void setDynFaultMessageDevices(List<Object> dynFaultMessageDevices) {
+        this.dynFaultMessageDevices = dynFaultMessageDevices;
+    }
+
+    public Boolean getSystemHasWRSClassicDevices() {
+        return systemHasWRSClassicDevices;
+    }
+
+    public void setSystemHasWRSClassicDevices(Boolean systemHasWRSClassicDevices) {
+        this.systemHasWRSClassicDevices = systemHasWRSClassicDevices;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetParameterValuesDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetParameterValuesDTO.java
new file mode 100644 (file)
index 0000000..bab0e0a
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class GetParameterValuesDTO {
+
+    @SerializedName("LastAccess")
+    @Expose
+    private String lastAccess;
+    @SerializedName("Values")
+    @Expose
+    private List<ValueDTO> values = null;
+    @SerializedName("IsNewJobCreated")
+    @Expose
+    private Boolean isNewJobCreated;
+
+    public String getLastAccess() {
+        return lastAccess;
+    }
+
+    public void setLastAccess(String lastAccess) {
+        this.lastAccess = lastAccess;
+    }
+
+    public List<ValueDTO> getValues() {
+        return values;
+    }
+
+    public void setValues(List<ValueDTO> values) {
+        this.values = values;
+    }
+
+    public Boolean getIsNewJobCreated() {
+        return isNewJobCreated;
+    }
+
+    public void setIsNewJobCreated(Boolean isNewJobCreated) {
+        this.isNewJobCreated = isNewJobCreated;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetSystemListDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetSystemListDTO.java
new file mode 100644 (file)
index 0000000..356b515
--- /dev/null
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class GetSystemListDTO {
+
+    @SerializedName("Id")
+    @Expose
+    private Integer id;
+    @SerializedName("GatewayId")
+    @Expose
+    private Integer gatewayId;
+    @SerializedName("IsForeignSystem")
+    @Expose
+    private Boolean isForeignSystem;
+    @SerializedName("AccessLevel")
+    @Expose
+    private Integer accessLevel;
+    @SerializedName("GatewayUsername")
+    @Expose
+    private String gatewayUsername;
+    @SerializedName("Name")
+    @Expose
+    private String name;
+    @SerializedName("SystemShares")
+    @Expose
+    private List<Object> systemShares = null;
+    @SerializedName("GatewaySoftwareVersion")
+    @Expose
+    private String gatewaySoftwareVersion;
+    @SerializedName("UserNameOwner")
+    @Expose
+    private String userNameOwner;
+    @SerializedName("SystemShareId")
+    @Expose
+    private Integer systemShareId;
+    @SerializedName("OperatorName")
+    @Expose
+    private String operatorName;
+    @SerializedName("Location")
+    @Expose
+    private String location;
+    @SerializedName("InstallationDate")
+    @Expose
+    private String installationDate;
+    @SerializedName("ImageId")
+    @Expose
+    private Integer imageId;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getGatewayId() {
+        return gatewayId;
+    }
+
+    public void setGatewayId(Integer gatewayId) {
+        this.gatewayId = gatewayId;
+    }
+
+    public Boolean getIsForeignSystem() {
+        return isForeignSystem;
+    }
+
+    public void setIsForeignSystem(Boolean isForeignSystem) {
+        this.isForeignSystem = isForeignSystem;
+    }
+
+    public Integer getAccessLevel() {
+        return accessLevel;
+    }
+
+    public void setAccessLevel(Integer accessLevel) {
+        this.accessLevel = accessLevel;
+    }
+
+    public String getGatewayUsername() {
+        return gatewayUsername;
+    }
+
+    public void setGatewayUsername(String gatewayUsername) {
+        this.gatewayUsername = gatewayUsername;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public List<Object> getSystemShares() {
+        return systemShares;
+    }
+
+    public void setSystemShares(List<Object> systemShares) {
+        this.systemShares = systemShares;
+    }
+
+    public String getGatewaySoftwareVersion() {
+        return gatewaySoftwareVersion;
+    }
+
+    public void setGatewaySoftwareVersion(String gatewaySoftwareVersion) {
+        this.gatewaySoftwareVersion = gatewaySoftwareVersion;
+    }
+
+    public String getUserNameOwner() {
+        return userNameOwner;
+    }
+
+    public void setUserNameOwner(String userNameOwner) {
+        this.userNameOwner = userNameOwner;
+    }
+
+    public @Nullable Integer getSystemShareId() {
+        return systemShareId;
+    }
+
+    public void setSystemShareId(Integer systemShareId) {
+        this.systemShareId = systemShareId;
+    }
+
+    public String getOperatorName() {
+        return operatorName;
+    }
+
+    public void setOperatorName(String operatorName) {
+        this.operatorName = operatorName;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public void setLocation(String location) {
+        this.location = location;
+    }
+
+    public String getInstallationDate() {
+        return installationDate;
+    }
+
+    public void setInstallationDate(String installationDate) {
+        this.installationDate = installationDate;
+    }
+
+    public Integer getImageId() {
+        return imageId;
+    }
+
+    public void setImageId(Integer imageId) {
+        this.imageId = imageId;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetSystemStateListDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/GetSystemStateListDTO.java
new file mode 100644 (file)
index 0000000..9dbcb1a
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class GetSystemStateListDTO {
+
+    @SerializedName("SystemId")
+    @Expose
+    private Integer systemId;
+    @SerializedName("GatewayState")
+    @Expose
+    private GatewayStateDTO gatewayState;
+    @SerializedName("AccessLevel")
+    @Expose
+    private Integer accessLevel;
+    @SerializedName("IsSystemShareDeleted")
+    @Expose
+    private Boolean isSystemShareDeleted;
+    @SerializedName("IsSystemShareRejected")
+    @Expose
+    private Boolean isSystemShareRejected;
+    @SerializedName("IsSystemDeleted")
+    @Expose
+    private Boolean isSystemDeleted;
+    @SerializedName("FirstActiveAlertCode")
+    @Expose
+    private Integer firstActiveAlertCode;
+
+    public Integer getSystemId() {
+        return systemId;
+    }
+
+    public void setSystemId(Integer systemId) {
+        this.systemId = systemId;
+    }
+
+    public GatewayStateDTO getGatewayState() {
+        return gatewayState;
+    }
+
+    public void setGatewayState(GatewayStateDTO gatewayState) {
+        this.gatewayState = gatewayState;
+    }
+
+    public Integer getAccessLevel() {
+        return accessLevel;
+    }
+
+    public void setAccessLevel(Integer accessLevel) {
+        this.accessLevel = accessLevel;
+    }
+
+    public Boolean getIsSystemShareDeleted() {
+        return isSystemShareDeleted;
+    }
+
+    public void setIsSystemShareDeleted(Boolean isSystemShareDeleted) {
+        this.isSystemShareDeleted = isSystemShareDeleted;
+    }
+
+    public Boolean getIsSystemShareRejected() {
+        return isSystemShareRejected;
+    }
+
+    public void setIsSystemShareRejected(Boolean isSystemShareRejected) {
+        this.isSystemShareRejected = isSystemShareRejected;
+    }
+
+    public Boolean getIsSystemDeleted() {
+        return isSystemDeleted;
+    }
+
+    public void setIsSystemDeleted(Boolean isSystemDeleted) {
+        this.isSystemDeleted = isSystemDeleted;
+    }
+
+    public Integer getFirstActiveAlertCode() {
+        return firstActiveAlertCode;
+    }
+
+    public void setFirstActiveAlertCode(Integer firstActiveAlertCode) {
+        this.firstActiveAlertCode = firstActiveAlertCode;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/HistoryMessageDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/HistoryMessageDTO.java
new file mode 100644 (file)
index 0000000..ad292e7
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class HistoryMessageDTO {
+
+    @SerializedName("Id")
+    @Expose
+    private Integer id;
+    @SerializedName("ErrorCode")
+    @Expose
+    private Integer errorCode;
+    @SerializedName("Description")
+    @Expose
+    private String description;
+    @SerializedName("OccurTimeLocal")
+    @Expose
+    private String occurTimeLocal;
+    @SerializedName("Active")
+    @Expose
+    private Boolean active;
+    @SerializedName("Index")
+    @Expose
+    private Integer index;
+    @SerializedName("Device")
+    @Expose
+    private String device;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getErrorCode() {
+        return errorCode;
+    }
+
+    public void setErrorCode(Integer errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getOccurTimeLocal() {
+        return occurTimeLocal;
+    }
+
+    public void setOccurTimeLocal(String occurTimeLocal) {
+        this.occurTimeLocal = occurTimeLocal;
+    }
+
+    public Boolean getActive() {
+        return active;
+    }
+
+    public void setActive(Boolean active) {
+        this.active = active;
+    }
+
+    public Integer getIndex() {
+        return index;
+    }
+
+    public void setIndex(Integer index) {
+        this.index = index;
+    }
+
+    public String getDevice() {
+        return device;
+    }
+
+    public void setDevice(String device) {
+        this.device = device;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/LoginResponseDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/LoginResponseDTO.java
new file mode 100644 (file)
index 0000000..f6f6a23
--- /dev/null
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class LoginResponseDTO {
+
+    @SerializedName("access_token")
+    @Expose
+    private String accessToken;
+    @SerializedName("expires_in")
+    @Expose
+    private Integer expiresIn;
+    @SerializedName("token_type")
+    @Expose
+    private String tokenType;
+    @SerializedName("refresh_token")
+    @Expose
+    private String refreshToken;
+    @SerializedName("scope")
+    @Expose
+    private String scope;
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public void setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+    }
+
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+
+    public String getTokenType() {
+        return tokenType;
+    }
+
+    public void setTokenType(String tokenType) {
+        this.tokenType = tokenType;
+    }
+
+    public String getRefreshToken() {
+        return refreshToken;
+    }
+
+    public void setRefreshToken(String refreshToken) {
+        this.refreshToken = refreshToken;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/MenuItemDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/MenuItemDTO.java
new file mode 100644 (file)
index 0000000..ffe488d
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class MenuItemDTO {
+
+    @SerializedName("Name")
+    @Expose
+    private String name;
+    @SerializedName("SubMenuEntries")
+    @Expose
+    private List<SubMenuEntryDTO> subMenuEntries = null;
+    @SerializedName("ParameterNode")
+    @Expose
+    private Boolean parameterNode;
+    @SerializedName("ImageName")
+    @Expose
+    private String imageName;
+    @SerializedName("TabViews")
+    @Expose
+    private List<Object> tabViews = null;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public List<SubMenuEntryDTO> getSubMenuEntries() {
+        return subMenuEntries;
+    }
+
+    public void setSubMenuEntries(List<SubMenuEntryDTO> subMenuEntries) {
+        this.subMenuEntries = subMenuEntries;
+    }
+
+    public Boolean getParameterNode() {
+        return parameterNode;
+    }
+
+    public void setParameterNode(Boolean parameterNode) {
+        this.parameterNode = parameterNode;
+    }
+
+    public String getImageName() {
+        return imageName;
+    }
+
+    public void setImageName(String imageName) {
+        this.imageName = imageName;
+    }
+
+    public List<Object> getTabViews() {
+        return tabViews;
+    }
+
+    public void setTabViews(List<Object> tabViews) {
+        this.tabViews = tabViews;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/MenuItemTabViewDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/MenuItemTabViewDTO.java
new file mode 100644 (file)
index 0000000..0e9d4b1
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+public class MenuItemTabViewDTO {
+    @SerializedName("IsExpertView")
+    @Expose
+    public Boolean isExpertView;
+
+    @SerializedName("TabName")
+    @Expose
+    public String tabName;
+
+    @SerializedName("GuiId")
+    @Expose
+    public Long guiId;
+
+    @SerializedName("BundleId")
+    @Expose
+    public Long bundleId;
+
+    @SerializedName("ParameterDescriptors")
+    @Expose
+    public List<ParameterDescriptorDTO> parameterDescriptors;
+
+    @SerializedName("ViewType")
+    @Expose
+    public Long viewType;
+
+    @SerializedName("SvgSchemaDeviceId")
+    @Expose
+    public Long svgSchemaDeviceId;
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ParameterDescriptorDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ParameterDescriptorDTO.java
new file mode 100644 (file)
index 0000000..1f8aae1
--- /dev/null
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+public class ParameterDescriptorDTO {
+    @SerializedName("ValueId")
+    @Expose
+    public Long valueId;
+
+    @SerializedName("SortId")
+    @Expose
+    public Long sortId;
+
+    @SerializedName("SubBundleId")
+    @Expose
+    public Long subBundleId;
+
+    @SerializedName("ParameterId")
+    @Expose
+    public Long parameterId;
+
+    @SerializedName("IsReadOnly")
+    @Expose
+    public Boolean isReadOnly;
+
+    @SerializedName("NoDataPoint")
+    @Expose
+    public Boolean noDataPoint;
+
+    @SerializedName("IsExpertProtectable")
+    @Expose
+    public Boolean isExpertProtectable;
+
+    @SerializedName("Name")
+    @Expose
+    public String name;
+
+    @SerializedName("Group")
+    @Expose
+    public String group;
+
+    @SerializedName("ControlType")
+    @Expose
+    public Integer controlType;
+
+    @SerializedName("Value")
+    @Expose
+    public String value;
+
+    @SerializedName("ValueState")
+    @Expose
+    public Long valueState;
+
+    @SerializedName("HasDependentParameter")
+    @Expose
+    public Boolean hasDependentParameter;
+
+    @SerializedName("ProtGrp")
+    @Expose
+    public String protGrp;
+
+    @SerializedName("Unit")
+    @Expose
+    public String unit;
+
+    @SerializedName("Decimals")
+    @Expose
+    public Long decimals;
+
+    @SerializedName("MinValueCondition")
+    @Expose
+    public String minValueCondition;
+
+    @SerializedName("MaxValueCondition")
+    @Expose
+    public String maxValueCondition;
+
+    @SerializedName("MinValue")
+    @Expose
+    public Long minValue;
+
+    @SerializedName("MaxValue")
+    @Expose
+    public Long maxValue;
+
+    @SerializedName("StepWidth")
+    @Expose
+    public double stepWidth;
+
+    @SerializedName("NamePrefix")
+    @Expose
+    public String namePrefix;
+
+    @SerializedName("TurnOffValue")
+    @Expose
+    public Long turnOffValue;
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ReadFaultMessagesDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ReadFaultMessagesDTO.java
new file mode 100644 (file)
index 0000000..6fa2bab
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class ReadFaultMessagesDTO {
+
+    @SerializedName("MessagesChanged")
+    @Expose
+    private Boolean messagesChanged;
+    @SerializedName("LastRead")
+    @Expose
+    private String lastRead;
+    @SerializedName("CurrentMessages")
+    @Expose
+    private List<CurrentMessageDTO> currentMessages = null;
+    @SerializedName("HistoryMessages")
+    @Expose
+    private List<HistoryMessageDTO> historyMessages = null;
+    @SerializedName("DeviceFaultMessages")
+    @Expose
+    private List<Object> deviceFaultMessages = null;
+
+    public Boolean getMessagesChanged() {
+        return messagesChanged;
+    }
+
+    public void setMessagesChanged(Boolean messagesChanged) {
+        this.messagesChanged = messagesChanged;
+    }
+
+    public String getLastRead() {
+        return lastRead;
+    }
+
+    public void setLastRead(String lastRead) {
+        this.lastRead = lastRead;
+    }
+
+    public List<CurrentMessageDTO> getCurrentMessages() {
+        return currentMessages;
+    }
+
+    public void setCurrentMessages(List<CurrentMessageDTO> currentMessages) {
+        this.currentMessages = currentMessages;
+    }
+
+    public List<HistoryMessageDTO> getHistoryMessages() {
+        return historyMessages;
+    }
+
+    public void setHistoryMessages(List<HistoryMessageDTO> historyMessages) {
+        this.historyMessages = historyMessages;
+    }
+
+    public List<Object> getDeviceFaultMessages() {
+        return deviceFaultMessages;
+    }
+
+    public void setDeviceFaultMessages(List<Object> deviceFaultMessages) {
+        this.deviceFaultMessages = deviceFaultMessages;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/SubMenuEntryDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/SubMenuEntryDTO.java
new file mode 100644 (file)
index 0000000..4ab599f
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class SubMenuEntryDTO {
+
+    @SerializedName("Name")
+    @Expose
+    private String name;
+    @SerializedName("SubMenuEntries")
+    @Expose
+    private List<Object> subMenuEntries = null;
+    @SerializedName("ParameterNode")
+    @Expose
+    private Boolean parameterNode;
+    @SerializedName("ImageName")
+    @Expose
+    private String imageName;
+    @SerializedName("TabViews")
+    @Expose
+    private List<MenuItemTabViewDTO> tabViews = null;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public List<Object> getSubMenuEntries() {
+        return subMenuEntries;
+    }
+
+    public void setSubMenuEntries(List<Object> subMenuEntries) {
+        this.subMenuEntries = subMenuEntries;
+    }
+
+    public Boolean getParameterNode() {
+        return parameterNode;
+    }
+
+    public void setParameterNode(Boolean parameterNode) {
+        this.parameterNode = parameterNode;
+    }
+
+    public String getImageName() {
+        return imageName;
+    }
+
+    public void setImageName(String imageName) {
+        this.imageName = imageName;
+    }
+
+    public List<MenuItemTabViewDTO> getTabViews() {
+        return tabViews;
+    }
+
+    public void setTabViews(List<MenuItemTabViewDTO> tabViews) {
+        this.tabViews = tabViews;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/SubMenuEntryWithMenuItemTabView.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/SubMenuEntryWithMenuItemTabView.java
new file mode 100644 (file)
index 0000000..5c2ec66
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+/**
+ * Link the SubMenuEntryDTO with the MenuItemTabViewDTO
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+public class SubMenuEntryWithMenuItemTabView {
+    public SubMenuEntryDTO subMenuEntryDTO;
+    public MenuItemTabViewDTO menuItemTabViewDTO;
+
+    public SubMenuEntryWithMenuItemTabView(SubMenuEntryDTO subMenuEntryDTO, MenuItemTabViewDTO menuItemTabViewDTO) {
+        this.subMenuEntryDTO = subMenuEntryDTO;
+        this.menuItemTabViewDTO = menuItemTabViewDTO;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ValueDTO.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/dto/ValueDTO.java
new file mode 100644 (file)
index 0000000..f0e021d
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.dto;
+
+import javax.annotation.Generated;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * generated with https://www.jsonschema2pojo.org/
+ * 
+ * @author Bo Biene - Initial contribution
+ */
+@Generated("jsonschema2pojo")
+public class ValueDTO {
+
+    @SerializedName("ValueId")
+    @Expose
+    private Long valueId;
+    @SerializedName("Value")
+    @Expose
+    private String value;
+    @SerializedName("State")
+    @Expose
+    private Integer state;
+
+    public Long getValueId() {
+        return valueId;
+    }
+
+    public void setValueId(Long valueId) {
+        this.valueId = valueId;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public Integer getState() {
+        return state;
+    }
+
+    public void setState(Integer state) {
+        this.state = state;
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetAccountBridgeHandler.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetAccountBridgeHandler.java
new file mode 100644 (file)
index 0000000..50a64ab
--- /dev/null
@@ -0,0 +1,276 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.handler;
+
+import static org.openhab.binding.wolfsmartset.internal.WolfSmartsetBindingConstants.CONFIG_SYSTEM_ID;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.wolfsmartset.internal.api.WolfSmartsetApi;
+import org.openhab.binding.wolfsmartset.internal.api.WolfSmartsetCloudException;
+import org.openhab.binding.wolfsmartset.internal.config.WolfSmartsetAccountConfiguration;
+import org.openhab.binding.wolfsmartset.internal.discovery.WolfSmartsetAccountDiscoveryService;
+import org.openhab.binding.wolfsmartset.internal.dto.GetGuiDescriptionForGatewayDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WolfSmartsetAccountBridgeHandler} is responsible for managing
+ * communication with the WolfSmartset API.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetAccountBridgeHandler extends BaseBridgeHandler {
+    private static final int REFRESH_STARTUP_DELAY_SECONDS = 3;
+    private static final int REFRESH_INTERVAL_SECONDS = 1;
+    private static final int DEFAULT_REFRESH_INTERVAL_CONFIGURATION_MINUTES = 10;
+    private static final int DEFAULT_REFRESH_INTERVAL_VALUES_SECONDS = 15;
+
+    private final Logger logger = LoggerFactory.getLogger(WolfSmartsetAccountBridgeHandler.class);
+
+    private final HttpClient httpClient;
+
+    private @NonNullByDefault({}) WolfSmartsetApi api;
+    private int refreshIntervalStructureMinutes;
+    private int refreshIntervalValuesSeconds;
+    private boolean discoveryEnabled;
+    private @Nullable List<GetSystemListDTO> cachedSystems = null;
+
+    private final Map<String, WolfSmartsetSystemBridgeHandler> systemHandlers = new ConcurrentHashMap<>();
+    private final Set<String> systemIds = new CopyOnWriteArraySet<>();
+
+    private @Nullable Future<?> refreshSystemsJob;
+    private final AtomicInteger refreshConfigurationCounter = new AtomicInteger(REFRESH_STARTUP_DELAY_SECONDS);
+    private final AtomicInteger refreshValuesCounter = new AtomicInteger(REFRESH_STARTUP_DELAY_SECONDS);
+
+    public WolfSmartsetAccountBridgeHandler(final Bridge bridge, HttpClient httpClient) {
+        super(bridge);
+        this.httpClient = httpClient;
+    }
+
+    @Override
+    public void initialize() {
+        logger.debug("AccountBridge: Initializing");
+
+        WolfSmartsetAccountConfiguration config = getConfigAs(WolfSmartsetAccountConfiguration.class);
+
+        Integer value;
+        value = config.refreshIntervalStructure;
+        refreshIntervalStructureMinutes = value == null ? DEFAULT_REFRESH_INTERVAL_CONFIGURATION_MINUTES : value;
+
+        value = config.refreshIntervalValues;
+        refreshIntervalValuesSeconds = value == null ? DEFAULT_REFRESH_INTERVAL_VALUES_SECONDS : value;
+
+        String username = config.username;
+        String password = config.password;
+        username = username == null ? "" : username;
+        password = password == null ? "" : password;
+
+        Boolean booleanValue = config.discoveryEnabled;
+        discoveryEnabled = booleanValue == null ? false : booleanValue.booleanValue();
+        logger.debug("AccountBridge: System and unit discovery is {}", discoveryEnabled ? "enabled" : "disabled");
+        if (username.trim().isEmpty() || password.trim().isEmpty()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing username or password");
+        } else {
+            try {
+                api = new WolfSmartsetApi(username, password, httpClient, scheduler);
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Checking authorization");
+                scheduleRefreshJob();
+            } catch (WolfSmartsetCloudException e) {
+                logger.error("unable to create wolf smartset api", e);
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public void dispose() {
+        cancelRefreshJob();
+        api.stopRequestQueue();
+        logger.debug("AccountBridge: Disposing");
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(WolfSmartsetAccountDiscoveryService.class);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    @Override
+    public void childHandlerInitialized(ThingHandler systemHandler, Thing systemThing) {
+        String systemId = (String) systemThing.getConfiguration().get(CONFIG_SYSTEM_ID);
+        systemHandlers.put(systemId, (WolfSmartsetSystemBridgeHandler) systemHandler);
+        systemIds.add(systemId);
+        scheduleRefreshJob();
+        logger.debug("AccountBridge: Adding system handler for {} with id {}", systemThing.getUID(), systemId);
+    }
+
+    @Override
+    public void childHandlerDisposed(ThingHandler systemHandler, Thing systemThing) {
+        String systemId = (String) systemThing.getConfiguration().get(CONFIG_SYSTEM_ID);
+        systemHandlers.remove(systemId);
+        systemIds.remove(systemId);
+        logger.debug("AccountBridge: Removing system handler for {} with id {}", systemThing.getUID(), systemId);
+    }
+
+    /**
+     * returns truee if BackgroundDiscoveryEnabled
+     */
+    public boolean isBackgroundDiscoveryEnabled() {
+        return discoveryEnabled;
+    }
+
+    /**
+     * returns the list of the GetSystemListDTO available
+     */
+    public @Nullable List<GetSystemListDTO> getRegisteredSystems() {
+        return cachedSystems;
+    }
+
+    /**
+     * force a full update of the wolf smartset cloud configuration
+     */
+    public void scheduleRefreshJob() {
+        logger.debug("AccountBridge: Scheduling system refresh job");
+        cancelRefreshJob();
+        refreshConfigurationCounter.set(0);
+        refreshValuesCounter.set(0);
+        refreshSystemsJob = scheduler.scheduleWithFixedDelay(this::refreshSystems, REFRESH_STARTUP_DELAY_SECONDS,
+                REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * The refresh job updates the system channels on the refresh interval set in the system thing config.
+     * The system update process involves first running a system summary transaction to
+     * determine if any system data has changed since the last summary. If any change is detected,
+     * a full query of the systems is performed.
+     */
+    private void refreshSystems() {
+        if (refreshConfigurationCounter.getAndDecrement() == 0) {
+            refreshConfigurationCounter.set(refreshIntervalStructureMinutes * 60);
+            if (api.login()) {
+                logger.debug("AccountBridge: refreshing configuration");
+                updateStatus(ThingStatus.ONLINE);
+                cachedSystems = api.getSystems();
+                if (cachedSystems != null) {
+                    for (GetSystemListDTO system : api.getSystems()) {
+                        WolfSmartsetSystemBridgeHandler handler = systemHandlers.get(system.getId().toString());
+                        if (handler != null) {
+                            GetGuiDescriptionForGatewayDTO systemDescription = api.getSystemDescription(system.getId(),
+                                    system.getGatewayId());
+                            handler.updateConfiguration(system, systemDescription);
+                        }
+                    }
+                }
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Authorization failed");
+            }
+        }
+
+        if (refreshValuesCounter.getAndDecrement() == 0) {
+            refreshValuesCounter.set(refreshIntervalValuesSeconds);
+            if (api.login()) {
+                logger.debug("AccountBridge: refreshing values");
+                updateStatus(ThingStatus.ONLINE);
+
+                var systemConfigs = systemHandlers.values().stream().map(s -> s.getSystemConfig())
+                        .filter(s -> s != null).collect(Collectors.toSet());
+                if (systemConfigs != null && systemConfigs.size() > 0) {
+                    var systemStates = api.getSystemState(systemConfigs);
+                    if (systemStates != null) {
+                        for (var systemState : systemStates) {
+                            if (systemState != null) {
+                                var systemHandler = systemHandlers.get(systemState.getSystemId().toString());
+                                if (systemHandler != null) {
+                                    systemHandler.updateSystemState(systemState);
+                                }
+                            }
+                        }
+                    } else {
+                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                                "Failed to update system states");
+                    }
+
+                    for (var systemHandler : systemHandlers.values()) {
+                        if (systemHandler != null) {
+                            var systemConfig = systemHandler.getSystemConfig();
+                            if (systemConfig != null) {
+                                var faultMessages = api.getFaultMessages(systemConfig.getId(),
+                                        systemConfig.getGatewayId());
+
+                                systemHandler.updateFaultMessages(faultMessages);
+
+                                for (var unitHandler : systemHandler.getUnitHandler()) {
+                                    if (unitHandler != null) {
+                                        var tabmenu = unitHandler.getTabMenu();
+                                        if (tabmenu != null) {
+                                            var lastRefreshTime = unitHandler.getLastRefreshTime();
+                                            var valueIds = tabmenu.parameterDescriptors.stream()
+                                                    .filter(p -> p.valueId > 0).map(p -> p.valueId)
+                                                    .collect(Collectors.toList());
+                                            var paramValues = api.getGetParameterValues(systemConfig.getId(),
+                                                    systemConfig.getGatewayId(), tabmenu.bundleId, valueIds,
+                                                    lastRefreshTime);
+
+                                            unitHandler.updateValues(paramValues);
+                                        }
+                                    }
+                                }
+                            } else {
+                                // waiting for config.
+                                systemHandler.updateSystemState(null);
+                            }
+                        }
+                    }
+                }
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Authorization failed");
+            }
+        }
+    }
+
+    private void cancelRefreshJob() {
+        Future<?> localRefreshSystemsJob = refreshSystemsJob;
+        if (localRefreshSystemsJob != null) {
+            localRefreshSystemsJob.cancel(true);
+            logger.debug("AccountBridge: Canceling system refresh job");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetSystemBridgeHandler.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetSystemBridgeHandler.java
new file mode 100644 (file)
index 0000000..154c7ee
--- /dev/null
@@ -0,0 +1,268 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.handler;
+
+import static org.openhab.binding.wolfsmartset.internal.WolfSmartsetBindingConstants.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wolfsmartset.internal.config.WolfSmartsetSystemConfiguration;
+import org.openhab.binding.wolfsmartset.internal.discovery.WolfSmartsetSystemDiscoveryService;
+import org.openhab.binding.wolfsmartset.internal.dto.GetGuiDescriptionForGatewayDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemListDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.GetSystemStateListDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.ReadFaultMessagesDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.SubMenuEntryWithMenuItemTabView;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WolfSmartsetSystemBridgeHandler} is the handler for an WolfSmartset system.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetSystemBridgeHandler extends BaseBridgeHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(WolfSmartsetSystemBridgeHandler.class);
+
+    private @NonNullByDefault({}) String systemId;
+
+    private final Map<String, WolfSmartsetUnitThingHandler> unitHandlers = new ConcurrentHashMap<>();
+
+    private @Nullable GetSystemListDTO savedSystem;
+    private @Nullable List<SubMenuEntryWithMenuItemTabView> savedUnits;
+    private Map<String, State> stateCache = new ConcurrentHashMap<>();
+
+    public WolfSmartsetSystemBridgeHandler(Bridge bridge) {
+        super(bridge);
+    }
+
+    @Override
+    public void initialize() {
+        systemId = getConfigAs(WolfSmartsetSystemConfiguration.class).systemId;
+        logger.debug("SystemBridge: Initializing system '{}'", systemId);
+        clearSavedState();
+        updateStatus(WolfSmartsetUtils.isBridgeOnline(getBridge()) ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(WolfSmartsetSystemDiscoveryService.class);
+    }
+
+    @Override
+    public void dispose() {
+        logger.debug("SystemBridge: Disposing system '{}'", systemId);
+    }
+
+    @Override
+    public void childHandlerInitialized(ThingHandler unitHandler, Thing unitThing) {
+        String unitId = (String) unitThing.getConfiguration().get(CONFIG_UNIT_ID);
+        unitHandlers.put(unitId, (WolfSmartsetUnitThingHandler) unitHandler);
+        logger.debug("SystemBridge: Saving unit handler for {} with id {}", unitThing.getUID(), unitId);
+        var accountBridgeHandler = getAccountBridgeHandler();
+        if (accountBridgeHandler != null) {
+            accountBridgeHandler.scheduleRefreshJob();
+        }
+    }
+
+    @Override
+    public void childHandlerDisposed(ThingHandler unitHandler, Thing unitThing) {
+        String unitId = (String) unitThing.getConfiguration().get(CONFIG_UNIT_ID);
+        unitHandlers.remove(unitId);
+        logger.debug("SystemBridge: Removing unit handler for {} with id {}", unitThing.getUID(), unitId);
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+            updateStatus(ThingStatus.ONLINE);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (command instanceof RefreshType) {
+            State state = stateCache.get(channelUID.getId());
+            if (state != null) {
+                updateState(channelUID.getId(), state);
+            }
+        }
+    }
+
+    /**
+     * Return the associated account bridge handler
+     * 
+     * @return returns the {@link WolfSmartsetAccountBridgeHandler} linked to this
+     *         {@link WolfSmartsetSystemBridgeHandler}
+     */
+    public @Nullable WolfSmartsetAccountBridgeHandler getAccountBridgeHandler() {
+        var bridgeHandler = this.getBridge();
+        if (bridgeHandler != null) {
+            return (WolfSmartsetAccountBridgeHandler) bridgeHandler.getHandler();
+        }
+        return null;
+    }
+
+    /**
+     * Return the subordinated unit handler
+     * 
+     * @return a List of {@link WolfSmartsetUnitThingHandler} with the subordinated unit handler
+     */
+    public Collection<WolfSmartsetUnitThingHandler> getUnitHandler() {
+        return unitHandlers.values();
+    }
+
+    /**
+     * Returns the list configuration of the units available for this system
+     * 
+     * @return a list of {@link SubMenuEntryWithMenuItemTabView} representing the available units for this system
+     */
+    public List<SubMenuEntryWithMenuItemTabView> getUnits() {
+        List<SubMenuEntryWithMenuItemTabView> localSavedUnits = savedUnits;
+        return localSavedUnits == null ? EMPTY_UNITS : localSavedUnits;
+    }
+
+    /**
+     * Return the configuration of this system
+     * 
+     * @return {@link GetSystemListDTO} representing the this system
+     */
+    public @Nullable GetSystemListDTO getSystemConfig() {
+        return savedSystem;
+    }
+
+    /**
+     * Return the id of this system
+     * 
+     * @return the id of this system
+     */
+    public String getSystemId() {
+        return systemId;
+    }
+
+    /**
+     * Update the system state with the dto
+     * 
+     * @param systemState {@link GetSystemStateListDTO} the dto representing the current state of this system
+     */
+    public void updateSystemState(@Nullable GetSystemStateListDTO systemState) {
+        if (systemState != null) {
+            if (systemState.getIsSystemDeleted()) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "System has been deleted");
+            } else if (systemState.getIsSystemShareDeleted()) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "System share has been removed");
+            } else if (systemState.getIsSystemShareRejected()) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "System share has been rejected");
+            }
+        }
+    }
+
+    /**
+     * Process the available fault messages
+     * 
+     * @param faultMessages {@link ReadFaultMessagesDTO} the dto representing the list of the current faultmessages
+     */
+    public void updateFaultMessages(@Nullable ReadFaultMessagesDTO faultMessages) {
+        if (faultMessages != null) {
+            if (faultMessages.getCurrentMessages() != null) {
+                for (var message : faultMessages.getCurrentMessages()) {
+                    logger.warn("System {} faultmessage: {}, since {}", systemId, message.getDescription(),
+                            message.getOccurTimeLocal());
+                }
+            }
+        }
+    }
+
+    /**
+     * Update the configuration of the system and the subordinated units
+     * 
+     * @param system {@link GetSystemListDTO} representing this system
+     * @param systemDescription {@link GetGuiDescriptionForGatewayDTO} repesenting the units of this system
+     */
+    public void updateConfiguration(@Nullable GetSystemListDTO system,
+            @Nullable GetGuiDescriptionForGatewayDTO systemDescription) {
+        if (system != null && systemDescription != null) {
+            logger.debug("SystemBridge: Updating channels for system id {}, name {}", system.getId(), system.getName());
+            updateStatus(ThingStatus.ONLINE);
+            savedSystem = system;
+
+            Map<String, String> properties = editProperties();
+            properties.put(Thing.PROPERTY_FIRMWARE_VERSION, system.getGatewaySoftwareVersion());
+            properties.put(THING_PROPERTY_GATEWAY_ID, system.getGatewayId().toString());
+            properties.put(THING_PROPERTY_GATEWAY_USERNAME, system.getGatewayUsername());
+            properties.put(THING_PROPERTY_INSTALLATION_DATE, system.getInstallationDate());
+            properties.put(THING_PROPERTY_LOCATION, system.getLocation());
+            properties.put(THING_PROPERTY_OPERATOR_NAME, system.getOperatorName());
+            properties.put(THING_PROPERTY_USERNAME_OWNER, system.getUserNameOwner());
+            properties.put(THING_PROPERTY_ACCESSLEVEL, system.getAccessLevel().toString());
+            updateProperties(properties);
+
+            updateUnitsConfiguration(systemDescription);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "Unable to retrieve configuration");
+        }
+    }
+
+    private void updateUnitsConfiguration(GetGuiDescriptionForGatewayDTO systemDescription) {
+        List<SubMenuEntryWithMenuItemTabView> listUnits = new ArrayList<>();
+        var fachmannNode = systemDescription.getMenuItems().stream()
+                .filter(m -> "Fachmann".equalsIgnoreCase(m.getName())).findFirst();
+
+        if (fachmannNode.isPresent()) {
+            for (var submenu : fachmannNode.get().getSubMenuEntries()) {
+                for (var tabmenu : submenu.getTabViews()) {
+                    listUnits.add(new SubMenuEntryWithMenuItemTabView(submenu, tabmenu));
+
+                    var handler = unitHandlers.get(tabmenu.bundleId.toString());
+                    if (handler != null) {
+                        handler.updateConfiguration(submenu, tabmenu);
+                    }
+                }
+            }
+
+        }
+        savedUnits = listUnits;
+    }
+
+    private void clearSavedState() {
+        savedSystem = null;
+        savedUnits = null;
+        stateCache.clear();
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetUnitThingHandler.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetUnitThingHandler.java
new file mode 100644 (file)
index 0000000..4733244
--- /dev/null
@@ -0,0 +1,268 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.handler;
+
+import static org.openhab.binding.wolfsmartset.internal.WolfSmartsetBindingConstants.*;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.wolfsmartset.internal.config.WolfSmartsetUnitConfiguration;
+import org.openhab.binding.wolfsmartset.internal.dto.GetParameterValuesDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.MenuItemTabViewDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.ParameterDescriptorDTO;
+import org.openhab.binding.wolfsmartset.internal.dto.SubMenuEntryDTO;
+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.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link WolfSmartsetUnitThingHandler} is responsible for updating the channels associated
+ * with an WolfSmartset unit.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public class WolfSmartsetUnitThingHandler extends BaseThingHandler {
+
+    public static final String CAPABILITY_ADC = "adc";
+    public static final String CAPABILITY_CO2 = "co2";
+    public static final String CAPABILITY_DRY_CONTACT = "dryContact";
+    public static final String CAPABILITY_HUMIDITY = "humidity";
+    public static final String CAPABILITY_OCCUPANCY = "occupancy";
+    public static final String CAPABILITY_TEMPERATURE = "temperature";
+    public static final String CAPABILITY_UNKNOWN = "unknown";
+
+    private final Logger logger = LoggerFactory.getLogger(WolfSmartsetUnitThingHandler.class);
+
+    private @NonNullByDefault({}) String unitId;
+    private @Nullable Instant lastRefreshTime;
+
+    private Map<String, State> stateCache = new ConcurrentHashMap<>();
+    private Map<Long, ParameterDescriptorDTO> paramDescriptionMap = new ConcurrentHashMap<>();
+    private @Nullable SubMenuEntryDTO submenu;
+    private @Nullable MenuItemTabViewDTO tabmenu;
+
+    public WolfSmartsetUnitThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        unitId = getConfigAs(WolfSmartsetUnitConfiguration.class).unitId;
+        logger.debug("UnitThing: Initializing unit '{}'", unitId);
+        clearSavedState();
+        var bridgeHandler = getBridge();
+        if (bridgeHandler != null) {
+            bridgeStatusChanged(bridgeHandler.getStatusInfo());
+        }
+    }
+
+    @Override
+    public void dispose() {
+        logger.debug("UnitThing: Disposing unit '{}'", unitId);
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+            if (this.submenu != null && this.tabmenu != null) {
+                updateStatus(ThingStatus.ONLINE);
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING);
+            }
+
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (command instanceof RefreshType) {
+            State state = stateCache.get(channelUID.getId());
+            if (state != null) {
+                updateState(channelUID.getId(), state);
+            }
+        }
+    }
+
+    /**
+     * Get the {@link SubMenuEntryDTO} for this unit
+     * 
+     * @return the {@link SubMenuEntryDTO} for this unit
+     */
+    public @Nullable SubMenuEntryDTO getSubMenu() {
+        return this.submenu;
+    }
+
+    /**
+     * Get the {@link MenuItemTabViewDTO} for this unit
+     * 
+     * @return the {@link MenuItemTabViewDTO} for this unit
+     */
+    public @Nullable MenuItemTabViewDTO getTabMenu() {
+        return this.tabmenu;
+    }
+
+    /**
+     * Get the {@link Instant} of the last valid call of updateValues
+     * 
+     * @return the getInstallationDate of the last valid call of updateValues
+     */
+    public @Nullable Instant getLastRefreshTime() {
+        return this.lastRefreshTime;
+    }
+
+    /**
+     * Update the configuration of this unit and create / update the related channels
+     * 
+     * @param submenu the {@link SubMenuEntryDTO} for this unit
+     * @param tabmenu the {@link MenuItemTabViewDTO} for this unit
+     */
+    public void updateConfiguration(SubMenuEntryDTO submenu, MenuItemTabViewDTO tabmenu) {
+        this.submenu = submenu;
+        this.tabmenu = tabmenu;
+        var bridgeHandler = getBridge();
+        if (bridgeHandler != null) {
+            bridgeStatusChanged(bridgeHandler.getStatusInfo());
+        }
+        lastRefreshTime = null;
+
+        ThingBuilder thingBuilder = editThing();
+        var thingId = thing.getUID();
+
+        paramDescriptionMap.clear();
+        for (var param : tabmenu.parameterDescriptors) {
+            paramDescriptionMap.put(param.valueId, param);
+            var channelId = new ChannelUID(thingId, param.parameterId.toString()); // "bindingId:type:thingId:1")
+            if (thing.getChannel(channelId) == null) {
+                logger.debug("UnitThing: Create channel '{}'", channelId);
+                Channel channel = ChannelBuilder.create(channelId, getItemType(param.controlType)).withLabel(param.name)
+                        .withType(getChannelType(param)).build();
+                thingBuilder.withChannel(channel);
+            }
+        }
+
+        updateThing(thingBuilder.build());
+
+        for (var param : tabmenu.parameterDescriptors) {
+            var channelId = new ChannelUID(thingId, param.parameterId.toString());
+            setState(channelId, WolfSmartsetUtils.undefOrString(param.value));
+        }
+    }
+
+    /**
+     * Update the values of the channels
+     * 
+     * @param values {@link GetParameterValuesDTO} representing the new values
+     */
+    public void updateValues(@Nullable GetParameterValuesDTO values) {
+        var thingId = thing.getUID();
+        if (values != null && values.getValues() != null && values.getValues().size() > 0) {
+            if (!values.getIsNewJobCreated()) {
+                lastRefreshTime = Instant.now();
+            }
+
+            for (var value : values.getValues()) {
+                var param = paramDescriptionMap.get(value.getValueId());
+                if (param != null) {
+                    var channelId = new ChannelUID(thingId, param.parameterId.toString());
+                    setState(channelId, WolfSmartsetUtils.undefOrString(value.getValue()));
+                }
+            }
+        }
+    }
+
+    /**
+     * Stores the state for the channel in stateCache and calls updateState of this Thing
+     * 
+     * @param channelId {@link ChannelUID} the id of the channel to update
+     * @param state {@link State} the new state for the channel
+     */
+    private void setState(ChannelUID channelId, State state) {
+        stateCache.put(channelId.getId(), state);
+        updateState(channelId, state);
+    }
+
+    private ChannelTypeUID getChannelType(ParameterDescriptorDTO parmeter) {
+        if (parmeter.unit == null || parmeter.unit.isBlank()) {
+            if (parmeter.controlType == null) {
+                return new ChannelTypeUID(BINDING_ID, CH_STRING);
+            } else {
+                switch (parmeter.controlType) {
+                    case 1:
+                    case 3:
+                    case 6:
+                    case 8:
+                        return new ChannelTypeUID(BINDING_ID, CH_NUMBER);
+                    case 5:
+                        return new ChannelTypeUID(BINDING_ID, CH_CONTACT);
+                    case 9:
+                    case 10:
+                        return new ChannelTypeUID(BINDING_ID, CH_DATETIME);
+                    default:
+                        return new ChannelTypeUID(BINDING_ID, CH_STRING);
+                }
+            }
+        } else {
+            switch (parmeter.unit) {
+                case "bar":
+                    return new ChannelTypeUID(BINDING_ID, CH_PRESSURE);
+                case "%":
+                case "Std":
+                    return new ChannelTypeUID(BINDING_ID, CH_NUMBER);
+                case "°C":
+                    return new ChannelTypeUID(BINDING_ID, CH_TEMPERATURE);
+                default:
+                    return new ChannelTypeUID(BINDING_ID, CH_STRING);
+            }
+        }
+    }
+
+    private String getItemType(Integer controlType) {
+        switch (controlType) {
+            case 1:
+            case 3:
+            case 6:
+            case 8:
+                return "Number";
+            case 5:
+                return "Contact";
+            case 9:
+            case 10:
+                return "DateTime";
+            default:
+                return "String";
+        }
+    }
+
+    private void clearSavedState() {
+        stateCache.clear();
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetUtils.java b/bundles/org.openhab.binding.wolfsmartset/src/main/java/org/openhab/binding/wolfsmartset/internal/handler/WolfSmartsetUtils.java
new file mode 100644 (file)
index 0000000..a308069
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2010-2021 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.wolfsmartset.internal.handler;
+
+import java.time.ZonedDateTime;
+import java.util.Date;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PointType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link WolfSmartsetUtils} contains utility methods used by the
+ * thing handler and the bridge handler.
+ *
+ * @author Bo Biene - Initial contribution
+ */
+@NonNullByDefault
+public final class WolfSmartsetUtils {
+
+    private static final int UNKNOWN_VALUE = -5002;
+
+    /*
+     * Checks to see if a bridge is online.
+     */
+    public static boolean isBridgeOnline(@Nullable Bridge bridge) {
+        boolean bridgeStatus = false;
+        if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
+            bridgeStatus = true;
+        }
+        return bridgeStatus;
+    }
+
+    /*
+     * Set the state to the passed value. If value is null, set the state to UNDEF
+     */
+    public static State undefOrOnOff(@Nullable Boolean value) {
+        return value == null ? UnDefType.UNDEF : (value.booleanValue() ? OnOffType.ON : OnOffType.OFF);
+    }
+
+    public static State undefOrString(@Nullable String value) {
+        return value == null ? UnDefType.UNDEF : new StringType(value);
+    }
+
+    public static State undefOrDecimal(@Nullable Number value) {
+        return (value == null || isUnknown(value)) ? UnDefType.UNDEF : new DecimalType(value.doubleValue());
+    }
+
+    public static State undefOrQuantity(@Nullable Number value, Unit<?> unit) {
+        return (value == null || isUnknown(value)) ? UnDefType.UNDEF : new QuantityType<>(value, unit);
+    }
+
+    public static State undefOrTemperature(@Nullable Number value) {
+        return (value == null || isUnknown(value)) ? UnDefType.UNDEF
+                : new QuantityType<>(value.doubleValue() / 10.0, ImperialUnits.FAHRENHEIT);
+    }
+
+    public static State undefOrPoint(@Nullable String value) {
+        return value == null ? UnDefType.UNDEF : new PointType(value);
+    }
+
+    public static State undefOrDate(@Nullable Date date, TimeZoneProvider timeZoneProvider) {
+        return date == null ? UnDefType.UNDEF
+                : new DateTimeType(ZonedDateTime.ofInstant(date.toInstant(), timeZoneProvider.getTimeZone()));
+    }
+
+    private static boolean isUnknown(Number value) {
+        return value.intValue() == UNKNOWN_VALUE;
+    }
+
+    /*
+     * Convert a QuantityType<Temperature> to the internal format used by the WolfSmartset API.
+     */
+    @SuppressWarnings("unchecked")
+    public static Integer convertQuantityTypeToWolfSmartsetTemp(Object value) {
+        if (value instanceof QuantityType<?>) {
+            QuantityType<Temperature> convertedTemp = ((QuantityType<Temperature>) value)
+                    .toUnit(ImperialUnits.FAHRENHEIT);
+            if (convertedTemp != null) {
+                return Integer.valueOf(convertedTemp.intValue() * 10);
+            }
+        }
+        throw new IllegalArgumentException("temperature is not a QuantityType");
+    }
+}
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..0c22127
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="wolfsmartset" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>WolfSmartset Binding</name>
+       <description>This is the binding for WolfSmartset smart systems.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..33fa857
--- /dev/null
@@ -0,0 +1,47 @@
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:wolfsmartset:account">
+               <parameter name="username" type="text" required="false">
+                       <label>Username</label>
+               </parameter>
+               <parameter name="password" type="text" required="false">
+                       <label>Password</label>
+                       <context>password</context>
+               </parameter>
+               <parameter name="refreshIntervalStructure" type="integer" min="2" required="false" unit="m">
+                       <label>Structure Refresh Interval</label>
+                       <description>Specifies the refresh interval in minutes</description>
+                       <default>10</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="refreshIntervalValues" type="integer" min="2" required="false" unit="s">
+                       <label>States Refresh Interval</label>
+                       <description>Specifies time in seconds to refresh states</description>
+                       <default>15</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="discoveryEnabled" type="boolean" required="false">
+                       <label>Background Discovery</label>
+                       <description>Enable/disable automatic discovery</description>
+                       <default>true</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:wolfsmartset:system">
+               <parameter name="systemId" type="text" required="true">
+                       <label>System ID</label>
+                       <description>System ID assigned to this system by WolfSmartset</description>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:wolfsmartset:unit">
+               <parameter name="unitId" type="text" required="true">
+                       <label>Unit Id</label>
+                       <description>Id assigned to this unit (e.g. rs:101)</description>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.wolfsmartset/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..c24778e
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="wolfsmartset"
+       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">
+
+       <!-- THING TYPE ACCOUNT -->
+       <bridge-type id="account">
+               <label>WolfSmartset Account</label>
+               <description>Represents an account at WolfSmartset</description>
+               <config-description-ref uri="thing-type:wolfsmartset:account"/>
+       </bridge-type>
+
+       <!-- THING TYPE SYSTEM -->
+       <bridge-type id="system">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="account"/>
+               </supported-bridge-type-refs>
+               <label>WolfSmartset System</label>
+               <description>An WolfSmartset system</description>
+               <representation-property>systemId</representation-property>
+               <config-description-ref uri="thing-type:wolfsmartset:system"/>
+       </bridge-type>
+
+       <!-- THING TYPE UNIT -->
+       <thing-type id="unit">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="system"/>
+               </supported-bridge-type-refs>
+               <label>WolfSmartset Unit</label>
+               <description>An WolfSmartset remote unit</description>
+
+               <representation-property>unitId</representation-property>
+               <config-description-ref uri="thing-type:wolfsmartset:unit"/>
+       </thing-type>
+
+       <channel-type id="number">
+               <item-type>Number</item-type>
+               <label>Number</label>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="contact">
+               <item-type>Contact</item-type>
+               <label>Contact</label>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="string">
+               <item-type>String</item-type>
+               <label>String</label>
+               <state readOnly="true" pattern="%s"/>
+       </channel-type>
+
+       <channel-type id="datetime">
+               <item-type>DateTime</item-type>
+               <label>Date Time</label>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="barometric-pressure">
+               <item-type>Number:Pressure</item-type>
+               <label>Pressure</label>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index 73f6869e16a801359776c0e691407618fefcb72c..47e550dd4978e8cef9849a6692c9036711829770 100644 (file)
     <module>org.openhab.binding.windcentrale</module>
     <module>org.openhab.binding.wlanthermo</module>
     <module>org.openhab.binding.wled</module>
+    <module>org.openhab.binding.wolfsmartset</module>
     <module>org.openhab.binding.xmltv</module>
     <module>org.openhab.binding.xmppclient</module>
     <module>org.openhab.binding.yamahareceiver</module>