]> git.basschouten.com Git - openhab-addons.git/blob
107881b36c96a963c3a2d256ac686be259c844cd
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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;
42
43 /**
44  * The {@link BigAssFanDiscoveryService} class implements a service
45  * for discovering the Big Ass Fans.
46  *
47  * @author Mark Hilbush - Initial contribution
48  */
49 @NonNullByDefault
50 @Component(service = DiscoveryService.class, configurationPid = "discovery.bigassfan")
51 public class BigAssFanDiscoveryService extends AbstractDiscoveryService {
52     private final Logger logger = LoggerFactory.getLogger(BigAssFanDiscoveryService.class);
53
54     private static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
55     private static final long BACKGROUND_DISCOVERY_DELAY = 8L;
56
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;(.*);(.*)[)]");
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 static final long POLL_FREQ = 300L;
74     private static final long POLL_DELAY = 12L;
75     private @Nullable 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(@Nullable 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(@Nullable 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 synchronized void startListenerJob() {
119         if (this.listenerJob == null) {
120             logger.debug("Starting discovery listener job in {} seconds", BACKGROUND_DISCOVERY_DELAY);
121             terminate = false;
122             this.listenerJob = scheduledExecutorService.schedule(listenerRunnable, BACKGROUND_DISCOVERY_DELAY,
123                     TimeUnit.SECONDS);
124         }
125     }
126
127     private void cancelListenerJob() {
128         ScheduledFuture<?> localListenerJob = this.listenerJob;
129         if (localListenerJob != null) {
130             logger.debug("Canceling discovery listener job");
131             localListenerJob.cancel(true);
132             terminate = true;
133             this.listenerJob = null;
134         }
135     }
136
137     @Override
138     public void startScan() {
139     }
140
141     @Override
142     public void stopScan() {
143     }
144
145     private synchronized void listen() {
146         logger.info("BigAssFan discovery service is running");
147         DiscoveryListener localDiscoveryListener;
148
149         try {
150             localDiscoveryListener = new DiscoveryListener();
151             discoveryListener = localDiscoveryListener;
152         } catch (SocketException se) {
153             logger.warn("Got Socket exception creating multicast socket: {}", se.getMessage(), se);
154             return;
155         } catch (IOException ioe) {
156             logger.warn("Got IO exception creating multicast socket: {}", ioe.getMessage(), ioe);
157             return;
158         }
159
160         logger.debug("Waiting for discovery messages");
161         while (!terminate) {
162             try {
163                 // Wait for a discovery message
164                 processMessage(localDiscoveryListener.waitForMessage());
165             } catch (SocketTimeoutException e) {
166                 // Read on socket timed out; check for termination
167                 continue;
168             } catch (IOException ioe) {
169                 logger.warn("Got IO exception waiting for message: {}", ioe.getMessage(), ioe);
170                 break;
171             }
172         }
173         localDiscoveryListener.shutdown();
174         logger.debug("DiscoveryListener job is exiting");
175     }
176
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));
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 synchronized void schedulePollJob() {
246         cancelPollJob();
247         if (this.pollJob == null) {
248             logger.debug("Scheduling discovery poll job to run every {} seconds starting in {} sec", POLL_FREQ,
249                     POLL_DELAY);
250             pollJob = scheduler.scheduleWithFixedDelay(() -> {
251                 try {
252                     DiscoveryListener localListener = discoveryListener;
253                     if (localListener != null) {
254                         localListener.pollForDevices();
255                     }
256                 } catch (RuntimeException e) {
257                     logger.warn("Poll job got unexpected exception: {}", e.getMessage(), e);
258                 }
259             }, POLL_DELAY, POLL_FREQ, TimeUnit.SECONDS);
260         }
261     }
262
263     private void cancelPollJob() {
264         ScheduledFuture<?> localPollJob = pollJob;
265         if (localPollJob != null) {
266             logger.debug("Canceling poll job");
267             localPollJob.cancel(true);
268             this.pollJob = null;
269         }
270     }
271 }