]> git.basschouten.com Git - openhab-addons.git/blob
4bcd9937c8a781525b5413de9c8b59dc736e3dd6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.plugwise.internal;
14
15 import static java.util.stream.Collectors.*;
16 import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
17
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.Iterator;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Set;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.plugwise.internal.handler.PlugwiseStickHandler;
31 import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
32 import org.openhab.binding.plugwise.internal.listener.PlugwiseStickStatusListener;
33 import org.openhab.binding.plugwise.internal.protocol.AnnounceAwakeRequestMessage;
34 import org.openhab.binding.plugwise.internal.protocol.InformationRequestMessage;
35 import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
36 import org.openhab.binding.plugwise.internal.protocol.Message;
37 import org.openhab.binding.plugwise.internal.protocol.RoleCallRequestMessage;
38 import org.openhab.binding.plugwise.internal.protocol.RoleCallResponseMessage;
39 import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
40 import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
41 import org.openhab.core.config.discovery.AbstractDiscoveryService;
42 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.ThingUID;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * Discovers Plugwise devices by periodically reading the Circle+ node/MAC table with {@link RoleCallRequestMessage}s.
52  * Sleeping end devices are discovered when they announce being awake with a {@link AnnounceAwakeRequestMessage}. To
53  * reduce network traffic {@link InformationRequestMessage}s are only sent to undiscovered devices.
54  *
55  * @author Wouter Born, Karel Goderis - Initial contribution
56  */
57 @NonNullByDefault
58 public class PlugwiseThingDiscoveryService extends AbstractDiscoveryService
59         implements PlugwiseMessageListener, PlugwiseStickStatusListener {
60
61     private static class CurrentRoleCall {
62         private boolean isRoleCalling;
63         private int currentNodeID;
64         private int attempts;
65         private long lastRequestMillis;
66     }
67
68     private static class DiscoveredNode {
69         private final MACAddress macAddress;
70         private final Map<String, String> properties = new HashMap<>();
71         private DeviceType deviceType = DeviceType.UNKNOWN;
72         private int attempts;
73         private long lastRequestMillis;
74
75         public DiscoveredNode(MACAddress macAddress) {
76             this.macAddress = macAddress;
77         }
78
79         public boolean isDataComplete() {
80             return deviceType != DeviceType.UNKNOWN && !properties.isEmpty();
81         }
82     }
83
84     private static final Set<ThingTypeUID> DISCOVERED_THING_TYPES_UIDS = SUPPORTED_THING_TYPES_UIDS.stream()
85             .filter(thingTypeUID -> !thingTypeUID.equals(THING_TYPE_STICK))
86             .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
87
88     private static final int MIN_NODE_ID = 0;
89     private static final int MAX_NODE_ID = 63;
90     private static final int DISCOVERY_INTERVAL = 180;
91
92     private static final int WATCH_INTERVAL = 1;
93
94     private static final int MESSAGE_TIMEOUT = 15;
95     private static final int MESSAGE_RETRY_ATTEMPTS = 5;
96
97     private final Logger logger = LoggerFactory.getLogger(PlugwiseThingDiscoveryService.class);
98
99     private final PlugwiseStickHandler stickHandler;
100
101     private @Nullable ScheduledFuture<?> discoveryJob;
102     private @Nullable ScheduledFuture<?> watchJob;
103     private CurrentRoleCall currentRoleCall = new CurrentRoleCall();
104
105     private final Map<MACAddress, DiscoveredNode> discoveredNodes = new ConcurrentHashMap<>();
106
107     public PlugwiseThingDiscoveryService(PlugwiseStickHandler stickHandler) throws IllegalArgumentException {
108         super(DISCOVERED_THING_TYPES_UIDS, 1, true);
109         this.stickHandler = stickHandler;
110         this.stickHandler.addStickStatusListener(this);
111     }
112
113     @Override
114     public synchronized void abortScan() {
115         logger.debug("Aborting nodes discovery");
116         super.abortScan();
117         currentRoleCall.isRoleCalling = false;
118         stopDiscoveryWatchJob();
119     }
120
121     public void activate() {
122         super.activate(new HashMap<>());
123     }
124
125     private void createDiscoveryResult(DiscoveredNode node) {
126         String mac = node.macAddress.toString();
127         ThingUID bridgeUID = stickHandler.getThing().getUID();
128         ThingTypeUID thingTypeUID = PlugwiseUtils.getThingTypeUID(node.deviceType);
129         if (thingTypeUID != null) {
130             ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, mac);
131
132             thingDiscovered(DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID)
133                     .withLabel("Plugwise " + node.deviceType.toString())
134                     .withProperty(PlugwiseBindingConstants.CONFIG_PROPERTY_MAC_ADDRESS, mac)
135                     .withProperties(new HashMap<>(node.properties))
136                     .withRepresentationProperty(PlugwiseBindingConstants.PROPERTY_MAC_ADDRESS).build());
137         }
138     }
139
140     @Override
141     protected void deactivate() {
142         super.deactivate();
143         stickHandler.removeMessageListener(this);
144         stickHandler.removeStickStatusListener(this);
145     }
146
147     private void discoverNewNodeDetails(MACAddress macAddress) {
148         if (!isAlreadyDiscovered(macAddress)) {
149             logger.debug("Discovered new node ({})", macAddress);
150             discoveredNodes.put(macAddress, new DiscoveredNode(macAddress));
151             updateInformation(macAddress);
152         } else {
153             logger.debug("Already discovered node ({})", macAddress);
154         }
155     }
156
157     protected void discoverNodes() {
158         MACAddress circlePlusMAC = getCirclePlusMAC();
159         if (getStickStatus() != ThingStatus.ONLINE) {
160             logger.debug("Discovery with role call not possible (Stick status is {})", getStickStatus());
161         } else if (circlePlusMAC == null) {
162             logger.debug("Discovery with role call not possible (Circle+ MAC address is null)");
163         } else if (currentRoleCall.isRoleCalling) {
164             logger.debug("Discovery with role call not possible (already role calling)");
165         } else {
166             stickHandler.addMessageListener(this);
167             discoveredNodes.clear();
168             currentRoleCall.isRoleCalling = true;
169             currentRoleCall.currentNodeID = Integer.MIN_VALUE;
170
171             discoverNewNodeDetails(circlePlusMAC);
172
173             logger.debug("Discovering nodes with role call on Circle+ ({})", circlePlusMAC);
174             roleCall(MIN_NODE_ID);
175             startDiscoveryWatchJob();
176         }
177     }
178
179     private @Nullable MACAddress getCirclePlusMAC() {
180         return stickHandler.getCirclePlusMAC();
181     }
182
183     private ThingStatus getStickStatus() {
184         return stickHandler.getThing().getStatus();
185     }
186
187     private void handleAnnounceAwakeRequest(AnnounceAwakeRequestMessage message) {
188         discoverNewNodeDetails(message.getMACAddress());
189     }
190
191     private void handleInformationResponse(InformationResponseMessage message) {
192         MACAddress mac = message.getMACAddress();
193         DiscoveredNode node = discoveredNodes.get(mac);
194         if (node != null) {
195             node.deviceType = message.getDeviceType();
196             PlugwiseUtils.updateProperties(node.properties, message);
197             if (node.isDataComplete()) {
198                 createDiscoveryResult(node);
199                 discoveredNodes.remove(mac);
200                 logger.debug("Finished discovery of {} ({})", node.deviceType, mac);
201             }
202         } else {
203             logger.debug("Received information response for already discovered node ({})", mac);
204         }
205     }
206
207     @Override
208     public void handleReponseMessage(Message message) {
209         switch (message.getType()) {
210             case ANNOUNCE_AWAKE_REQUEST:
211                 handleAnnounceAwakeRequest((AnnounceAwakeRequestMessage) message);
212                 break;
213             case DEVICE_INFORMATION_RESPONSE:
214                 handleInformationResponse((InformationResponseMessage) message);
215                 break;
216             case DEVICE_ROLE_CALL_RESPONSE:
217                 handleRoleCallResponse((RoleCallResponseMessage) message);
218                 break;
219             default:
220                 logger.trace("Received unhandled {} message from {}", message.getType(), message.getMACAddress());
221                 break;
222         }
223     }
224
225     private void handleRoleCallResponse(RoleCallResponseMessage message) {
226         logger.debug("Node with ID {} has MAC address: {}", message.getNodeID(), message.getNodeMAC());
227
228         if (message.getNodeID() <= MAX_NODE_ID && (message.getNodeMAC() != null)) {
229             discoverNewNodeDetails(message.getNodeMAC());
230             // Check if there is any other on the network
231             int nextNodeID = message.getNodeID() + 1;
232             if (nextNodeID <= MAX_NODE_ID) {
233                 roleCall(nextNodeID);
234             } else {
235                 currentRoleCall.isRoleCalling = false;
236             }
237         } else {
238             currentRoleCall.isRoleCalling = false;
239         }
240
241         if (!currentRoleCall.isRoleCalling) {
242             logger.debug("Finished discovering devices with role call on Circle+ ({})", getCirclePlusMAC());
243         }
244     }
245
246     private boolean isAlreadyDiscovered(MACAddress macAddress) {
247         Thing thing = stickHandler.getThingByMAC(macAddress);
248         if (thing != null) {
249             logger.debug("Node ({}) has existing thing: {}", macAddress, thing.getUID());
250         }
251         return thing != null;
252     }
253
254     /**
255      * Role calling is basically asking the Circle+ to return all the devices known to it. Up to 64 devices
256      * are supported in a Plugwise network, and role calling is done by sequentially sending
257      * {@link RoleCallRequestMessage} for all possible IDs in the network (0 <= ID <= 63)
258      *
259      * @param nodeID of the device to role call
260      */
261     private void roleCall(int nodeID) {
262         if (MIN_NODE_ID <= nodeID && nodeID <= MAX_NODE_ID) {
263             sendMessage(new RoleCallRequestMessage(getCirclePlusMAC(), nodeID));
264             if (nodeID != currentRoleCall.currentNodeID) {
265                 currentRoleCall.attempts = 0;
266             } else {
267                 currentRoleCall.attempts++;
268             }
269             currentRoleCall.currentNodeID = nodeID;
270             currentRoleCall.lastRequestMillis = System.currentTimeMillis();
271         } else {
272             logger.warn("Invalid node ID for role call: {}", nodeID);
273         }
274     }
275
276     private void sendMessage(Message message) {
277         stickHandler.sendMessage(message, PlugwiseMessagePriority.UPDATE_AND_DISCOVERY);
278     }
279
280     @Override
281     protected void startBackgroundDiscovery() {
282         logger.debug("Starting Plugwise device background discovery");
283
284         Runnable discoveryRunnable = () -> {
285             logger.debug("Discover nodes (background discovery)");
286             discoverNodes();
287         };
288
289         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
290         if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
291             discoveryJob = scheduler.scheduleWithFixedDelay(discoveryRunnable, 0, DISCOVERY_INTERVAL, TimeUnit.SECONDS);
292         }
293     }
294
295     private void startDiscoveryWatchJob() {
296         logger.debug("Starting Plugwise discovery watch job");
297
298         Runnable watchRunnable = () -> {
299             if (currentRoleCall.isRoleCalling) {
300                 if ((System.currentTimeMillis() - currentRoleCall.lastRequestMillis) > (MESSAGE_TIMEOUT * 1000)
301                         && currentRoleCall.attempts < MESSAGE_RETRY_ATTEMPTS) {
302                     logger.debug("Resending timed out role call message for node with ID {} on Circle+ ({})",
303                             currentRoleCall.currentNodeID, getCirclePlusMAC());
304                     roleCall(currentRoleCall.currentNodeID);
305                 } else if (currentRoleCall.attempts >= MESSAGE_RETRY_ATTEMPTS) {
306                     logger.debug("Giving up on role call for node with ID {} on Circle+ ({})",
307                             currentRoleCall.currentNodeID, getCirclePlusMAC());
308                     currentRoleCall.isRoleCalling = false;
309                 }
310             }
311
312             Iterator<Entry<MACAddress, DiscoveredNode>> it = discoveredNodes.entrySet().iterator();
313             while (it.hasNext()) {
314                 Entry<MACAddress, DiscoveredNode> entry = it.next();
315                 DiscoveredNode node = entry.getValue();
316                 if (System.currentTimeMillis() - node.lastRequestMillis > (MESSAGE_TIMEOUT * 1000)
317                         && node.attempts < MESSAGE_RETRY_ATTEMPTS) {
318                     logger.debug("Resending timed out information request message to node ({})", node.macAddress);
319                     updateInformation(node.macAddress);
320                     node.attempts++;
321                 } else if (node.attempts >= MESSAGE_RETRY_ATTEMPTS) {
322                     logger.debug("Giving up on information request for node ({})", node.macAddress);
323                     it.remove();
324                 }
325             }
326
327             if (!currentRoleCall.isRoleCalling && discoveredNodes.isEmpty()) {
328                 logger.debug("Discovery no longer needs to be watched");
329                 stopDiscoveryWatchJob();
330             }
331         };
332
333         ScheduledFuture<?> localWatchJob = watchJob;
334         if (localWatchJob == null || localWatchJob.isCancelled()) {
335             watchJob = scheduler.scheduleWithFixedDelay(watchRunnable, WATCH_INTERVAL, WATCH_INTERVAL,
336                     TimeUnit.SECONDS);
337         }
338     }
339
340     @Override
341     protected void startScan() {
342         logger.debug("Discover nodes (manual discovery)");
343         discoverNodes();
344     }
345
346     @Override
347     public void stickStatusChanged(ThingStatus status) {
348         if (status.equals(ThingStatus.ONLINE)) {
349             logger.debug("Discover nodes (Stick online)");
350             discoverNodes();
351         }
352     }
353
354     @Override
355     protected void stopBackgroundDiscovery() {
356         logger.debug("Stopping Plugwise device background discovery");
357         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
358         if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
359             localDiscoveryJob.cancel(true);
360             discoveryJob = null;
361         }
362         stopDiscoveryWatchJob();
363     }
364
365     private void stopDiscoveryWatchJob() {
366         logger.debug("Stopping Plugwise discovery watch job");
367         ScheduledFuture<?> localWatchJob = watchJob;
368         if (localWatchJob != null && !localWatchJob.isCancelled()) {
369             localWatchJob.cancel(true);
370             watchJob = null;
371         }
372     }
373
374     private void updateInformation(MACAddress macAddress) {
375         sendMessage(new InformationRequestMessage(macAddress));
376     }
377 }