]> git.basschouten.com Git - openhab-addons.git/commitdiff
[ventaair] New VentaAir binding for air humidifiers (#9979)
authorStefan Triller <t2000@users.noreply.github.com>
Sat, 10 Apr 2021 19:54:28 +0000 (21:54 +0200)
committerGitHub <noreply@github.com>
Sat, 10 Apr 2021 19:54:28 +0000 (21:54 +0200)
* [ventaair] New VentaAir binding for air humidifiers

New binding that implements support for air humidifier from Venta Air.

Closes #9922

Signed-off-by: Stefan Triller <github@stefantriller.de>
34 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.ventaair/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/README.md [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/Communicator.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirDeviceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/discovery/VentaDeviceDiscovery.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/Action.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/AllActions.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/AutomaticAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/BoostAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/ChildLockAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/FanAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/HumidityAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/PowerAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/SleepModeAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/TemperatureUnitAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/TimerAction.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/CommandMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/DeviceInfoMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Header.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Info.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Measurements.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Message.java [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/thing/channels.xml [new file with mode: 0644]
bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 428c1e03a397a013c7cd41a8475b5cc37383551f..58d6473b38a4970541cedbf0cd97cc41f46b6ab4 100644 (file)
 /bundles/org.openhab.binding.velbus/ @cedricboon
 /bundles/org.openhab.binding.velux/ @gs4711
 /bundles/org.openhab.binding.venstarthermostat/ @hww3 @digitaldan
+/bundles/org.openhab.binding.ventaair/ @t2000
 /bundles/org.openhab.binding.verisure/ @jannegpriv
 /bundles/org.openhab.binding.vigicrues/ @clinique
 /bundles/org.openhab.binding.vitotronic/ @steand
index 62161f65f2fc8361dde86a805b9616b63ddae912..34f4680e0180380a4ce6f71b8987a5436052c967 100644 (file)
       <artifactId>org.openhab.binding.venstarthermostat</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.ventaair</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.verisure</artifactId>
diff --git a/bundles/org.openhab.binding.ventaair/NOTICE b/bundles/org.openhab.binding.ventaair/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.ventaair/README.md b/bundles/org.openhab.binding.ventaair/README.md
new file mode 100644 (file)
index 0000000..6828551
--- /dev/null
@@ -0,0 +1,127 @@
+# VentaAir Binding
+
+This binding is for air humidifiers from Venta Air.
+Thankfully the vendor allows for communicating within the local network without needing any internet access or accounts.
+This is even stated in the official manual.
+Hence this binding communicates locally with the humidifier and is able to read out the current measurements and settings, as well as changing the settings.
+
+## Supported Things
+
+It currently supports the LW60-T device (`ThingType`: "lw60t") as well as a `ThingType` ("generic") for other models.
+For now the generic `ThingType` only adds the "boost" channel, but in the status reply from the device there is more which could be added in the future by someone who owns a different device.
+
+## Discovery
+
+This binding supports an automatic discovery for humidifiers that are connected to the local network and which are in the same broadcast domain.
+To do so, the binding listens to UDP port 48000 for data and creates `DiscoveryResult`s based on the received data from the device.
+This comes in handy for getting the MAC address for the device for example.
+Once the `DiscoveryResult` is added as a `Thing`, a connection to the device will be created and it will beep, showing a confirmation screen that the device "openHAB" would like to get access.
+After confirming this request, the user can link its items to receive data or control the device.
+
+## Thing Configuration
+
+There are three mandatory configuration parameters for a thing: `ipAddress`, `macAddress` and `deviceType`.
+
+| parameter  | required | description |
+|----------|------------|-------------------------------------------|
+| ipAddress | Y | The IP Address or hostname of the device. |
+| macAddress | Y | The MAC address of the device. |
+| deviceType | Y | Defines the type of device. It is an integer value and its best to use the automatic discovery to obtain it from the device. |
+| pollingTime | N | The time interval in seconds in which the data should be polled from the device, default is 10 seconds. |
+| hash | N | It is a negative integer value and it is used by the device to identify a connection to a client, like the App from the vendor for example. (*) |
+
+(*) I do not know whether there are devices which are restricted to only one client, so I added this parameter to allow the user to set the same value as his App on the phone (can be obtained via sniffing the network).
+However, the LW60-T allows for multiple connections to different clients, identified by different `hash` values at the same time without issues.
+By default the binding uses "-42", so a new ID that is not known to the device and hence it asks for confirmation, see the Discovery section.
+
+Example Thing configuration:
+
+```
+Thing ventaair:lw60t:humidifier [ ipAddress="192.168.42.69", macAddress="f8:f0:05:a6:4e:03", deviceType=4, pollingTime=10, hash=-42]
+```
+
+## Channels
+
+These are the channels that are currently supported:
+
+
+| channel  | type (RO=read-only)   | description                  |
+|----------|--------|------------------------------|
+| power          | Switch | This is the power on/off channel  |
+| fanSpeed       | Number | This is the channel to control the steps (in range 0-5 where 0 means "off") for the speed of the fan |
+| targetHumidity | Number | This channel sets the target humidity (in percent) that should be tried to reach by the device (allowed values: 30-70)  |
+| timer          | Number | This channel sets the power off timer to the set value in hours, i.e. 3 = turn off in 3 hours from now (allowed values: 0-9 where 0 means "off") |
+| sleepMode      | Switch | This channel controls the sleep mode of the device (dims the display and slows down the fan) |
+| childLock      | Switch | This is the control channel for the child lock |
+| automatic      | Switch | This is the control channel to start the automatic operation mode of the device |
+| cleanMode      | Switch (RO) | This is the channel that indicates if the device is in the cleaning mode |
+| temperature    | Number:Temperature (RO) | This channel provides the current measured temperature in Celsius or Fahrenheit as configured on the device |
+| humidity       | Number:Dimensionless (RO) | This channel provides the humidity measured by the device in percent |
+| waterLevel     | Number (RO) | This channel indicates the water level of the tank where 1 is equal to the yellow "refill tank" warning on the device/App |
+| fanRPM         | Number (RO) | This channel provides the speed of the ventilation fan |
+| timerTimePassed  | Number:Time (RO) | If a timer has been set, this channel provides the minutes since when the timer was started  |
+| operationTime  | Number:Time (RO) | This channel provides the operation time of the device in hours |
+| discReplaceTime | Number:Time (RO) | This channel provides the time in how many hours the cleaning disc should be replaced  |
+| cleaningTime   | Number:Time (RO) | This channel provides the time in how many hours the device should be cleaned  |
+| boost          | Switch | This is the control channel for the boost mode (on some devices that supports it) |
+
+## Full Example
+
+Things:
+
+```
+Thing ventaair:lw60t:humidifier [ ipAddress="192.168.42.69", macAddress="f8:f0:05:a6:4e:03", deviceType=4, pollingTime=10, hash=-42]
+```
+
+Items:
+
+```
+Group gHumidifier "Air Humidifier" <humidity>
+
+Switch Humidifier_Power "Power: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:power" }
+Number Humidifier_FanSpeed "FanSpeed: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:fanSpeed" }
+Number Humidifier_TargetHum "Target Humidity: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:targetHumidity" }
+Number Humidifier_Timer "Timer: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:timer" }
+
+Switch Humidifier_SleepMode "SleepMode:" (gHumidifier) { channel="ventaair:lw60t:humidifier:sleepMode" }
+Switch Humidifier_ChildLock "ChildLock:" (gHumidifier) { channel="ventaair:lw60t:humidifier:childLock" }
+Switch Humidifier_Automatic "Automatic:" (gHumidifier) { channel="ventaair:lw60t:humidifier:automatic" }
+
+Switch Humidifier_CleaningMode "Cleaning mode:" (gHumidifier) { channel="ventaair:lw60t:humidifier:cleanMode" }
+
+Number:Temperature Humidifier_Temperature "Temp: [%.1f %unit%]" (gHumidifier) { channel="ventaair:lw60t:humidifier:temperature" }
+Number:Temperature Humidifier_temperatureF "Temp: [%.1f °F]" (gHumidifier) { channel="ventaair:lw60t:humidifier:temperature" }
+Number Humidifier_Humidity "Humidity: [%.1f %%]" (gHumidifier) { channel="ventaair:lw60t:humidifier:humidity" }
+
+Number Humidifier_WaterLevel "WaterLevel: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:waterLevel" }
+Number Humidifier_FanRPM "Fan RPM: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:fanRPM" }
+
+Number Humidifier_TimerTime "Timer time: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:timerTimePassed" }
+Number Humidifier_OpTime "Operation Time: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:operationTime" }
+Number Humidifier_ReplaceTime "Disc replace in (h): [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:discReplaceTime" }
+Number Humidifier_CleaningTime "Cleaning in (h): [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:cleaningTime" }
+
+//for generic devices:
+Switch boost "Boost:" { channel="ventaair:generic:humidifier:boost" }
+```
+
+Sitemap:
+
+```
+Text item=Humidifier_Humidity
+Text item=Humidifier_Temperature
+Switch item=Humidifier_Power
+Switch item=Humidifier_SleepMode
+Switch item=Humidifier_FanSpeed icon="fan" mappings=[0="0", 1="1", 2="2", 3="3", 4="4", 5="5"]
+Switch item=Humidifier_TargetHum mappings=[30="30", 35="35", 40="40", 45="45", 50="50", 55="55", 60="60", 65="65", 70="70"]
+Switch item=Humidifier_Timer mappings=[0="0", 1="1", 3="3", 5="5", 7="7", 9="9"]
+Text item=Humidifier_WaterLevel
+Text item=Humidifier_FanRPM
+Text item=Humidifier_OpTime
+Text item=Humidifier_ReplaceTime
+Text item=Humidifier_CleaningTime
+Text item=Humidifier_TimerTime
+Switch item=Humidifier_CleaningModeActive
+Switch item=Humidifier_ChildLock
+Switch item=Humidifier_Automatic
+```
diff --git a/bundles/org.openhab.binding.ventaair/pom.xml b/bundles/org.openhab.binding.ventaair/pom.xml
new file mode 100644 (file)
index 0000000..58e74bf
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  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.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.ventaair</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: VentaAir Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.ventaair/src/main/feature/feature.xml b/bundles/org.openhab.binding.ventaair/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..41477b7
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.ventaair-${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-ventaair" description="VentaAir Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ventaair/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/Communicator.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/Communicator.java
new file mode 100644 (file)
index 0000000..f358fd4
--- /dev/null
@@ -0,0 +1,179 @@
+/**
+ * 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.ventaair.internal;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ventaair.internal.VentaThingHandler.StateUpdatedCallback;
+import org.openhab.binding.ventaair.internal.message.action.Action;
+import org.openhab.binding.ventaair.internal.message.dto.CommandMessage;
+import org.openhab.binding.ventaair.internal.message.dto.DeviceInfoMessage;
+import org.openhab.binding.ventaair.internal.message.dto.Header;
+import org.openhab.binding.ventaair.internal.message.dto.Message;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link Communicator} is responsible for sending/receiving commands to/from the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class Communicator {
+    private static final Duration COMMUNICATION_TIMEOUT = Duration.ofSeconds(5);
+
+    private final Logger logger = LoggerFactory.getLogger(Communicator.class);
+
+    private @Nullable String ipAddress;
+    private Header header;
+    private int pollingTimeInSeconds;
+    private StateUpdatedCallback callback;
+
+    private Gson gson = new Gson();
+
+    private @Nullable ScheduledFuture<?> pollingJob;
+
+    public Communicator(@Nullable String ipAddress, Header header, @Nullable BigDecimal pollingTime,
+            StateUpdatedCallback callback) {
+        this.ipAddress = ipAddress;
+        this.header = header;
+        if (pollingTime != null) {
+            this.pollingTimeInSeconds = pollingTime.intValue();
+        } else {
+            this.pollingTimeInSeconds = 60;
+        }
+        this.callback = callback;
+    }
+
+    /**
+     * Sends a request message to the device, reads the reply and informs the listener about the current device data
+     */
+    public void pollDataFromDevice() {
+        String messageJson = gson.toJson(new Message(header));
+
+        try (Socket socket = new Socket(ipAddress, VentaAirBindingConstants.PORT)) {
+            socket.setSoTimeout((int) COMMUNICATION_TIMEOUT.toMillis());
+            InputStream input = socket.getInputStream();
+            OutputStream output = socket.getOutputStream();
+
+            byte[] dataToSend = buildMessageBytes(messageJson, "GET", "Complete");
+            // we write these lines to the log in order to help users with new/other venta devices, so they only need to
+            // enable debug logging
+            logger.debug("Sending request data message (String):\n{}", new String(dataToSend));
+            logger.debug("Sending request data message (bytes): [{}]", HexUtils.bytesToHex(dataToSend, ", "));
+            output.write(dataToSend);
+
+            BufferedReader br = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+            String reply = "";
+            while ((reply = br.readLine()) != null) {
+                if (reply.startsWith("{")) {
+                    // remove padding byte(s) after JSON data
+                    String data = String.valueOf(reply.toCharArray(), 0, reply.length() - 1);
+                    // we write this line to the log in order to help users with new/other venta devices, so they only
+                    // need to enable debug logging
+                    logger.debug("Got Data from device: {}", data);
+
+                    DeviceInfoMessage deviceInfoMessage = gson.fromJson(data, DeviceInfoMessage.class);
+                    if (deviceInfoMessage != null) {
+                        callback.stateUpdated(deviceInfoMessage);
+                    }
+                }
+            }
+            br.close();
+            socket.close();
+        } catch (IOException e) {
+            callback.communicationProblem();
+        }
+    }
+
+    private byte[] buildMessageBytes(String message, String method, String endpoint) throws IOException {
+        ByteArrayOutputStream getInfoOutputStream = new ByteArrayOutputStream();
+        getInfoOutputStream
+                .write(createMessageHeader(method, endpoint, message.length()).getBytes(StandardCharsets.UTF_8));
+        getInfoOutputStream.write(message.getBytes(StandardCharsets.UTF_8));
+        getInfoOutputStream.write(new byte[] { 0x1c, 0x00 });
+        return getInfoOutputStream.toByteArray();
+    }
+
+    private String createMessageHeader(String method, String endPoint, int contentLength) {
+        return method + " /" + endPoint + "\n" + "Content-Length: " + contentLength + "\n" + "\n";
+    }
+
+    /**
+     * Sends and {@link Action} to the device to set for example the FanSpeed or TargetHumidity
+     *
+     * @param action - The action to be send to the device
+     */
+    public void sendActionToDevice(Action action) throws IOException {
+        CommandMessage message = new CommandMessage(action, header);
+
+        String messageJson = gson.toJson(message);
+
+        try (Socket socket = new Socket(ipAddress, VentaAirBindingConstants.PORT)) {
+            OutputStream output = socket.getOutputStream();
+
+            byte[] dataToSend = buildMessageBytes(messageJson, "POST", "Action");
+
+            // we write these lines to the log in order to help users with new/other venta devices, so they only need to
+            // enable debug logging
+            logger.debug("sending: {}", new String(dataToSend));
+            logger.debug("sendingArray: {}", Arrays.toString(dataToSend));
+
+            output.write(dataToSend);
+            socket.close();
+        }
+    }
+
+    /**
+     * Starts the polling job to fetch the current device data
+     *
+     * @param scheduler - The scheduler of the {@link ThingHandler}
+     */
+    public void startPollDataFromDevice(ScheduledExecutorService scheduler) {
+        stopPollDataFromDevice();
+        pollingJob = scheduler.scheduleWithFixedDelay(this::pollDataFromDevice, 2, pollingTimeInSeconds,
+                TimeUnit.SECONDS);
+    }
+
+    /**
+     * Stops the polling for device data
+     */
+    public void stopPollDataFromDevice() {
+        ScheduledFuture<?> localPollingJob = pollingJob;
+        if (localPollingJob != null && !localPollingJob.isCancelled()) {
+            localPollingJob.cancel(true);
+        }
+        logger.debug("Setting polling job to null");
+        pollingJob = null;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirBindingConstants.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirBindingConstants.java
new file mode 100644 (file)
index 0000000..4e2e797
--- /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.ventaair.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link VentaAirBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Stefan Triller - Initial contribution
+ */
+@NonNullByDefault
+public class VentaAirBindingConstants {
+
+    private static final String BINDING_ID = "ventaair";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_LW60T = new ThingTypeUID(BINDING_ID, "lw60t");
+    public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, "generic");
+
+    // List of all Channel ids
+    public static final String CHANNEL_POWER = "power";
+    public static final String CHANNEL_FAN_SPEED = "fanSpeed";
+    public static final String CHANNEL_TARGET_HUMIDITY = "targetHumidity";
+    public static final String CHANNEL_TIMER = "timer";
+    public static final String CHANNEL_SLEEP_MODE = "sleepMode";
+    public static final String CHANNEL_BOOST = "boost";
+    public static final String CHANNEL_CHILD_LOCK = "childLock";
+    public static final String CHANNEL_AUTOMATIC = "automatic";
+    public static final String CHANNEL_TEMPERATURE = "temperature";
+    public static final String CHANNEL_HUMIDITY = "humidity";
+    public static final String CHANNEL_WATERLEVEL = "waterLevel";
+    public static final String CHANNEL_FAN_RPM = "fanRPM";
+    public static final String CHANNEL_CLEAN_MODE = "cleanMode";
+    public static final String CHANNEL_OPERATION_TIME = "operationTime";
+    public static final String CHANNEL_DISC_REPLACE_TIME = "discReplaceTime";
+    public static final String CHANNEL_CLEANING_TIME = "cleaningTime";
+    public static final String CHANNEL_TIMER_TIME_PASSED = "timerTimePassed";
+
+    public static final int PORT = 48000;
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirDeviceConfiguration.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirDeviceConfiguration.java
new file mode 100644 (file)
index 0000000..949ffa0
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * 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.ventaair.internal;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link VentaAirDeviceConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Stefan Triller - Initial contribution
+ */
+@NonNullByDefault
+public class VentaAirDeviceConfiguration {
+    public String ipAddress = "";
+    public String macAddress = "";
+    public BigDecimal deviceType = BigDecimal.ZERO;
+    // we all know that 42 is the answer to everything, so let's pick this one ;)
+    public BigDecimal hash = new BigDecimal("-42");
+    public BigDecimal pollingTime = BigDecimal.TEN;
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirHandlerFactory.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirHandlerFactory.java
new file mode 100644 (file)
index 0000000..3d0df13
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * 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.ventaair.internal;
+
+import static org.openhab.binding.ventaair.internal.VentaAirBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link VentaAirHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Stefan Triller - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.ventaair", service = ThingHandlerFactory.class)
+public class VentaAirHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LW60T, THING_TYPE_GENERIC);
+
+    @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 (THING_TYPE_LW60T.equals(thingTypeUID) || THING_TYPE_GENERIC.equals(thingTypeUID)) {
+            return new VentaThingHandler(thing);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaThingHandler.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaThingHandler.java
new file mode 100644 (file)
index 0000000..ede6692
--- /dev/null
@@ -0,0 +1,333 @@
+/**
+ * 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.ventaair.internal;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Temperature;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ventaair.internal.message.action.Action;
+import org.openhab.binding.ventaair.internal.message.action.AllActions;
+import org.openhab.binding.ventaair.internal.message.action.AutomaticAction;
+import org.openhab.binding.ventaair.internal.message.action.BoostAction;
+import org.openhab.binding.ventaair.internal.message.action.ChildLockAction;
+import org.openhab.binding.ventaair.internal.message.action.FanAction;
+import org.openhab.binding.ventaair.internal.message.action.HumidityAction;
+import org.openhab.binding.ventaair.internal.message.action.PowerAction;
+import org.openhab.binding.ventaair.internal.message.action.SleepModeAction;
+import org.openhab.binding.ventaair.internal.message.action.TimerAction;
+import org.openhab.binding.ventaair.internal.message.dto.DeviceInfoMessage;
+import org.openhab.binding.ventaair.internal.message.dto.Header;
+import org.openhab.binding.ventaair.internal.message.dto.Info;
+import org.openhab.binding.ventaair.internal.message.dto.Measurements;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.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.BaseThingHandler;
+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 VentaThingHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Stefan Triller - Initial contribution
+ */
+@NonNullByDefault
+public class VentaThingHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(VentaThingHandler.class);
+
+    private VentaAirDeviceConfiguration config = new VentaAirDeviceConfiguration();
+
+    private @Nullable Communicator communicator;
+
+    private Map<String, State> channelValueCache = new HashMap<>();
+
+    public VentaThingHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("Handle command={} for channel={} with channelID={}", command, channelUID, channelUID.getId());
+        if (command instanceof RefreshType) {
+            refreshChannelFromCache(channelUID);
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case VentaAirBindingConstants.CHANNEL_POWER:
+                if (command instanceof OnOffType) {
+                    dispatchActionToDevice(new PowerAction(command == OnOffType.ON));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_FAN_SPEED:
+                if (command instanceof DecimalType) {
+                    int fanStage = ((DecimalType) command).intValue();
+                    dispatchActionToDevice(new FanAction(fanStage));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_TARGET_HUMIDITY:
+                if (command instanceof DecimalType) {
+                    int targetHumidity = ((DecimalType) command).intValue();
+                    dispatchActionToDevice(new HumidityAction(targetHumidity));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_TIMER:
+                if (command instanceof DecimalType) {
+                    int timer = ((DecimalType) command).intValue();
+                    dispatchActionToDevice(new TimerAction(timer));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_SLEEP_MODE:
+                if (command instanceof OnOffType) {
+                    dispatchActionToDevice(new SleepModeAction(command == OnOffType.ON));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_BOOST:
+                if (command instanceof OnOffType) {
+                    dispatchActionToDevice(new BoostAction(command == OnOffType.ON));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_CHILD_LOCK:
+                if (command instanceof OnOffType) {
+                    dispatchActionToDevice(new ChildLockAction(command == OnOffType.ON));
+                }
+                break;
+            case VentaAirBindingConstants.CHANNEL_AUTOMATIC:
+                if (command instanceof OnOffType) {
+                    dispatchActionToDevice(new AutomaticAction(command == OnOffType.ON));
+                }
+                break;
+
+            default:
+                break;
+        }
+    }
+
+    @Override
+    public void initialize() {
+        config = getConfigAs(VentaAirDeviceConfiguration.class);
+
+        updateStatus(ThingStatus.UNKNOWN);
+
+        String configErrorMessage;
+        if ((configErrorMessage = validateConfig()) != null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErrorMessage);
+            return;
+        }
+
+        Header header = new Header(config.macAddress, config.deviceType.intValue(), config.hash.toString(), "openHAB");
+
+        communicator = new Communicator(config.ipAddress, header, config.pollingTime, new StateUpdatedCallback());
+        communicator.startPollDataFromDevice(scheduler);
+    }
+
+    private @Nullable String validateConfig() {
+        if (config.ipAddress.isEmpty()) {
+            return "IP address not set";
+        }
+        if (config.macAddress.isEmpty()) {
+            return "Mac Address not set, use discovery to find the correct one";
+        }
+        if (config.deviceType == BigDecimal.ZERO) {
+            return "Device Type not set, use discovery to find the correct one";
+        }
+        if (config.pollingTime.compareTo(BigDecimal.ZERO) <= 0) {
+            return "Polling time has to be larger than 0 seconds";
+        }
+
+        return null;
+    }
+
+    private void dispatchActionToDevice(Action action) {
+        Communicator localCommunicator = communicator;
+        if (localCommunicator != null) {
+            logger.debug("Dispatching Action={} to the device", action.getClass());
+            try {
+                localCommunicator.sendActionToDevice(action);
+            } catch (IOException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+                return;
+            }
+            localCommunicator.pollDataFromDevice();
+        } else {
+            logger.error("Should send action={} to device but communicator is not available.", action.getClass());
+        }
+    }
+
+    private void refreshChannelFromCache(ChannelUID channelUID) {
+        State cachedState = channelValueCache.get(channelUID.getId());
+        if (cachedState != null) {
+            updateState(channelUID, cachedState);
+        }
+    }
+
+    private void updateProperties(Info info) {
+        Thing thing = getThing();
+        thing.setProperty("SWDisplay", info.getSwDisplay());
+        thing.setProperty("SWPower", info.getSwPower());
+        thing.setProperty("SWTouch", info.getSwTouch());
+        thing.setProperty("SWWIFI", info.getSwWIFI());
+    }
+
+    @Override
+    public void dispose() {
+        Communicator localCommunicator = communicator;
+        if (localCommunicator != null) {
+            localCommunicator.stopPollDataFromDevice();
+        }
+        communicator = null;
+    }
+
+    class StateUpdatedCallback {
+        /**
+         * Method to pass the data received from the device to the handler
+         *
+         * @param message - message containing the parsed data from the device
+         */
+        public void stateUpdated(DeviceInfoMessage message) {
+            if (messageIsEmpty(message)) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
+                        "Please allow openHAB to access your device");
+                return;
+            }
+
+            AllActions actions = message.getCurrentActions();
+
+            Unit<Temperature> temperatureUnit = SIUnits.CELSIUS;
+
+            if (actions != null) {
+                OnOffType powerState = OnOffType.from(actions.isPower());
+                updateState(VentaAirBindingConstants.CHANNEL_POWER, powerState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_POWER, powerState);
+
+                DecimalType fanspeedState = new DecimalType(actions.getFanSpeed());
+                updateState(VentaAirBindingConstants.CHANNEL_FAN_SPEED, fanspeedState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_FAN_SPEED, fanspeedState);
+
+                DecimalType targetHumState = new DecimalType(actions.getTargetHum());
+                updateState(VentaAirBindingConstants.CHANNEL_TARGET_HUMIDITY, targetHumState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_TARGET_HUMIDITY, targetHumState);
+
+                DecimalType timerState = new DecimalType(actions.getTimer());
+                updateState(VentaAirBindingConstants.CHANNEL_TIMER, timerState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_TIMER, timerState);
+
+                OnOffType sleepModeState = OnOffType.from(actions.isSleepMode());
+                updateState(VentaAirBindingConstants.CHANNEL_SLEEP_MODE, sleepModeState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_SLEEP_MODE, sleepModeState);
+
+                OnOffType boostState = OnOffType.from(actions.isBoost());
+                updateState(VentaAirBindingConstants.CHANNEL_BOOST, boostState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_BOOST, boostState);
+
+                OnOffType childLockState = OnOffType.from(actions.isChildLock());
+                updateState(VentaAirBindingConstants.CHANNEL_CHILD_LOCK, childLockState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_CHILD_LOCK, childLockState);
+
+                OnOffType automaticState = OnOffType.from(actions.isAutomatic());
+                updateState(VentaAirBindingConstants.CHANNEL_AUTOMATIC, automaticState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_AUTOMATIC, automaticState);
+
+                temperatureUnit = actions.getTempUnit() == 0 ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
+            }
+
+            Measurements measurements = message.getMeasurements();
+
+            if (measurements != null) {
+                QuantityType<Temperature> temperatureState = new QuantityType<>(measurements.getTemperature(),
+                        temperatureUnit);
+                updateState(VentaAirBindingConstants.CHANNEL_TEMPERATURE, temperatureState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_TEMPERATURE, temperatureState);
+
+                QuantityType<Dimensionless> humidityState = new QuantityType<>(measurements.getHumidity(),
+                        Units.PERCENT);
+                updateState(VentaAirBindingConstants.CHANNEL_HUMIDITY, humidityState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_HUMIDITY, humidityState);
+
+                DecimalType waterLevelState = new DecimalType(measurements.getWaterLevel());
+                updateState(VentaAirBindingConstants.CHANNEL_WATERLEVEL, waterLevelState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_WATERLEVEL, waterLevelState);
+
+                DecimalType fanRPMstate = new DecimalType(measurements.getFanRpm());
+                updateState(VentaAirBindingConstants.CHANNEL_FAN_RPM, fanRPMstate);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_FAN_RPM, fanRPMstate);
+            }
+
+            Info info = message.getInfo();
+            if (info != null) {
+                int opHours = info.getOperationT() * 5 / 60;
+                int discReplaceHours = info.getDiscIonT() * 5 / 60;
+                int cleaningHours = info.getCleaningT() * 5 / 60;
+
+                QuantityType<Time> opHoursState = new QuantityType<Time>(opHours, Units.HOUR);
+                updateState(VentaAirBindingConstants.CHANNEL_OPERATION_TIME, opHoursState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_OPERATION_TIME, opHoursState);
+
+                QuantityType<Time> discReplaceHoursState = new QuantityType<Time>(2200 - discReplaceHours, Units.HOUR);
+                updateState(VentaAirBindingConstants.CHANNEL_DISC_REPLACE_TIME, discReplaceHoursState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_DISC_REPLACE_TIME, discReplaceHoursState);
+
+                QuantityType<Time> cleaningHoursState = new QuantityType<Time>(4400 - cleaningHours, Units.HOUR);
+                updateState(VentaAirBindingConstants.CHANNEL_CLEANING_TIME, cleaningHoursState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_CLEANING_TIME, cleaningHoursState);
+
+                OnOffType cleanModeState = info.isCleanMode() ? OnOffType.ON : OnOffType.OFF;
+                updateState(VentaAirBindingConstants.CHANNEL_CLEAN_MODE, cleanModeState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_CLEAN_MODE, cleanModeState);
+
+                QuantityType<Time> timerTimePassedState = new QuantityType<Time>(info.getTimerT(), Units.MINUTE);
+                updateState(VentaAirBindingConstants.CHANNEL_TIMER_TIME_PASSED, timerTimePassedState);
+                channelValueCache.put(VentaAirBindingConstants.CHANNEL_TIMER_TIME_PASSED, timerTimePassedState);
+
+                updateProperties(info);
+            }
+
+            updateStatus(ThingStatus.ONLINE);
+        }
+
+        private boolean messageIsEmpty(DeviceInfoMessage message) {
+            if (message.getCurrentActions() == null && message.getInfo() == null && message.getMeasurements() == null) {
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Method to inform the handler about a communication issue
+         */
+        public void communicationProblem() {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/discovery/VentaDeviceDiscovery.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/discovery/VentaDeviceDiscovery.java
new file mode 100644 (file)
index 0000000..b898822
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * 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.ventaair.internal.discovery;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ventaair.internal.VentaAirBindingConstants;
+import org.openhab.binding.ventaair.internal.message.dto.Header;
+import org.openhab.binding.ventaair.internal.message.dto.Message;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.util.HexUtils;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Discovers Venta Air humidifier and cleaner devices by listening for UDP messages
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, configurationPid = "discovery.ventaair")
+public class VentaDeviceDiscovery extends AbstractDiscoveryService {
+    private static final String REPRESENTATION_PROPERTY = "macAddress";
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
+            .singleton(VentaAirBindingConstants.THING_TYPE_LW60T);
+    // defined as int, because AbstractDiscoveryService wants and int and not long as provided by Duration.getSeconds()
+    private static final int MANUAL_DISCOVERY_TIME = 30;
+    private static final Duration TIME_BETWEEN_SCANS = Duration.ofSeconds(30);
+
+    private final Logger logger = LoggerFactory.getLogger(VentaDeviceDiscovery.class);
+
+    private @Nullable ScheduledFuture<?> scanJob = null;
+
+    public VentaDeviceDiscovery() {
+        super(SUPPORTED_THING_TYPES_UIDS, MANUAL_DISCOVERY_TIME, true);
+    }
+
+    @Override
+    protected void startScan() {
+        findDevices();
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        super.startBackgroundDiscovery();
+
+        ScheduledFuture<?> localScanJob = scanJob;
+        if (localScanJob != null) {
+            localScanJob.cancel(true);
+        }
+
+        scanJob = scheduler.scheduleWithFixedDelay(this::findDevices, 5, TIME_BETWEEN_SCANS.getSeconds(),
+                TimeUnit.SECONDS);
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        super.stopBackgroundDiscovery();
+
+        ScheduledFuture<?> localScanJob = scanJob;
+        if (localScanJob != null) {
+            localScanJob.cancel(true);
+        }
+        scanJob = null;
+    }
+
+    private void findDevices() {
+        byte[] buf = new byte[512];
+        try (DatagramSocket socket = new DatagramSocket(VentaAirBindingConstants.PORT)) {
+            DatagramPacket packet = new DatagramPacket(buf, buf.length);
+            socket.receive(packet);
+
+            Message m = parseDiscoveryPaket(packet.getData());
+            if (m == null) {
+                logger.debug("Received broken discovery packet data={}", HexUtils.bytesToHex(packet.getData(), ", "));
+                return;
+            }
+
+            logger.debug("Found device with: IP={} Mac={} and device type={}", m.getHeader().getIpAdress(),
+                    m.getHeader().getMacAdress(), m.getHeader().getDeviceType());
+
+            ThingTypeUID thingTypeUID;
+            switch (m.getHeader().getDeviceType()) {
+                case 4:
+                    thingTypeUID = VentaAirBindingConstants.THING_TYPE_LW60T;
+                    break;
+                default:
+                    thingTypeUID = VentaAirBindingConstants.THING_TYPE_GENERIC;
+                    break;
+            }
+            createDiscoveryResult(thingTypeUID, m.getHeader());
+        } catch (SocketException e) {
+            logger.warn("Could not open port {} to scan for Venta devices in the network.",
+                    VentaAirBindingConstants.PORT);
+        } catch (IOException e) {
+            // swallow, since we already log the broken packet above
+        }
+    }
+
+    private @Nullable Message parseDiscoveryPaket(byte[] packet) {
+        Gson gson = new Gson();
+        Message msg = null;
+
+        String packetAsString = new String(packet, StandardCharsets.UTF_8);
+
+        String[] lines = packetAsString.split("\n");
+        if (lines.length >= 3) {
+            String input = lines[2];
+            int end = input.lastIndexOf("}"); // strip padding bytes added by the device
+            if (end > 0) {
+                String rawJSONstring = input.substring(0, end + 1);
+                try {
+                    msg = gson.fromJson(rawJSONstring, Message.class);
+                } catch (JsonSyntaxException e) {
+                    logger.debug("Received invalid JSON data={}", rawJSONstring, e);
+                }
+            }
+        }
+        return msg;
+    }
+
+    private void createDiscoveryResult(ThingTypeUID thingTypeUID, Header messageHeader) {
+        String ipAddress = messageHeader.getIpAdress();
+        String macAddress = messageHeader.getMacAdress();
+        int deviceType = messageHeader.getDeviceType();
+
+        ThingUID uid = new ThingUID(thingTypeUID, ipAddress.replace(".", "_"));
+        HashMap<String, Object> properties = new HashMap<>();
+        properties.put("ipAddress", ipAddress);
+        properties.put(REPRESENTATION_PROPERTY, macAddress);
+        properties.put("deviceType", deviceType);
+
+        String typeLabel = thingTypeUID.getId().toUpperCase();
+
+        DiscoveryResult result = DiscoveryResultBuilder.create(uid).withRepresentationProperty(REPRESENTATION_PROPERTY)
+                .withProperties(properties).withLabel(typeLabel + " (IP=" + ipAddress + ")").build();
+
+        this.thingDiscovered(result);
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/Action.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/Action.java
new file mode 100644 (file)
index 0000000..9f69b61
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * 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.ventaair.internal.message.action;
+
+/**
+ * Marker interface for Actions that can be send to the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public interface Action {
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/AllActions.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/AllActions.java
new file mode 100644 (file)
index 0000000..ee0d850
--- /dev/null
@@ -0,0 +1,102 @@
+/**
+ * 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.ventaair.internal.message.action;
+
+/**
+ * Actions send by the device, containing information about the current device settings
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class AllActions implements Action {
+    private boolean Power = true;
+    private int FanSpeed;
+    private int TargetHum = 65;
+    private int Timer;
+    private boolean Boost;
+    private boolean SleepMode;
+    private boolean ChildLock;
+    private boolean Automatic;
+    private int SysLanguage; // 3?
+    private int CleanLanguage; // 0?
+    private int TempUnit; // 0=Celsius, 1=Fahrenheit?
+    private int DisplayLeft;
+    private int DisplayRight;
+    private int Reset;
+    private int ConINet;
+    private boolean DelUser; // default false
+
+    public boolean isPower() {
+        return Power;
+    }
+
+    public int getFanSpeed() {
+        return FanSpeed;
+    }
+
+    public int getTargetHum() {
+        return TargetHum;
+    }
+
+    public int getTimer() {
+        return Timer;
+    }
+
+    public boolean isBoost() {
+        return Boost;
+    }
+
+    public boolean isSleepMode() {
+        return SleepMode;
+    }
+
+    public boolean isChildLock() {
+        return ChildLock;
+    }
+
+    public boolean isAutomatic() {
+        return Automatic;
+    }
+
+    public int getSysLanguage() {
+        return SysLanguage;
+    }
+
+    public int getCleanLanguage() {
+        return CleanLanguage;
+    }
+
+    public int getTempUnit() {
+        return TempUnit;
+    }
+
+    public int getDisplayLeft() {
+        return DisplayLeft;
+    }
+
+    public int getDisplayRight() {
+        return DisplayRight;
+    }
+
+    public int getReset() {
+        return Reset;
+    }
+
+    public int getConINet() {
+        return ConINet;
+    }
+
+    public boolean isDelUser() {
+        return DelUser;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/AutomaticAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/AutomaticAction.java
new file mode 100644 (file)
index 0000000..c2d0f4e
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to enable the automatic mode of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class AutomaticAction implements Action {
+    @SuppressWarnings("unused")
+    private boolean Automatic;
+
+    public AutomaticAction(boolean automatic) {
+        this.Automatic = automatic;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/BoostAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/BoostAction.java
new file mode 100644 (file)
index 0000000..bf31935
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to enable the boost mode of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class BoostAction implements Action {
+    @SuppressWarnings("unused")
+    private boolean Boost;
+
+    public BoostAction(boolean boost) {
+        this.Boost = boost;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/ChildLockAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/ChildLockAction.java
new file mode 100644 (file)
index 0000000..c0c1b51
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to enable the child lock mode of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class ChildLockAction implements Action {
+    @SuppressWarnings("unused")
+    private boolean ChildLock;
+
+    public ChildLockAction(boolean childLockOn) {
+        this.ChildLock = childLockOn;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/FanAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/FanAction.java
new file mode 100644 (file)
index 0000000..d285dd0
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to control the fan speed of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class FanAction implements Action {
+    @SuppressWarnings("unused")
+    private int FanSpeed;
+
+    public FanAction(int fanSpeed) {
+        this.FanSpeed = fanSpeed;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/HumidityAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/HumidityAction.java
new file mode 100644 (file)
index 0000000..84e6376
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to set the target humidity of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class HumidityAction implements Action {
+    @SuppressWarnings("unused")
+    private int TargetHum;
+
+    public HumidityAction(int targetHumidity) {
+        this.TargetHum = targetHumidity;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/PowerAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/PowerAction.java
new file mode 100644 (file)
index 0000000..5fe5f40
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to power on/off the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class PowerAction implements Action {
+    @SuppressWarnings("unused")
+    private boolean Power;
+
+    public PowerAction(boolean on) {
+        this.Power = on;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/SleepModeAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/SleepModeAction.java
new file mode 100644 (file)
index 0000000..541418e
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to enable the sleep mode of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class SleepModeAction implements Action {
+    @SuppressWarnings("unused")
+    private boolean SleepMode;
+
+    public SleepModeAction(boolean sleepModeOn) {
+        this.SleepMode = sleepModeOn;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/TemperatureUnitAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/TemperatureUnitAction.java
new file mode 100644 (file)
index 0000000..bbe5212
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * 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.ventaair.internal.message.action;
+
+/**
+ * Action to change the temperature unit of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class TemperatureUnitAction implements Action {
+    @SuppressWarnings("unused")
+    // 0=Celsius, 1=Fahrenheit
+    private int TempUnit;
+
+    public TemperatureUnitAction(int temperatureUnit) {
+        this.TempUnit = temperatureUnit;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/TimerAction.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/action/TimerAction.java
new file mode 100644 (file)
index 0000000..3ed63a4
--- /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.ventaair.internal.message.action;
+
+/**
+ * Action to set an off timer on the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class TimerAction implements Action {
+    @SuppressWarnings("unused")
+    private int Timer;
+
+    public TimerAction(int timer) {
+        this.Timer = timer;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/CommandMessage.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/CommandMessage.java
new file mode 100644 (file)
index 0000000..f4a1aea
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * 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.ventaair.internal.message.dto;
+
+import org.openhab.binding.ventaair.internal.message.action.Action;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Message containing a command to be send to the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class CommandMessage extends Message {
+    @SerializedName(value = "Action")
+    private Action action;
+
+    public CommandMessage(Action action, Header header) {
+        super(header);
+        this.action = action;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/DeviceInfoMessage.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/DeviceInfoMessage.java
new file mode 100644 (file)
index 0000000..e99daa4
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * 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.ventaair.internal.message.dto;
+
+import org.openhab.binding.ventaair.internal.message.action.AllActions;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Message send by the device, containing information about its current state
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class DeviceInfoMessage extends Message {
+
+    public DeviceInfoMessage(Header header) {
+        super(header);
+    }
+
+    @SerializedName(value = "Action")
+    private AllActions currentActions;
+
+    @SerializedName(value = "Info")
+    private Info info;
+
+    @SerializedName(value = "Measure")
+    private Measurements measurements;
+
+    public AllActions getCurrentActions() {
+        return currentActions;
+    }
+
+    public Info getInfo() {
+        return info;
+    }
+
+    public Measurements getMeasurements() {
+        return measurements;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Header.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Header.java
new file mode 100644 (file)
index 0000000..8914c8a
--- /dev/null
@@ -0,0 +1,86 @@
+/**
+ * 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.ventaair.internal.message.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Header which is part of a message to/from a device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class Header {
+
+    @SerializedName(value = "MacAdress")
+    private String macAdress;
+
+    @SerializedName(value = "IpAdress")
+    private String ipAdress;
+
+    @SerializedName(value = "DeviceType")
+    private int deviceType;
+
+    @SerializedName(value = "Hash")
+    private String hash;
+
+    @SerializedName(value = "DeviceName")
+    private String deviceName;
+
+    public Header(String mac, int devType, String hash, String devName) {
+        this.macAdress = mac;
+        this.deviceType = devType;
+        this.hash = hash;
+        this.deviceName = devName;
+    }
+
+    public String getMacAdress() {
+        return macAdress;
+    }
+
+    public void setMacAdress(String macAdress) {
+        this.macAdress = macAdress;
+    }
+
+    public String getIpAdress() {
+        return ipAdress;
+    }
+
+    public void setIpAdress(String ipAdress) {
+        this.ipAdress = ipAdress;
+    }
+
+    public int getDeviceType() {
+        return deviceType;
+    }
+
+    public void setDeviceType(int deviceType) {
+        this.deviceType = deviceType;
+    }
+
+    public String getHash() {
+        return hash;
+    }
+
+    public void setHash(String hash) {
+        this.hash = hash;
+    }
+
+    public String getDeviceName() {
+        return deviceName;
+    }
+
+    public void setDeviceName(String deviceName) {
+        this.deviceName = deviceName;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Info.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Info.java
new file mode 100644 (file)
index 0000000..b487f35
--- /dev/null
@@ -0,0 +1,128 @@
+/**
+ * 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.ventaair.internal.message.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Part of the {@link DeviceInfoMessage} containing details about the device state
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class Info {
+    @SerializedName(value = "SWDisplay")
+    private String swDisplay;
+
+    @SerializedName(value = "SWPower")
+    private String swPower;
+
+    @SerializedName(value = "SWTouch")
+    private String swTouch;
+
+    @SerializedName(value = "SWWIFI")
+    private String swWIFI;
+
+    @SerializedName(value = "CleanMode")
+    private boolean cleanMode; // default false?
+
+    @SerializedName(value = "RelState")
+    private boolean[] relState; // [true,true,false,false]
+
+    @SerializedName(value = "TimerT")
+    private int timerT;
+
+    @SerializedName(value = "OperationT")
+    private int operationT;
+
+    @SerializedName(value = "DiscIonT")
+    private int discIonT;
+
+    @SerializedName(value = "CleaningT")
+    private int cleaningT;
+
+    @SerializedName(value = "FilterT")
+    private int filterT;
+
+    @SerializedName(value = "UVCOnT")
+    private int uvCOnT;
+
+    @SerializedName(value = "UVCOffT")
+    private int uvCOffT;
+
+    @SerializedName(value = "CleaningR")
+    private int cleaningR;
+
+    @SerializedName(value = "Warnings")
+    private int warnings;
+
+    public String getSwDisplay() {
+        return swDisplay;
+    }
+
+    public String getSwPower() {
+        return swPower;
+    }
+
+    public String getSwTouch() {
+        return swTouch;
+    }
+
+    public String getSwWIFI() {
+        return swWIFI;
+    }
+
+    public boolean isCleanMode() {
+        return cleanMode;
+    }
+
+    public boolean[] getRelState() {
+        return relState;
+    }
+
+    public int getTimerT() {
+        return timerT;
+    }
+
+    public int getOperationT() {
+        return operationT;
+    }
+
+    public int getDiscIonT() {
+        return discIonT;
+    }
+
+    public int getCleaningT() {
+        return cleaningT;
+    }
+
+    public int getFilterT() {
+        return filterT;
+    }
+
+    public int getUvCOnT() {
+        return uvCOnT;
+    }
+
+    public int getUvCOffT() {
+        return uvCOffT;
+    }
+
+    public int getCleaningR() {
+        return cleaningR;
+    }
+
+    public int getWarnings() {
+        return warnings;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Measurements.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Measurements.java
new file mode 100644 (file)
index 0000000..611ad86
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * 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.ventaair.internal.message.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Part of the {@link DeviceInfoMessage} containing the measurements of the device
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class Measurements {
+    @SerializedName(value = "Temperature")
+    private double temperature;
+
+    @SerializedName(value = "Humidity")
+    private double humidity;
+
+    @SerializedName(value = "Dust")
+    private int dust;
+
+    @SerializedName(value = "WaterLevel")
+    private int waterLevel;
+
+    @SerializedName(value = "FanRpm")
+    private int fanRpm;
+
+    public double getTemperature() {
+        return temperature;
+    }
+
+    public double getHumidity() {
+        return humidity;
+    }
+
+    public int getDust() {
+        return dust;
+    }
+
+    public int getWaterLevel() {
+        return waterLevel;
+    }
+
+    public int getFanRpm() {
+        return fanRpm;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Message.java b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/message/dto/Message.java
new file mode 100644 (file)
index 0000000..a2817d8
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * 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.ventaair.internal.message.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Base class for messages
+ *
+ * @author Stefan Triller - Initial contribution
+ *
+ */
+public class Message {
+
+    @SerializedName(value = "Header")
+    protected Header header;
+
+    public Message(Header header) {
+        this.header = header;
+    }
+
+    public Header getHeader() {
+        return header;
+    }
+}
diff --git a/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..e520ff3
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="ventaair" 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>VentaAir Binding</name>
+       <description>This is the binding for Venta Air - Air cleaning and humidifying devices</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..1a95d71
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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:ventaair:humidifier">
+               <parameter name="ipAddress" type="text" required="true">
+                       <label>IP Address</label>
+                       <context>network-address</context>
+                       <description>IP Address or hostname of the device</description>
+               </parameter>
+               <parameter name="macAddress" type="text" required="true">
+                       <label>MAC Address</label>
+                       <description>MAC Address of the device</description>
+               </parameter>
+               <parameter name="deviceType" type="integer" required="true">
+                       <label>Device Type</label>
+                       <description>Type of the device as integer</description>
+               </parameter>
+               <parameter name="pollingTime" type="integer" required="false" unit="s" min="1" max="86400">
+                       <label>Polling Interval</label>
+                       <default>10</default>
+                       <description>Time in seconds between fetching data from the device</description>
+               </parameter>
+               <parameter name="hash" type="integer" required="false" max="-1">
+                       <label>Hash</label>
+                       <description>Optional negative number that relates to a connection (like from the VentaApp) to the device</description>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/thing/channels.xml
new file mode 100644 (file)
index 0000000..42639dd
--- /dev/null
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="ventaair"
+       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">
+
+       <channel-type id="fanSpeed">
+               <item-type>Number</item-type>
+               <label>Fan Speed</label>
+               <description>Speed of the ventilation fan (0-5)</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">1</option>
+                               <option value="2">2</option>
+                               <option value="3">3</option>
+                               <option value="4">4</option>
+                               <option value="5">5</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="targetHumidity">
+               <item-type>Number</item-type>
+               <label>Target Humidity</label>
+               <description>Target Humidity (30-70)</description>
+               <category>Humidity</category>
+               <state readOnly="false">
+                       <options>
+                               <option value="30">30 %</option>
+                               <option value="35">35 %</option>
+                               <option value="40">40 %</option>
+                               <option value="45">45 %</option>
+                               <option value="50">50 %</option>
+                               <option value="55">55 %</option>
+                               <option value="60">60 %</option>
+                               <option value="65">65 %</option>
+                               <option value="70">70 %</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="timer">
+               <item-type>Number</item-type>
+               <label>Timer</label>
+               <description>Timer (0,1,3,5,7,9h)</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">1 hour</option>
+                               <option value="3">3 hours</option>
+                               <option value="5">5 hours</option>
+                               <option value="7">7 hours</option>
+                               <option value="9">9 hours</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="timerTimePassed">
+               <item-type>Number:Time</item-type>
+               <label>Timer Time Passed</label>
+               <description>Time that has passed since set by the Timer</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="sleepMode">
+               <item-type>Switch</item-type>
+               <label>Sleep Mode</label>
+               <description>Sleep Mode</description>
+       </channel-type>
+
+       <channel-type id="boost">
+               <item-type>Switch</item-type>
+               <label>Boost</label>
+               <description>Boost</description>
+       </channel-type>
+
+       <channel-type id="childLock">
+               <item-type>Switch</item-type>
+               <label>Child Lock</label>
+               <description>Child Lock</description>
+       </channel-type>
+
+       <channel-type id="automatic">
+               <item-type>Switch</item-type>
+               <label>Automatic</label>
+               <description>Automatic</description>
+       </channel-type>
+
+       <channel-type id="temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>Current Temperature</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="humidity">
+               <item-type>Number:Dimenionsless</item-type>
+               <label>Humidity</label>
+               <description>Current Humidity</description>
+               <category>Humidity</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="waterLevel">
+               <item-type>Number</item-type>
+               <label>Water Level</label>
+               <description>Water Level</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Critical</option>
+                               <option value="1">Refill tank</option>
+                               <option value="2">OK</option>
+                               <option value="3">Full</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="fanRPM">
+               <item-type>Number</item-type>
+               <label>Fan RPM</label>
+               <description>Fan RPM</description>
+               <state pattern="%d RPM" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="cleanMode">
+               <item-type>Switch</item-type>
+               <label>Cleaning Mode</label>
+               <description>Device is in cleaning mode (ON)</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="operationTime">
+               <item-type>Number:Time</item-type>
+               <label>Operation Time</label>
+               <description>Operation Time since the device was first started (in hours)</description>
+               <state pattern="%d" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="discReplaceTime">
+               <item-type>Number:Time</item-type>
+               <label>Hygiene Disc Replacement</label>
+               <description>Time until the Hygiene Disc should be replaced (in hours)</description>
+               <state pattern="%d" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="cleaningTime">
+               <item-type>Number:Time</item-type>
+               <label>Cleaning Time</label>
+               <description>Time until next cleaning (in hours)</description>
+               <state pattern="%d" readOnly="true"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ventaair/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..f12a17d
--- /dev/null
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="ventaair"
+       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 id="lw60t">
+               <label>LW60-T VentaAir Humidifier</label>
+               <description>Thing for Venta Air LW60-T Humidifiers</description>
+
+               <channels>
+                       <channel id="power" typeId="system.power"/>
+                       <channel id="fanSpeed" typeId="fanSpeed"/>
+                       <channel id="targetHumidity" typeId="targetHumidity"/>
+                       <channel id="timer" typeId="timer"/>
+                       <channel id="sleepMode" typeId="sleepMode"/>
+                       <channel id="childLock" typeId="childLock"/>
+                       <channel id="automatic" typeId="automatic"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="humidity" typeId="humidity"/>
+                       <channel id="waterLevel" typeId="waterLevel"/>
+                       <channel id="fanRPM" typeId="fanRPM"/>
+                       <channel id="cleanMode" typeId="cleanMode"/>
+                       <channel id="timerTimePassed" typeId="timerTimePassed"/>
+                       <channel id="operationTime" typeId="operationTime"/>
+                       <channel id="discReplaceTime" typeId="discReplaceTime"/>
+                       <channel id="cleaningTime" typeId="cleaningTime"/>
+               </channels>
+
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:ventaair:humidifier"/>
+
+       </thing-type>
+
+       <!-- Generic type has boost channel and maybe in the future other channels -->
+       <thing-type id="generic">
+               <label>Generic Humidifier/Cleaner</label>
+               <description>Thing for Venta Air Humidifiers/Cleaners</description>
+
+               <channels>
+                       <channel id="power" typeId="system.power"/>
+                       <channel id="fanSpeed" typeId="fanSpeed"/>
+                       <channel id="targetHumidity" typeId="targetHumidity"/>
+                       <channel id="timer" typeId="timer"/>
+                       <channel id="sleepMode" typeId="sleepMode"/>
+                       <channel id="boost" typeId="boost"/>
+                       <channel id="childLock" typeId="childLock"/>
+                       <channel id="automatic" typeId="automatic"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="humidity" typeId="humidity"/>
+                       <channel id="waterLevel" typeId="waterLevel"/>
+                       <channel id="fanRPM" typeId="fanRPM"/>
+                       <channel id="cleanMode" typeId="cleanMode"/>
+                       <channel id="timerTimePassed" typeId="timerTimePassed"/>
+                       <channel id="operationTime" typeId="operationTime"/>
+                       <channel id="discReplaceTime" typeId="discReplaceTime"/>
+                       <channel id="cleaningTime" typeId="cleaningTime"/>
+               </channels>
+
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:ventaair:humidifier"/>
+
+       </thing-type>
+
+</thing:thing-descriptions>
index d391f54efc3874fe21b8140eda48c612a6b92b47..38bfdb58ebe89a14418e404c1468c65bc679432d 100644 (file)
     <module>org.openhab.binding.velbus</module>
     <module>org.openhab.binding.velux</module>
     <module>org.openhab.binding.venstarthermostat</module>
+    <module>org.openhab.binding.ventaair</module>
     <module>org.openhab.binding.verisure</module>
     <module>org.openhab.binding.vigicrues</module>
     <module>org.openhab.binding.vitotronic</module>