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.bundles org.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