diff --git a/CODEOWNERS b/CODEOWNERS
index 428c1e03a39..58d6473b38a 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -286,6 +286,7 @@
/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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 62161f65f2f..34f4680e018 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1411,6 +1411,11 @@
org.openhab.binding.venstarthermostat${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.ventaair
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.verisure
diff --git a/bundles/org.openhab.binding.ventaair/NOTICE b/bundles/org.openhab.binding.ventaair/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/NOTICE
@@ -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
index 00000000000..68285514a9a
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/README.md
@@ -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"
+
+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
index 00000000000..58e74bff82b
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.ventaair
+
+ openHAB Add-ons :: Bundles :: VentaAir Binding
+
+
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
index 00000000000..41477b78396
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.ventaair/${project.version}
+
+
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
index 00000000000..f358fd4d8e8
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/Communicator.java
@@ -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
index 00000000000..4e2e797408b
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirBindingConstants.java
@@ -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
index 00000000000..949ffa03c22
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirDeviceConfiguration.java
@@ -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
index 00000000000..3d0df13e2af
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaAirHandlerFactory.java
@@ -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 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
index 00000000000..ede6692f680
--- /dev/null
+++ b/bundles/org.openhab.binding.ventaair/src/main/java/org/openhab/binding/ventaair/internal/VentaThingHandler.java
@@ -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 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 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 temperatureState = new QuantityType<>(measurements.getTemperature(),
+ temperatureUnit);
+ updateState(VentaAirBindingConstants.CHANNEL_TEMPERATURE, temperatureState);
+ channelValueCache.put(VentaAirBindingConstants.CHANNEL_TEMPERATURE, temperatureState);
+
+ QuantityType 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