2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ventaair.internal.discovery;
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.DatagramSocket;
18 import java.net.SocketException;
19 import java.nio.charset.StandardCharsets;
20 import java.time.Duration;
21 import java.util.HashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.ventaair.internal.VentaAirBindingConstants;
29 import org.openhab.binding.ventaair.internal.message.dto.Header;
30 import org.openhab.binding.ventaair.internal.message.dto.Message;
31 import org.openhab.core.config.discovery.AbstractDiscoveryService;
32 import org.openhab.core.config.discovery.DiscoveryResult;
33 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
34 import org.openhab.core.config.discovery.DiscoveryService;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.thing.ThingUID;
37 import org.openhab.core.util.HexUtils;
38 import org.osgi.service.component.annotations.Component;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import com.google.gson.Gson;
43 import com.google.gson.JsonSyntaxException;
46 * Discovers Venta Air humidifier and cleaner devices by listening for UDP messages
48 * @author Stefan Triller - Initial contribution
52 @Component(service = DiscoveryService.class, configurationPid = "discovery.ventaair")
53 public class VentaDeviceDiscovery extends AbstractDiscoveryService {
54 private static final String REPRESENTATION_PROPERTY = "macAddress";
55 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set
56 .of(VentaAirBindingConstants.THING_TYPE_LW60T);
57 // defined as int, because AbstractDiscoveryService wants and int and not long as provided by Duration.getSeconds()
58 private static final int MANUAL_DISCOVERY_TIME = 30;
59 private static final Duration TIME_BETWEEN_SCANS = Duration.ofSeconds(30);
61 private final Logger logger = LoggerFactory.getLogger(VentaDeviceDiscovery.class);
63 private @Nullable ScheduledFuture<?> scanJob = null;
65 public VentaDeviceDiscovery() {
66 super(SUPPORTED_THING_TYPES_UIDS, MANUAL_DISCOVERY_TIME, true);
70 protected void startScan() {
75 protected void startBackgroundDiscovery() {
76 super.startBackgroundDiscovery();
78 ScheduledFuture<?> localScanJob = scanJob;
79 if (localScanJob != null) {
80 localScanJob.cancel(true);
83 scanJob = scheduler.scheduleWithFixedDelay(this::findDevices, 5, TIME_BETWEEN_SCANS.getSeconds(),
88 protected void stopBackgroundDiscovery() {
89 super.stopBackgroundDiscovery();
91 ScheduledFuture<?> localScanJob = scanJob;
92 if (localScanJob != null) {
93 localScanJob.cancel(true);
98 private void findDevices() {
99 byte[] buf = new byte[512];
100 try (DatagramSocket socket = new DatagramSocket(VentaAirBindingConstants.PORT)) {
101 DatagramPacket packet = new DatagramPacket(buf, buf.length);
102 socket.receive(packet);
104 Message m = parseDiscoveryPaket(packet.getData());
106 logger.debug("Received broken discovery packet data={}", HexUtils.bytesToHex(packet.getData(), ", "));
110 logger.debug("Found device with: IP={} Mac={} and device type={}", m.getHeader().getIpAdress(),
111 m.getHeader().getMacAdress(), m.getHeader().getDeviceType());
113 ThingTypeUID thingTypeUID;
114 switch (m.getHeader().getDeviceType()) {
116 thingTypeUID = VentaAirBindingConstants.THING_TYPE_LW60T;
119 thingTypeUID = VentaAirBindingConstants.THING_TYPE_GENERIC;
122 createDiscoveryResult(thingTypeUID, m.getHeader());
123 } catch (SocketException e) {
124 logger.warn("Could not open port {} to scan for Venta devices in the network.",
125 VentaAirBindingConstants.PORT);
126 } catch (IOException e) {
127 // swallow, since we already log the broken packet above
131 private @Nullable Message parseDiscoveryPaket(byte[] packet) {
132 Gson gson = new Gson();
135 String packetAsString = new String(packet, StandardCharsets.UTF_8);
137 String[] lines = packetAsString.split("\n");
138 if (lines.length >= 3) {
139 String input = lines[2];
140 int end = input.lastIndexOf("}"); // strip padding bytes added by the device
142 String rawJSONstring = input.substring(0, end + 1);
144 msg = gson.fromJson(rawJSONstring, Message.class);
145 } catch (JsonSyntaxException e) {
146 logger.debug("Received invalid JSON data={}", rawJSONstring, e);
153 private void createDiscoveryResult(ThingTypeUID thingTypeUID, Header messageHeader) {
154 String ipAddress = messageHeader.getIpAdress();
155 String macAddress = messageHeader.getMacAdress();
156 int deviceType = messageHeader.getDeviceType();
158 ThingUID uid = new ThingUID(thingTypeUID, ipAddress.replace(".", "_"));
159 HashMap<String, Object> properties = new HashMap<>();
160 properties.put("ipAddress", ipAddress);
161 properties.put(REPRESENTATION_PROPERTY, macAddress);
162 properties.put("deviceType", deviceType);
164 String typeLabel = thingTypeUID.getId().toUpperCase();
166 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withRepresentationProperty(REPRESENTATION_PROPERTY)
167 .withProperties(properties).withLabel(typeLabel + " (IP=" + ipAddress + ")").build();
169 this.thingDiscovered(result);