]> git.basschouten.com Git - openhab-addons.git/blob
771e862d31b411f75b8a8db98c8a8804d3f6cc4b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.discovery;
14
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.Collections;
22 import java.util.HashMap;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.ventaair.internal.VentaAirBindingConstants;
30 import org.openhab.binding.ventaair.internal.message.dto.Header;
31 import org.openhab.binding.ventaair.internal.message.dto.Message;
32 import org.openhab.core.config.discovery.AbstractDiscoveryService;
33 import org.openhab.core.config.discovery.DiscoveryResult;
34 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
35 import org.openhab.core.config.discovery.DiscoveryService;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.thing.ThingUID;
38 import org.openhab.core.util.HexUtils;
39 import org.osgi.service.component.annotations.Component;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import com.google.gson.Gson;
44 import com.google.gson.JsonSyntaxException;
45
46 /**
47  * Discovers Venta Air humidifier and cleaner devices by listening for UDP messages
48  *
49  * @author Stefan Triller - Initial contribution
50  *
51  */
52 @NonNullByDefault
53 @Component(service = DiscoveryService.class, configurationPid = "discovery.ventaair")
54 public class VentaDeviceDiscovery extends AbstractDiscoveryService {
55     private static final String REPRESENTATION_PROPERTY = "macAddress";
56     private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
57             .singleton(VentaAirBindingConstants.THING_TYPE_LW60T);
58     // defined as int, because AbstractDiscoveryService wants and int and not long as provided by Duration.getSeconds()
59     private static final int MANUAL_DISCOVERY_TIME = 30;
60     private static final Duration TIME_BETWEEN_SCANS = Duration.ofSeconds(30);
61
62     private final Logger logger = LoggerFactory.getLogger(VentaDeviceDiscovery.class);
63
64     private @Nullable ScheduledFuture<?> scanJob = null;
65
66     public VentaDeviceDiscovery() {
67         super(SUPPORTED_THING_TYPES_UIDS, MANUAL_DISCOVERY_TIME, true);
68     }
69
70     @Override
71     protected void startScan() {
72         findDevices();
73     }
74
75     @Override
76     protected void startBackgroundDiscovery() {
77         super.startBackgroundDiscovery();
78
79         ScheduledFuture<?> localScanJob = scanJob;
80         if (localScanJob != null) {
81             localScanJob.cancel(true);
82         }
83
84         scanJob = scheduler.scheduleWithFixedDelay(this::findDevices, 5, TIME_BETWEEN_SCANS.getSeconds(),
85                 TimeUnit.SECONDS);
86     }
87
88     @Override
89     protected void stopBackgroundDiscovery() {
90         super.stopBackgroundDiscovery();
91
92         ScheduledFuture<?> localScanJob = scanJob;
93         if (localScanJob != null) {
94             localScanJob.cancel(true);
95         }
96         scanJob = null;
97     }
98
99     private void findDevices() {
100         byte[] buf = new byte[512];
101         try (DatagramSocket socket = new DatagramSocket(VentaAirBindingConstants.PORT)) {
102             DatagramPacket packet = new DatagramPacket(buf, buf.length);
103             socket.receive(packet);
104
105             Message m = parseDiscoveryPaket(packet.getData());
106             if (m == null) {
107                 logger.debug("Received broken discovery packet data={}", HexUtils.bytesToHex(packet.getData(), ", "));
108                 return;
109             }
110
111             logger.debug("Found device with: IP={} Mac={} and device type={}", m.getHeader().getIpAdress(),
112                     m.getHeader().getMacAdress(), m.getHeader().getDeviceType());
113
114             ThingTypeUID thingTypeUID;
115             switch (m.getHeader().getDeviceType()) {
116                 case 4:
117                     thingTypeUID = VentaAirBindingConstants.THING_TYPE_LW60T;
118                     break;
119                 default:
120                     thingTypeUID = VentaAirBindingConstants.THING_TYPE_GENERIC;
121                     break;
122             }
123             createDiscoveryResult(thingTypeUID, m.getHeader());
124         } catch (SocketException e) {
125             logger.warn("Could not open port {} to scan for Venta devices in the network.",
126                     VentaAirBindingConstants.PORT);
127         } catch (IOException e) {
128             // swallow, since we already log the broken packet above
129         }
130     }
131
132     private @Nullable Message parseDiscoveryPaket(byte[] packet) {
133         Gson gson = new Gson();
134         Message msg = null;
135
136         String packetAsString = new String(packet, StandardCharsets.UTF_8);
137
138         String[] lines = packetAsString.split("\n");
139         if (lines.length >= 3) {
140             String input = lines[2];
141             int end = input.lastIndexOf("}"); // strip padding bytes added by the device
142             if (end > 0) {
143                 String rawJSONstring = input.substring(0, end + 1);
144                 try {
145                     msg = gson.fromJson(rawJSONstring, Message.class);
146                 } catch (JsonSyntaxException e) {
147                     logger.debug("Received invalid JSON data={}", rawJSONstring, e);
148                 }
149             }
150         }
151         return msg;
152     }
153
154     private void createDiscoveryResult(ThingTypeUID thingTypeUID, Header messageHeader) {
155         String ipAddress = messageHeader.getIpAdress();
156         String macAddress = messageHeader.getMacAdress();
157         int deviceType = messageHeader.getDeviceType();
158
159         ThingUID uid = new ThingUID(thingTypeUID, ipAddress.replace(".", "_"));
160         HashMap<String, Object> properties = new HashMap<>();
161         properties.put("ipAddress", ipAddress);
162         properties.put(REPRESENTATION_PROPERTY, macAddress);
163         properties.put("deviceType", deviceType);
164
165         String typeLabel = thingTypeUID.getId().toUpperCase();
166
167         DiscoveryResult result = DiscoveryResultBuilder.create(uid).withRepresentationProperty(REPRESENTATION_PROPERTY)
168                 .withProperties(properties).withLabel(typeLabel + " (IP=" + ipAddress + ")").build();
169
170         this.thingDiscovered(result);
171     }
172 }