2 * Copyright (c) 2010-2022 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.openhab.core.config.discovery.AbstractDiscoveryService;
31 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
32 import org.openhab.core.config.discovery.DiscoveryService;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingTypeUID;
35 import org.openhab.core.thing.ThingUID;
36 import org.osgi.service.component.annotations.Component;
37 import org.osgi.service.component.annotations.Modified;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * The {@link BigAssFanDiscoveryService} class implements a service
43 * for discovering the Big Ass Fans.
45 * @author Mark Hilbush - Initial contribution
47 @Component(service = DiscoveryService.class, configurationPid = "discovery.bigassfan")
48 public class BigAssFanDiscoveryService extends AbstractDiscoveryService {
49 private final Logger logger = LoggerFactory.getLogger(BigAssFanDiscoveryService.class);
51 private static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
52 private static final long BACKGROUND_DISCOVERY_DELAY = 8L;
54 // Our own thread pool for the long-running listener job
55 private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
56 private ScheduledFuture<?> listenerJob;
58 DiscoveryListener discoveryListener;
60 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 final long POLL_FREQ = 300L;
74 private final long POLL_DELAY = 12L;
75 private 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(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(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 void startListenerJob() {
119 if (listenerJob == null) {
121 logger.debug("Starting discovery listener job in {} seconds", BACKGROUND_DISCOVERY_DELAY);
122 listenerJob = scheduledExecutorService.schedule(listenerRunnable, BACKGROUND_DISCOVERY_DELAY,
127 private void cancelListenerJob() {
128 if (listenerJob != null) {
129 logger.debug("Canceling discovery listener job");
130 listenerJob.cancel(true);
137 public void startScan() {
141 public void stopScan() {
144 private synchronized void listen() {
145 logger.info("BigAssFan discovery service is running");
148 discoveryListener = new DiscoveryListener();
149 } catch (SocketException se) {
150 logger.warn("Got Socket exception creating multicast socket: {}", se.getMessage(), se);
152 } catch (IOException ioe) {
153 logger.warn("Got IO exception creating multicast socket: {}", ioe.getMessage(), ioe);
157 logger.debug("Waiting for discovery messages");
160 // Wait for a discovery message
161 processMessage(discoveryListener.waitForMessage());
162 } catch (SocketTimeoutException e) {
163 // Read on socket timed out; check for termination
165 } catch (IOException ioe) {
166 logger.warn("Got IO exception waiting for message: {}", ioe.getMessage(), ioe);
170 discoveryListener.shutdown();
171 logger.debug("DiscoveryListener job is exiting");
174 private void processMessage(BigAssFanDevice device) {
175 if (device == null) {
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 void schedulePollJob() {
246 logger.debug("Scheduling discovery poll job to run every {} seconds starting in {} sec", POLL_FREQ, POLL_DELAY);
248 pollJob = scheduler.scheduleWithFixedDelay(() -> {
250 discoveryListener.pollForDevices();
251 } catch (RuntimeException e) {
252 logger.warn("Poll job got unexpected exception: {}", e.getMessage(), e);
254 }, POLL_DELAY, POLL_FREQ, TimeUnit.SECONDS);
257 private void cancelPollJob() {
258 if (pollJob != null) {
259 logger.debug("Canceling poll job");
260 pollJob.cancel(true);