Skip to content

Commit 08602c0

Browse files
authored
[ventaair] New VentaAir binding for air humidifiers (openhab#9979)
* [ventaair] New VentaAir binding for air humidifiers New binding that implements support for air humidifier from Venta Air. Closes openhab#9922 Signed-off-by: Stefan Triller <[email protected]>
1 parent 95cdc3c commit 08602c0

34 files changed

+2028
-0
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@
286286
/bundles/org.openhab.binding.velbus/ @cedricboon
287287
/bundles/org.openhab.binding.velux/ @gs4711
288288
/bundles/org.openhab.binding.venstarthermostat/ @hww3 @digitaldan
289+
/bundles/org.openhab.binding.ventaair/ @t2000
289290
/bundles/org.openhab.binding.verisure/ @jannegpriv
290291
/bundles/org.openhab.binding.vigicrues/ @clinique
291292
/bundles/org.openhab.binding.vitotronic/ @steand

bom/openhab-addons/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,11 @@
14111411
<artifactId>org.openhab.binding.venstarthermostat</artifactId>
14121412
<version>${project.version}</version>
14131413
</dependency>
1414+
<dependency>
1415+
<groupId>org.openhab.addons.bundles</groupId>
1416+
<artifactId>org.openhab.binding.ventaair</artifactId>
1417+
<version>${project.version}</version>
1418+
</dependency>
14141419
<dependency>
14151420
<groupId>org.openhab.addons.bundles</groupId>
14161421
<artifactId>org.openhab.binding.verisure</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This content is produced and maintained by the openHAB project.
2+
3+
* Project home: https://www.openhab.org
4+
5+
== Declared Project Licenses
6+
7+
This program and the accompanying materials are made available under the terms
8+
of the Eclipse Public License 2.0 which is available at
9+
https://www.eclipse.org/legal/epl-2.0/.
10+
11+
== Source Code
12+
13+
https://github.com/openhab/openhab-addons
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# VentaAir Binding
2+
3+
This binding is for air humidifiers from Venta Air.
4+
Thankfully the vendor allows for communicating within the local network without needing any internet access or accounts.
5+
This is even stated in the official manual.
6+
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.
7+
8+
## Supported Things
9+
10+
It currently supports the LW60-T device (`ThingType`: "lw60t") as well as a `ThingType` ("generic") for other models.
11+
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.
12+
13+
## Discovery
14+
15+
This binding supports an automatic discovery for humidifiers that are connected to the local network and which are in the same broadcast domain.
16+
To do so, the binding listens to UDP port 48000 for data and creates `DiscoveryResult`s based on the received data from the device.
17+
This comes in handy for getting the MAC address for the device for example.
18+
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.
19+
After confirming this request, the user can link its items to receive data or control the device.
20+
21+
## Thing Configuration
22+
23+
There are three mandatory configuration parameters for a thing: `ipAddress`, `macAddress` and `deviceType`.
24+
25+
| parameter | required | description |
26+
|----------|------------|-------------------------------------------|
27+
| ipAddress | Y | The IP Address or hostname of the device. |
28+
| macAddress | Y | The MAC address of the device. |
29+
| 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. |
30+
| pollingTime | N | The time interval in seconds in which the data should be polled from the device, default is 10 seconds. |
31+
| 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. (*) |
32+
33+
(*) 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).
34+
However, the LW60-T allows for multiple connections to different clients, identified by different `hash` values at the same time without issues.
35+
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.
36+
37+
Example Thing configuration:
38+
39+
```
40+
Thing ventaair:lw60t:humidifier [ ipAddress="192.168.42.69", macAddress="f8:f0:05:a6:4e:03", deviceType=4, pollingTime=10, hash=-42]
41+
```
42+
43+
## Channels
44+
45+
These are the channels that are currently supported:
46+
47+
48+
| channel | type (RO=read-only) | description |
49+
|----------|--------|------------------------------|
50+
| power | Switch | This is the power on/off channel |
51+
| fanSpeed | Number | This is the channel to control the steps (in range 0-5 where 0 means "off") for the speed of the fan |
52+
| targetHumidity | Number | This channel sets the target humidity (in percent) that should be tried to reach by the device (allowed values: 30-70) |
53+
| 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") |
54+
| sleepMode | Switch | This channel controls the sleep mode of the device (dims the display and slows down the fan) |
55+
| childLock | Switch | This is the control channel for the child lock |
56+
| automatic | Switch | This is the control channel to start the automatic operation mode of the device |
57+
| cleanMode | Switch (RO) | This is the channel that indicates if the device is in the cleaning mode |
58+
| temperature | Number:Temperature (RO) | This channel provides the current measured temperature in Celsius or Fahrenheit as configured on the device |
59+
| humidity | Number:Dimensionless (RO) | This channel provides the humidity measured by the device in percent |
60+
| 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 |
61+
| fanRPM | Number (RO) | This channel provides the speed of the ventilation fan |
62+
| timerTimePassed | Number:Time (RO) | If a timer has been set, this channel provides the minutes since when the timer was started |
63+
| operationTime | Number:Time (RO) | This channel provides the operation time of the device in hours |
64+
| discReplaceTime | Number:Time (RO) | This channel provides the time in how many hours the cleaning disc should be replaced |
65+
| cleaningTime | Number:Time (RO) | This channel provides the time in how many hours the device should be cleaned |
66+
| boost | Switch | This is the control channel for the boost mode (on some devices that supports it) |
67+
68+
## Full Example
69+
70+
Things:
71+
72+
```
73+
Thing ventaair:lw60t:humidifier [ ipAddress="192.168.42.69", macAddress="f8:f0:05:a6:4e:03", deviceType=4, pollingTime=10, hash=-42]
74+
```
75+
76+
Items:
77+
78+
```
79+
Group gHumidifier "Air Humidifier" <humidity>
80+
81+
Switch Humidifier_Power "Power: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:power" }
82+
Number Humidifier_FanSpeed "FanSpeed: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:fanSpeed" }
83+
Number Humidifier_TargetHum "Target Humidity: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:targetHumidity" }
84+
Number Humidifier_Timer "Timer: [%s]" (gHumidifier) { channel="ventaair:lw60t:humidifier:timer" }
85+
86+
Switch Humidifier_SleepMode "SleepMode:" (gHumidifier) { channel="ventaair:lw60t:humidifier:sleepMode" }
87+
Switch Humidifier_ChildLock "ChildLock:" (gHumidifier) { channel="ventaair:lw60t:humidifier:childLock" }
88+
Switch Humidifier_Automatic "Automatic:" (gHumidifier) { channel="ventaair:lw60t:humidifier:automatic" }
89+
90+
Switch Humidifier_CleaningMode "Cleaning mode:" (gHumidifier) { channel="ventaair:lw60t:humidifier:cleanMode" }
91+
92+
Number:Temperature Humidifier_Temperature "Temp: [%.1f %unit%]" (gHumidifier) { channel="ventaair:lw60t:humidifier:temperature" }
93+
Number:Temperature Humidifier_temperatureF "Temp: [%.1f °F]" (gHumidifier) { channel="ventaair:lw60t:humidifier:temperature" }
94+
Number Humidifier_Humidity "Humidity: [%.1f %%]" (gHumidifier) { channel="ventaair:lw60t:humidifier:humidity" }
95+
96+
Number Humidifier_WaterLevel "WaterLevel: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:waterLevel" }
97+
Number Humidifier_FanRPM "Fan RPM: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:fanRPM" }
98+
99+
Number Humidifier_TimerTime "Timer time: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:timerTimePassed" }
100+
Number Humidifier_OpTime "Operation Time: [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:operationTime" }
101+
Number Humidifier_ReplaceTime "Disc replace in (h): [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:discReplaceTime" }
102+
Number Humidifier_CleaningTime "Cleaning in (h): [%d]" (gHumidifier) { channel="ventaair:lw60t:humidifier:cleaningTime" }
103+
104+
//for generic devices:
105+
Switch boost "Boost:" { channel="ventaair:generic:humidifier:boost" }
106+
```
107+
108+
Sitemap:
109+
110+
```
111+
Text item=Humidifier_Humidity
112+
Text item=Humidifier_Temperature
113+
Switch item=Humidifier_Power
114+
Switch item=Humidifier_SleepMode
115+
Switch item=Humidifier_FanSpeed icon="fan" mappings=[0="0", 1="1", 2="2", 3="3", 4="4", 5="5"]
116+
Switch item=Humidifier_TargetHum mappings=[30="30", 35="35", 40="40", 45="45", 50="50", 55="55", 60="60", 65="65", 70="70"]
117+
Switch item=Humidifier_Timer mappings=[0="0", 1="1", 3="3", 5="5", 7="7", 9="9"]
118+
Text item=Humidifier_WaterLevel
119+
Text item=Humidifier_FanRPM
120+
Text item=Humidifier_OpTime
121+
Text item=Humidifier_ReplaceTime
122+
Text item=Humidifier_CleaningTime
123+
Text item=Humidifier_TimerTime
124+
Switch item=Humidifier_CleaningModeActive
125+
Switch item=Humidifier_ChildLock
126+
Switch item=Humidifier_Automatic
127+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.openhab.addons.bundles</groupId>
9+
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
10+
<version>3.1.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>org.openhab.binding.ventaair</artifactId>
14+
15+
<name>openHAB Add-ons :: Bundles :: VentaAir Binding</name>
16+
17+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<features name="org.openhab.binding.ventaair-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
3+
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
4+
5+
<feature name="openhab-binding-ventaair" description="VentaAir Binding" version="${project.version}">
6+
<feature>openhab-runtime-base</feature>
7+
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ventaair/${project.version}</bundle>
8+
</feature>
9+
</features>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Copyright (c) 2010-2021 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.ventaair.internal;
14+
15+
import java.io.BufferedReader;
16+
import java.io.ByteArrayOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.io.InputStreamReader;
20+
import java.io.OutputStream;
21+
import java.math.BigDecimal;
22+
import java.net.Socket;
23+
import java.nio.charset.StandardCharsets;
24+
import java.time.Duration;
25+
import java.util.Arrays;
26+
import java.util.concurrent.ScheduledExecutorService;
27+
import java.util.concurrent.ScheduledFuture;
28+
import java.util.concurrent.TimeUnit;
29+
30+
import org.eclipse.jdt.annotation.NonNullByDefault;
31+
import org.eclipse.jdt.annotation.Nullable;
32+
import org.openhab.binding.ventaair.internal.VentaThingHandler.StateUpdatedCallback;
33+
import org.openhab.binding.ventaair.internal.message.action.Action;
34+
import org.openhab.binding.ventaair.internal.message.dto.CommandMessage;
35+
import org.openhab.binding.ventaair.internal.message.dto.DeviceInfoMessage;
36+
import org.openhab.binding.ventaair.internal.message.dto.Header;
37+
import org.openhab.binding.ventaair.internal.message.dto.Message;
38+
import org.openhab.core.thing.binding.ThingHandler;
39+
import org.openhab.core.util.HexUtils;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
43+
import com.google.gson.Gson;
44+
45+
/**
46+
* The {@link Communicator} is responsible for sending/receiving commands to/from the device
47+
*
48+
* @author Stefan Triller - Initial contribution
49+
*
50+
*/
51+
@NonNullByDefault
52+
public class Communicator {
53+
private static final Duration COMMUNICATION_TIMEOUT = Duration.ofSeconds(5);
54+
55+
private final Logger logger = LoggerFactory.getLogger(Communicator.class);
56+
57+
private @Nullable String ipAddress;
58+
private Header header;
59+
private int pollingTimeInSeconds;
60+
private StateUpdatedCallback callback;
61+
62+
private Gson gson = new Gson();
63+
64+
private @Nullable ScheduledFuture<?> pollingJob;
65+
66+
public Communicator(@Nullable String ipAddress, Header header, @Nullable BigDecimal pollingTime,
67+
StateUpdatedCallback callback) {
68+
this.ipAddress = ipAddress;
69+
this.header = header;
70+
if (pollingTime != null) {
71+
this.pollingTimeInSeconds = pollingTime.intValue();
72+
} else {
73+
this.pollingTimeInSeconds = 60;
74+
}
75+
this.callback = callback;
76+
}
77+
78+
/**
79+
* Sends a request message to the device, reads the reply and informs the listener about the current device data
80+
*/
81+
public void pollDataFromDevice() {
82+
String messageJson = gson.toJson(new Message(header));
83+
84+
try (Socket socket = new Socket(ipAddress, VentaAirBindingConstants.PORT)) {
85+
socket.setSoTimeout((int) COMMUNICATION_TIMEOUT.toMillis());
86+
InputStream input = socket.getInputStream();
87+
OutputStream output = socket.getOutputStream();
88+
89+
byte[] dataToSend = buildMessageBytes(messageJson, "GET", "Complete");
90+
// we write these lines to the log in order to help users with new/other venta devices, so they only need to
91+
// enable debug logging
92+
logger.debug("Sending request data message (String):\n{}", new String(dataToSend));
93+
logger.debug("Sending request data message (bytes): [{}]", HexUtils.bytesToHex(dataToSend, ", "));
94+
output.write(dataToSend);
95+
96+
BufferedReader br = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
97+
String reply = "";
98+
while ((reply = br.readLine()) != null) {
99+
if (reply.startsWith("{")) {
100+
// remove padding byte(s) after JSON data
101+
String data = String.valueOf(reply.toCharArray(), 0, reply.length() - 1);
102+
// we write this line to the log in order to help users with new/other venta devices, so they only
103+
// need to enable debug logging
104+
logger.debug("Got Data from device: {}", data);
105+
106+
DeviceInfoMessage deviceInfoMessage = gson.fromJson(data, DeviceInfoMessage.class);
107+
if (deviceInfoMessage != null) {
108+
callback.stateUpdated(deviceInfoMessage);
109+
}
110+
}
111+
}
112+
br.close();
113+
socket.close();
114+
} catch (IOException e) {
115+
callback.communicationProblem();
116+
}
117+
}
118+
119+
private byte[] buildMessageBytes(String message, String method, String endpoint) throws IOException {
120+
ByteArrayOutputStream getInfoOutputStream = new ByteArrayOutputStream();
121+
getInfoOutputStream
122+
.write(createMessageHeader(method, endpoint, message.length()).getBytes(StandardCharsets.UTF_8));
123+
getInfoOutputStream.write(message.getBytes(StandardCharsets.UTF_8));
124+
getInfoOutputStream.write(new byte[] { 0x1c, 0x00 });
125+
return getInfoOutputStream.toByteArray();
126+
}
127+
128+
private String createMessageHeader(String method, String endPoint, int contentLength) {
129+
return method + " /" + endPoint + "\n" + "Content-Length: " + contentLength + "\n" + "\n";
130+
}
131+
132+
/**
133+
* Sends and {@link Action} to the device to set for example the FanSpeed or TargetHumidity
134+
*
135+
* @param action - The action to be send to the device
136+
*/
137+
public void sendActionToDevice(Action action) throws IOException {
138+
CommandMessage message = new CommandMessage(action, header);
139+
140+
String messageJson = gson.toJson(message);
141+
142+
try (Socket socket = new Socket(ipAddress, VentaAirBindingConstants.PORT)) {
143+
OutputStream output = socket.getOutputStream();
144+
145+
byte[] dataToSend = buildMessageBytes(messageJson, "POST", "Action");
146+
147+
// we write these lines to the log in order to help users with new/other venta devices, so they only need to
148+
// enable debug logging
149+
logger.debug("sending: {}", new String(dataToSend));
150+
logger.debug("sendingArray: {}", Arrays.toString(dataToSend));
151+
152+
output.write(dataToSend);
153+
socket.close();
154+
}
155+
}
156+
157+
/**
158+
* Starts the polling job to fetch the current device data
159+
*
160+
* @param scheduler - The scheduler of the {@link ThingHandler}
161+
*/
162+
public void startPollDataFromDevice(ScheduledExecutorService scheduler) {
163+
stopPollDataFromDevice();
164+
pollingJob = scheduler.scheduleWithFixedDelay(this::pollDataFromDevice, 2, pollingTimeInSeconds,
165+
TimeUnit.SECONDS);
166+
}
167+
168+
/**
169+
* Stops the polling for device data
170+
*/
171+
public void stopPollDataFromDevice() {
172+
ScheduledFuture<?> localPollingJob = pollingJob;
173+
if (localPollingJob != null && !localPollingJob.isCancelled()) {
174+
localPollingJob.cancel(true);
175+
}
176+
logger.debug("Setting polling job to null");
177+
pollingJob = null;
178+
}
179+
}

0 commit comments

Comments
 (0)