]> git.basschouten.com Git - openhab-addons.git/blob
15f17ac0b31096b4a499078372583104ff8f2e67
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 package org.openhab.binding.bigassfan.internal.discovery;
14
15 import static org.openhab.binding.bigassfan.internal.BigAssFanBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.SocketException;
19 import java.net.SocketTimeoutException;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Set;
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;
29
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;
40
41 /**
42  * The {@link BigAssFanDiscoveryService} class implements a service
43  * for discovering the Big Ass Fans.
44  *
45  * @author Mark Hilbush - Initial contribution
46  */
47 @Component(service = DiscoveryService.class, configurationPid = "discovery.bigassfan")
48 public class BigAssFanDiscoveryService extends AbstractDiscoveryService {
49     private final Logger logger = LoggerFactory.getLogger(BigAssFanDiscoveryService.class);
50
51     private static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
52     private static final long BACKGROUND_DISCOVERY_DELAY = 8L;
53
54     // Our own thread pool for the long-running listener job
55     private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
56     private ScheduledFuture<?> listenerJob;
57
58     DiscoveryListener discoveryListener;
59
60     private boolean terminate;
61
62     private final Pattern announcementPattern = Pattern.compile("[(](.*);DEVICE;ID;(.*);(.*)[)]");
63
64     private Runnable listenerRunnable = () -> {
65         try {
66             listen();
67         } catch (RuntimeException e) {
68             logger.warn("Discovery listener got unexpected exception: {}", e.getMessage(), e);
69         }
70     };
71
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;
76
77     public BigAssFanDiscoveryService() {
78         super(SUPPORTED_THING_TYPES_UIDS, 0, BACKGROUND_DISCOVERY_ENABLED);
79     }
80
81     @Override
82     public Set<ThingTypeUID> getSupportedThingTypes() {
83         return SUPPORTED_THING_TYPES_UIDS;
84     }
85
86     @Override
87     protected void activate(Map<String, Object> configProperties) {
88         super.activate(configProperties);
89         logger.trace("BigAssFan discovery service ACTIVATED");
90     }
91
92     @Override
93     protected void deactivate() {
94         super.deactivate();
95         logger.trace("BigAssFan discovery service DEACTIVATED");
96     }
97
98     @Override
99     @Modified
100     protected void modified(Map<String, Object> configProperties) {
101         super.modified(configProperties);
102     }
103
104     @Override
105     protected void startBackgroundDiscovery() {
106         logger.debug("Starting background discovery");
107         startListenerJob();
108         schedulePollJob();
109     }
110
111     @Override
112     protected void stopBackgroundDiscovery() {
113         logger.debug("Stopping background discovery");
114         cancelPollJob();
115         cancelListenerJob();
116     }
117
118     private void startListenerJob() {
119         if (listenerJob == null) {
120             terminate = false;
121             logger.debug("Starting discovery listener job in {} seconds", BACKGROUND_DISCOVERY_DELAY);
122             listenerJob = scheduledExecutorService.schedule(listenerRunnable, BACKGROUND_DISCOVERY_DELAY,
123                     TimeUnit.SECONDS);
124         }
125     }
126
127     private void cancelListenerJob() {
128         if (listenerJob != null) {
129             logger.debug("Canceling discovery listener job");
130             listenerJob.cancel(true);
131             terminate = true;
132             listenerJob = null;
133         }
134     }
135
136     @Override
137     public void startScan() {
138     }
139
140     @Override
141     public void stopScan() {
142     }
143
144     private synchronized void listen() {
145         logger.info("BigAssFan discovery service is running");
146
147         try {
148             discoveryListener = new DiscoveryListener();
149         } catch (SocketException se) {
150             logger.warn("Got Socket exception creating multicast socket: {}", se.getMessage(), se);
151             return;
152         } catch (IOException ioe) {
153             logger.warn("Got IO exception creating multicast socket: {}", ioe.getMessage(), ioe);
154             return;
155         }
156
157         logger.debug("Waiting for discovery messages");
158         while (!terminate) {
159             try {
160                 // Wait for a discovery message
161                 processMessage(discoveryListener.waitForMessage());
162             } catch (SocketTimeoutException e) {
163                 // Read on socket timed out; check for termination
164                 continue;
165             } catch (IOException ioe) {
166                 logger.warn("Got IO exception waiting for message: {}", ioe.getMessage(), ioe);
167                 break;
168             }
169         }
170         discoveryListener.shutdown();
171         logger.debug("DiscoveryListener job is exiting");
172     }
173
174     private void processMessage(BigAssFanDevice device) {
175         if (device == null) {
176             return;
177         }
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));
181
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) {
187                 case 2:
188                     // L-Series fans
189                     device.setType(modelParts[0]);
190                     device.setModel(modelParts[1]);
191                     deviceDiscovered(device);
192                     break;
193                 case 3:
194                     // H-Series fans
195                     device.setType(modelParts[0]);
196                     device.setModel(modelParts[2]);
197                     deviceDiscovered(device);
198                     break;
199                 default:
200                     logger.info("Unable to extract device type from discovery message");
201                     break;
202             }
203         }
204     }
205
206     private synchronized void deviceDiscovered(BigAssFanDevice device) {
207         logger.debug("Device discovered: {}", device);
208
209         ThingTypeUID thingTypeUid;
210
211         if (device.isSwitch()) {
212             logger.debug("Add controller with IP={}, MAC={}, MODEL={}", device.getIpAddress(), device.getMacAddress(),
213                     device.getModel());
214             thingTypeUid = THING_TYPE_CONTROLLER;
215         } else if (device.isFan()) {
216             logger.debug("Add fan with IP={}, MAC={}, MODEL={}", device.getIpAddress(), device.getMacAddress(),
217                     device.getModel());
218             thingTypeUid = THING_TYPE_FAN;
219         } else if (device.isLight()) {
220             logger.debug("Add light with IP={}, MAC={}, MODEL={}", device.getIpAddress(), device.getMacAddress(),
221                     device.getModel());
222             thingTypeUid = THING_TYPE_LIGHT;
223         } else {
224             logger.info("Discovered unknown device type {} at IP={}", device.getModel(), device.getIpAddress());
225             return;
226         }
227
228         // We got a valid discovery message. Process it as a potential new thing
229         String serialNumber = device.getMacAddress().replace(":", "");
230
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");
238
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());
243     }
244
245     private void schedulePollJob() {
246         logger.debug("Scheduling discovery poll job to run every {} seconds starting in {} sec", POLL_FREQ, POLL_DELAY);
247         cancelPollJob();
248         pollJob = scheduler.scheduleWithFixedDelay(() -> {
249             try {
250                 discoveryListener.pollForDevices();
251             } catch (RuntimeException e) {
252                 logger.warn("Poll job got unexpected exception: {}", e.getMessage(), e);
253             }
254         }, POLL_DELAY, POLL_FREQ, TimeUnit.SECONDS);
255     }
256
257     private void cancelPollJob() {
258         if (pollJob != null) {
259             logger.debug("Canceling poll job");
260             pollJob.cancel(true);
261             pollJob = null;
262         }
263     }
264 }