]> git.basschouten.com Git - openhab-addons.git/blob
03aaee0e98a1ca549e277048f8d8b10abeaeca6f
[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
14 package org.openhab.binding.govee.internal;
15
16 import java.io.IOException;
17 import java.net.NetworkInterface;
18 import java.net.SocketException;
19 import java.util.Collections;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.Set;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.govee.internal.model.DiscoveryData;
27 import org.openhab.binding.govee.internal.model.DiscoveryResponse;
28 import org.openhab.core.config.discovery.AbstractDiscoveryService;
29 import org.openhab.core.config.discovery.DiscoveryResult;
30 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
31 import org.openhab.core.config.discovery.DiscoveryService;
32 import org.openhab.core.i18n.LocaleProvider;
33 import org.openhab.core.i18n.TranslationProvider;
34 import org.openhab.core.thing.ThingTypeUID;
35 import org.openhab.core.thing.ThingUID;
36 import org.osgi.framework.Bundle;
37 import org.osgi.framework.FrameworkUtil;
38 import org.osgi.service.component.annotations.Activate;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.Reference;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * Discovers Govee devices
46  *
47  * Scan approach:
48  * 1. Determines all local network interfaces
49  * 2. Send a multicast message on each interface to the Govee multicast address 239.255.255.250 at port 4001
50  * 3. Retrieve the list of devices
51  *
52  * Based on the description at https://app-h5.govee.com/user-manual/wlan-guide
53  *
54  * A typical scan response looks as follows
55  *
56  * <pre>{@code
57  * {
58  *   "msg":{
59  *     "cmd":"scan",
60  *     "data":{
61  *       "ip":"192.168.1.23",
62  *       "device":"1F:80:C5:32:32:36:72:4E",
63  *       "sku":"Hxxxx",
64  *       "bleVersionHard":"3.01.01",
65  *       "bleVersionSoft":"1.03.01",
66  *       "wifiVersionHard":"1.00.10",
67  *       "wifiVersionSoft":"1.02.03"
68  *     }
69  *   }
70  * }
71  * }
72  * </pre>
73  *
74  * Note that it uses the same port for receiving data like when receiving devices status updates.
75  *
76  * @see GoveeHandler
77  *
78  * @author Stefan Höhn - Initial Contribution
79  * @author Danny Baumann - Thread-Safe design refactoring
80  */
81 @NonNullByDefault
82 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee")
83 public class GoveeDiscoveryService extends AbstractDiscoveryService {
84     private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class);
85
86     private CommunicationManager communicationManager;
87
88     private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT);
89
90     @Activate
91     public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider,
92             @Reference CommunicationManager communicationManager) {
93         super(SUPPORTED_THING_TYPES_UIDS, 0, false);
94         this.i18nProvider = i18nProvider;
95         this.localeProvider = localeProvider;
96         this.communicationManager = communicationManager;
97     }
98
99     // for test purposes only
100     public GoveeDiscoveryService(CommunicationManager communicationManager) {
101         super(SUPPORTED_THING_TYPES_UIDS, 0, false);
102         this.communicationManager = communicationManager;
103     }
104
105     @Override
106     protected void startScan() {
107         logger.debug("starting Scan");
108
109         getLocalNetworkInterfaces().forEach(localNetworkInterface -> {
110             logger.debug("Discovering Govee devices on {} ...", localNetworkInterface);
111             try {
112                 communicationManager.runDiscoveryForInterface(localNetworkInterface, response -> {
113                     DiscoveryResult result = responseToResult(response);
114                     if (result != null) {
115                         thingDiscovered(result);
116                     }
117                 });
118                 logger.trace("After runDiscoveryForInterface");
119             } catch (IOException e) {
120                 logger.debug("Discovery with IO exception: {}", e.getMessage());
121             }
122             logger.trace("After try");
123         });
124     }
125
126     public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) {
127         final DiscoveryData data = response.msg().data();
128         final String macAddress = data.device();
129         if (macAddress.isEmpty()) {
130             logger.warn("Empty Mac address received during discovery - ignoring {}", response);
131             return null;
132         }
133
134         final String ipAddress = data.ip();
135         if (ipAddress.isEmpty()) {
136             logger.warn("Empty IP address received during discovery - ignoring {}", response);
137             return null;
138         }
139
140         final String sku = data.sku();
141         if (sku.isEmpty()) {
142             logger.warn("Empty SKU (product name) received during discovery - ignoring {}", response);
143             return null;
144         }
145
146         final String productName;
147         if (i18nProvider != null) {
148             Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class);
149             productName = i18nProvider.getText(bundle, "discovery.govee-light." + sku, null,
150                     localeProvider.getLocale());
151         } else {
152             productName = sku;
153         }
154         String nameForLabel = productName != null ? productName + " " + sku : sku;
155
156         ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_"));
157         DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUid)
158                 .withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS)
159                 .withProperty(GoveeBindingConstants.MAC_ADDRESS, macAddress)
160                 .withProperty(GoveeBindingConstants.IP_ADDRESS, ipAddress)
161                 .withProperty(GoveeBindingConstants.DEVICE_TYPE, sku)
162                 .withLabel(String.format("Govee %s (%s)", nameForLabel, ipAddress));
163
164         if (productName != null) {
165             builder.withProperty(GoveeBindingConstants.PRODUCT_NAME, productName);
166         }
167
168         String hwVersion = data.wifiVersionHard();
169         if (hwVersion != null) {
170             builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
171         }
172         String swVersion = data.wifiVersionSoft();
173         if (swVersion != null) {
174             builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
175         }
176
177         return builder.build();
178     }
179
180     private List<NetworkInterface> getLocalNetworkInterfaces() {
181         List<NetworkInterface> result = new LinkedList<>();
182         try {
183             for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
184                 try {
185                     if (networkInterface.isUp() && !networkInterface.isLoopback()
186                             && !networkInterface.isPointToPoint()) {
187                         result.add(networkInterface);
188                     }
189                 } catch (SocketException exception) {
190                     // ignore
191                 }
192             }
193         } catch (SocketException exception) {
194             return List.of();
195         }
196         return result;
197     }
198 }