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.bigassfan.internal.discovery;
15 import static org.openhab.binding.bigassfan.internal.BigAssFanBindingConstants.*;
17 import java.io.IOException;
18 import java.net.SocketException;
19 import java.net.SocketTimeoutException;
20 import java.util.HashMap;
23 import java.util.concurrent.Executors;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.core.config.discovery.AbstractDiscoveryService;
33 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
34 import org.openhab.core.config.discovery.DiscoveryService;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.thing.ThingUID;
38 import org.osgi.service.component.annotations.Component;
39 import org.osgi.service.component.annotations.Modified;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * The {@link BigAssFanDiscoveryService} class implements a service
45 * for discovering the Big Ass Fans.
47 * @author Mark Hilbush - Initial contribution
50 @Component(service = DiscoveryService.class, configurationPid = "discovery.bigassfan")
51 public class BigAssFanDiscoveryService extends AbstractDiscoveryService {
52 private final Logger logger = LoggerFactory.getLogger(BigAssFanDiscoveryService.class);
54 private static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
55 private static final long BACKGROUND_DISCOVERY_DELAY = 8L;
57 // Our own thread pool for the long-running listener job
58 private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
59 private @Nullable ScheduledFuture<?> listenerJob;
60 private @Nullable DiscoveryListener discoveryListener;
61 private boolean terminate;
62 private final Pattern announcementPattern = Pattern.compile("[(](.*);DEVICE;ID;(.*);(.*)[)]");
64 private Runnable listenerRunnable = () -> {
67 } catch (RuntimeException e) {
68 logger.warn("Discovery listener got unexpected exception: {}", e.getMessage(), e);
72 // Frequency (in seconds) with which we poll for new devices
73 private static final long POLL_FREQ = 300L;
74 private static final long POLL_DELAY = 12L;
75 private @Nullable ScheduledFuture<?> pollJob;
77 public BigAssFanDiscoveryService() {
78 super(SUPPORTED_THING_TYPES_UIDS, 0, BACKGROUND_DISCOVERY_ENABLED);
82 public Set<ThingTypeUID> getSupportedThingTypes() {
83 return SUPPORTED_THING_TYPES_UIDS;
87 protected void activate(@Nullable Map<String, Object> configProperties) {
88 super.activate(configProperties);
89 logger.trace("BigAssFan discovery service ACTIVATED");
93 protected void deactivate() {
95 logger.trace("BigAssFan discovery service DEACTIVATED");
100 protected void modified(@Nullable Map<String, Object> configProperties) {
101 super.modified(configProperties);
105 protected void startBackgroundDiscovery() {
106 logger.debug("Starting background discovery");
112 protected void stopBackgroundDiscovery() {
113 logger.debug("Stopping background discovery");
118 private synchronized void startListenerJob() {
119 if (this.listenerJob == null) {
120 logger.debug("Starting discovery listener job in {} seconds", BACKGROUND_DISCOVERY_DELAY);
122 this.listenerJob = scheduledExecutorService.schedule(listenerRunnable, BACKGROUND_DISCOVERY_DELAY,
127 private void cancelListenerJob() {
128 ScheduledFuture<?> localListenerJob = this.listenerJob;
129 if (localListenerJob != null) {
130 logger.debug("Canceling discovery listener job");
131 localListenerJob.cancel(true);
133 this.listenerJob = null;
138 public void startScan() {
142 public void stopScan() {
145 private synchronized void listen() {
146 logger.info("BigAssFan discovery service is running");
147 DiscoveryListener localDiscoveryListener;
150 localDiscoveryListener = new DiscoveryListener();
151 discoveryListener = localDiscoveryListener;
152 } catch (SocketException se) {
153 logger.warn("Got Socket exception creating multicast socket: {}", se.getMessage(), se);
155 } catch (IOException ioe) {
156 logger.warn("Got IO exception creating multicast socket: {}", ioe.getMessage(), ioe);
160 logger.debug("Waiting for discovery messages");
163 // Wait for a discovery message
164 processMessage(localDiscoveryListener.waitForMessage());
165 } catch (SocketTimeoutException e) {
166 // Read on socket timed out; check for termination
168 } catch (IOException ioe) {
169 logger.warn("Got IO exception waiting for message: {}", ioe.getMessage(), ioe);
173 localDiscoveryListener.shutdown();
174 logger.debug("DiscoveryListener job is exiting");
177 private void processMessage(BigAssFanDevice device) {
178 Matcher matcher = announcementPattern.matcher(device.getDiscoveryMessage());
179 if (matcher.find()) {
180 logger.debug("Match: grp1={}, grp2={}, grp(3)={}", matcher.group(1), matcher.group(2), matcher.group(3));
182 // Extract needed information from the discovery message
183 device.setLabel(matcher.group(1));
184 device.setMacAddress(matcher.group(2));
185 String[] modelParts = matcher.group(3).split(",");
186 switch (modelParts.length) {
189 device.setType(modelParts[0]);
190 device.setModel(modelParts[1]);
191 deviceDiscovered(device);
195 device.setType(modelParts[0]);
196 device.setModel(modelParts[2]);
197 deviceDiscovered(device);
200 logger.info("Unable to extract device type from discovery message");
206 private synchronized void deviceDiscovered(BigAssFanDevice device) {
207 logger.debug("Device discovered: {}", device);
209 ThingTypeUID thingTypeUid;
211 if (device.isSwitch()) {
212 logger.debug("Add controller with IP={}, MAC={}, MODEL={}", device.getIpAddress(), device.getMacAddress(),
214 thingTypeUid = THING_TYPE_CONTROLLER;
215 } else if (device.isFan()) {
216 logger.debug("Add fan with IP={}, MAC={}, MODEL={}", device.getIpAddress(), device.getMacAddress(),
218 thingTypeUid = THING_TYPE_FAN;
219 } else if (device.isLight()) {
220 logger.debug("Add light with IP={}, MAC={}, MODEL={}", device.getIpAddress(), device.getMacAddress(),
222 thingTypeUid = THING_TYPE_LIGHT;
224 logger.info("Discovered unknown device type {} at IP={}", device.getModel(), device.getIpAddress());
228 // We got a valid discovery message. Process it as a potential new thing
229 String serialNumber = device.getMacAddress().replace(":", "");
231 Map<String, Object> properties = new HashMap<>();
232 properties.put(THING_PROPERTY_MAC, device.getMacAddress());
233 properties.put(THING_PROPERTY_IP, device.getIpAddress());
234 properties.put(THING_PROPERTY_LABEL, device.getLabel());
235 properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
236 properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
237 properties.put(Thing.PROPERTY_VENDOR, "Haiku");
239 ThingUID uid = new ThingUID(thingTypeUid, serialNumber);
240 logger.debug("Creating discovery result for UID={}, IP={}", uid, device.getIpAddress());
241 thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties)
242 .withRepresentationProperty(THING_PROPERTY_MAC).withLabel(device.getLabel()).build());
245 private synchronized void schedulePollJob() {
247 if (this.pollJob == null) {
248 logger.debug("Scheduling discovery poll job to run every {} seconds starting in {} sec", POLL_FREQ,
250 pollJob = scheduler.scheduleWithFixedDelay(() -> {
252 DiscoveryListener localListener = discoveryListener;
253 if (localListener != null) {
254 localListener.pollForDevices();
256 } catch (RuntimeException e) {
257 logger.warn("Poll job got unexpected exception: {}", e.getMessage(), e);
259 }, POLL_DELAY, POLL_FREQ, TimeUnit.SECONDS);
263 private void cancelPollJob() {
264 ScheduledFuture<?> localPollJob = pollJob;
265 if (localPollJob != null) {
266 logger.debug("Canceling poll job");
267 localPollJob.cancel(true);