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
14 package org.openhab.binding.govee.internal;
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;
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;
45 * Discovers Govee devices
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
52 * Based on the description at https://app-h5.govee.com/user-manual/wlan-guide
54 * A typical scan response looks as follows
61 * "ip":"192.168.1.23",
62 * "device":"1F:80:C5:32:32:36:72:4E",
64 * "bleVersionHard":"3.01.01",
65 * "bleVersionSoft":"1.03.01",
66 * "wifiVersionHard":"1.00.10",
67 * "wifiVersionSoft":"1.02.03"
74 * Note that it uses the same port for receiving data like when receiving devices status updates.
78 * @author Stefan Höhn - Initial Contribution
79 * @author Danny Baumann - Thread-Safe design refactoring
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);
86 private CommunicationManager communicationManager;
88 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT);
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;
99 // for test purposes only
100 public GoveeDiscoveryService(CommunicationManager communicationManager) {
101 super(SUPPORTED_THING_TYPES_UIDS, 0, false);
102 this.communicationManager = communicationManager;
106 protected void startScan() {
107 logger.debug("starting Scan");
109 getLocalNetworkInterfaces().forEach(localNetworkInterface -> {
110 logger.debug("Discovering Govee devices on {} ...", localNetworkInterface);
112 communicationManager.runDiscoveryForInterface(localNetworkInterface, response -> {
113 DiscoveryResult result = responseToResult(response);
114 if (result != null) {
115 thingDiscovered(result);
118 logger.trace("After runDiscoveryForInterface");
119 } catch (IOException e) {
120 logger.debug("Discovery with IO exception: {}", e.getMessage());
122 logger.trace("After try");
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);
134 final String ipAddress = data.ip();
135 if (ipAddress.isEmpty()) {
136 logger.warn("Empty IP address received during discovery - ignoring {}", response);
140 final String sku = data.sku();
142 logger.warn("Empty SKU (product name) received during discovery - ignoring {}", response);
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());
154 String nameForLabel = productName != null ? productName + " " + sku : sku;
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));
164 if (productName != null) {
165 builder.withProperty(GoveeBindingConstants.PRODUCT_NAME, productName);
168 String hwVersion = data.wifiVersionHard();
169 if (hwVersion != null) {
170 builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
172 String swVersion = data.wifiVersionSoft();
173 if (swVersion != null) {
174 builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
177 return builder.build();
180 private List<NetworkInterface> getLocalNetworkInterfaces() {
181 List<NetworkInterface> result = new LinkedList<>();
183 for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
185 if (networkInterface.isUp() && !networkInterface.isLoopback()
186 && !networkInterface.isPointToPoint()) {
187 result.add(networkInterface);
189 } catch (SocketException exception) {
193 } catch (SocketException exception) {