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.broadlinkthermostat.internal.discovery;
15 import static org.openhab.binding.broadlinkthermostat.internal.BroadlinkBindingConstants.*;
17 import java.io.IOException;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.util.HashMap;
22 import java.util.Optional;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.core.config.discovery.AbstractDiscoveryService;
30 import org.openhab.core.config.discovery.DiscoveryResult;
31 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
32 import org.openhab.core.config.discovery.DiscoveryService;
33 import org.openhab.core.net.NetworkAddressService;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.thing.ThingUID;
37 import org.osgi.service.component.annotations.Activate;
38 import org.osgi.service.component.annotations.Component;
39 import org.osgi.service.component.annotations.Reference;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.github.mob41.blapi.BLDevice;
46 * The {@link BroadlinkDiscoveryService} is responsible for discovering Broadlink devices through
49 * @author Florian Mueller - Initial contribution
51 @Component(service = DiscoveryService.class, configurationPid = "discovery.broadlinkthermostat")
53 public class BroadlinkDiscoveryService extends AbstractDiscoveryService {
55 private final Logger logger = LoggerFactory.getLogger(BroadlinkDiscoveryService.class);
57 private final NetworkAddressService networkAddressService;
59 private static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Set.of(FLOUREON_THERMOSTAT_THING_TYPE,
60 RM_UNIVERSAL_REMOTE_THING_TYPE);
61 private static final int DISCOVERY_TIMEOUT_SECONDS = 30;
62 private @Nullable ScheduledFuture<?> backgroundDiscoveryFuture;
65 public BroadlinkDiscoveryService(@Reference NetworkAddressService networkAddressService) {
66 super(DISCOVERABLE_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS);
67 this.networkAddressService = networkAddressService;
70 private void createScanner() {
71 long timestampOfLastScan = getTimestampOfLastScan();
72 BLDevice[] blDevices = new BLDevice[0];
75 InetAddress sourceAddress = getIpAddress();
76 if (sourceAddress != null) {
77 logger.debug("Using source address {} for sending out broadcast request.", sourceAddress);
78 blDevices = BLDevice.discoverDevices(sourceAddress, 0, DISCOVERY_TIMEOUT_SECONDS * 1000);
80 blDevices = BLDevice.discoverDevices(DISCOVERY_TIMEOUT_SECONDS * 1000);
82 } catch (IOException e) {
83 logger.debug("Error while trying to discover broadlink devices: {}", e.getMessage());
85 logger.debug("Discovery service found {} broadlink devices.", blDevices.length);
87 for (BLDevice dev : blDevices) {
88 logger.debug("Broadlink device {} of type {} with Host {} and MAC {}", dev.getDeviceDescription(),
89 Integer.toHexString(dev.getDeviceType()), dev.getHost(), dev.getMac());
92 String id = dev.getHost().replace(".", "-");
93 logger.debug("Device ID with IP address replacement: {}", id);
95 id = getHostnameWithoutDomain(InetAddress.getByName(dev.getHost()).getHostName());
96 logger.debug("Device ID with DNS name: {}", id);
97 } catch (UnknownHostException e) {
98 logger.debug("Discovered device with IP {} does not have a DNS name, using IP as thing UID.",
101 var deviceDescription = dev.getDeviceDescription();
102 switch (deviceDescription) {
103 case "Floureon Thermostat":
104 thingUID = new ThingUID(FLOUREON_THERMOSTAT_THING_TYPE, id);
106 case "Hysen Thermostat":
107 thingUID = new ThingUID(HYSEN_THERMOSTAT_THING_TYPE, id);
110 thingUID = new ThingUID(RM_UNIVERSAL_REMOTE_THING_TYPE, id);
113 logger.debug("Unknown device description '{}'.", deviceDescription);
117 Map<String, Object> properties = new HashMap<>();
118 properties.put(HOST, dev.getHost());
119 properties.put(Thing.PROPERTY_MAC_ADDRESS, dev.getMac().getMacString());
120 properties.put(DESCRIPTION, dev.getDeviceDescription());
122 logger.debug("Thing {} property map: {}", thingUID, properties);
124 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
125 .withLabel(dev.getDeviceDescription() + " (" + id + ")")
126 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
128 thingDiscovered(discoveryResult);
130 removeOlderResults(timestampOfLastScan);
134 protected void startScan() {
135 scheduler.execute(this::createScanner);
139 protected void startBackgroundDiscovery() {
140 logger.trace("Starting background scan for Broadlink devices");
141 ScheduledFuture<?> currentBackgroundDiscoveryFuture = backgroundDiscoveryFuture;
142 if (currentBackgroundDiscoveryFuture != null) {
143 currentBackgroundDiscoveryFuture.cancel(true);
145 backgroundDiscoveryFuture = scheduler.scheduleWithFixedDelay(this::createScanner, 0, 60, TimeUnit.SECONDS);
149 protected void stopBackgroundDiscovery() {
150 logger.trace("Stopping background scan for Broadlink devices");
152 ScheduledFuture<?> backgroundDiscoveryFuture = this.backgroundDiscoveryFuture;
153 if (backgroundDiscoveryFuture != null && !backgroundDiscoveryFuture.isCancelled()) {
154 if (backgroundDiscoveryFuture.cancel(true)) {
155 this.backgroundDiscoveryFuture = null;
161 private @Nullable InetAddress getIpAddress() {
162 return getIpFromNetworkAddressService().orElse(null);
166 * Uses openHAB's NetworkAddressService to determine the local primary network interface.
168 * @return local ip or <code>empty</code> if configured primary IP is not set or could not be parsed.
170 private Optional<InetAddress> getIpFromNetworkAddressService() {
171 String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
172 if (ipAddress == null) {
173 logger.warn("No network interface could be found.");
174 return Optional.empty();
177 return Optional.of(InetAddress.getByName(ipAddress));
178 } catch (UnknownHostException e) {
179 logger.warn("Configured primary IP cannot be parsed: {} Details: {}", ipAddress, e.getMessage());
180 return Optional.empty();
184 private String getHostnameWithoutDomain(String hostname) {
185 String broadlinkRegex = "BroadLink-OEM[-A-Za-z0-9]{12}.*";
186 if (hostname.matches(broadlinkRegex)) {
187 String[] dotSeparatedString = hostname.split("\\.");
188 logger.debug("Found original broadlink DNS name {}, removing domain", hostname);
189 return dotSeparatedString[0].replace(".", "-");
191 logger.debug("DNS name does not match original broadlink name: {}, using it without modification. ",
193 return hostname.replace(".", "-");