2 * Copyright (c) 2010-2020 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.plugwise.internal;
15 import static java.util.stream.Collectors.*;
16 import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.Iterator;
22 import java.util.Map.Entry;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
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;
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.
55 * @author Wouter Born, Karel Goderis - Initial contribution
58 public class PlugwiseThingDiscoveryService extends AbstractDiscoveryService
59 implements PlugwiseMessageListener, PlugwiseStickStatusListener {
61 private static class CurrentRoleCall {
62 private boolean isRoleCalling;
63 private int currentNodeID;
65 private long lastRequestMillis;
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;
73 private long lastRequestMillis;
75 public DiscoveredNode(MACAddress macAddress) {
76 this.macAddress = macAddress;
79 public boolean isDataComplete() {
80 return deviceType != DeviceType.UNKNOWN && !properties.isEmpty();
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));
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;
92 private static final int WATCH_INTERVAL = 1;
94 private static final int MESSAGE_TIMEOUT = 15;
95 private static final int MESSAGE_RETRY_ATTEMPTS = 5;
97 private final Logger logger = LoggerFactory.getLogger(PlugwiseThingDiscoveryService.class);
99 private final PlugwiseStickHandler stickHandler;
101 private @Nullable ScheduledFuture<?> discoveryJob;
102 private @Nullable ScheduledFuture<?> watchJob;
103 private CurrentRoleCall currentRoleCall = new CurrentRoleCall();
105 private final Map<MACAddress, @Nullable DiscoveredNode> discoveredNodes = new ConcurrentHashMap<>();
107 public PlugwiseThingDiscoveryService(PlugwiseStickHandler stickHandler) throws IllegalArgumentException {
108 super(DISCOVERED_THING_TYPES_UIDS, 1, true);
109 this.stickHandler = stickHandler;
110 this.stickHandler.addStickStatusListener(this);
114 public synchronized void abortScan() {
115 logger.debug("Aborting nodes discovery");
117 currentRoleCall.isRoleCalling = false;
118 stopDiscoveryWatchJob();
121 public void activate() {
122 super.activate(new HashMap<>());
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);
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());
141 protected void deactivate() {
143 stickHandler.removeMessageListener(this);
144 stickHandler.removeStickStatusListener(this);
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);
153 logger.debug("Already discovered node ({})", macAddress);
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)");
166 stickHandler.addMessageListener(this);
167 discoveredNodes.clear();
168 currentRoleCall.isRoleCalling = true;
169 currentRoleCall.currentNodeID = Integer.MIN_VALUE;
171 discoverNewNodeDetails(circlePlusMAC);
173 logger.debug("Discovering nodes with role call on Circle+ ({})", circlePlusMAC);
174 roleCall(MIN_NODE_ID);
175 startDiscoveryWatchJob();
179 private @Nullable MACAddress getCirclePlusMAC() {
180 return stickHandler.getCirclePlusMAC();
183 private ThingStatus getStickStatus() {
184 return stickHandler.getThing().getStatus();
187 private void handleAnnounceAwakeRequest(AnnounceAwakeRequestMessage message) {
188 discoverNewNodeDetails(message.getMACAddress());
191 private void handleInformationResponse(InformationResponseMessage message) {
192 MACAddress mac = message.getMACAddress();
193 DiscoveredNode node = discoveredNodes.get(mac);
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);
203 logger.debug("Received information response for already discovered node ({})", mac);
208 public void handleReponseMessage(Message message) {
209 switch (message.getType()) {
210 case ANNOUNCE_AWAKE_REQUEST:
211 handleAnnounceAwakeRequest((AnnounceAwakeRequestMessage) message);
213 case DEVICE_INFORMATION_RESPONSE:
214 handleInformationResponse((InformationResponseMessage) message);
216 case DEVICE_ROLE_CALL_RESPONSE:
217 handleRoleCallResponse((RoleCallResponseMessage) message);
220 logger.trace("Received unhandled {} message from {}", message.getType(), message.getMACAddress());
225 private void handleRoleCallResponse(RoleCallResponseMessage message) {
226 logger.debug("Node with ID {} has MAC address: {}", message.getNodeID(), message.getNodeMAC());
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);
235 currentRoleCall.isRoleCalling = false;
238 currentRoleCall.isRoleCalling = false;
241 if (!currentRoleCall.isRoleCalling) {
242 logger.debug("Finished discovering devices with role call on Circle+ ({})", getCirclePlusMAC());
246 private boolean isAlreadyDiscovered(MACAddress macAddress) {
247 Thing thing = stickHandler.getThingByMAC(macAddress);
249 logger.debug("Node ({}) has existing thing: {}", macAddress, thing.getUID());
251 return thing != null;
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)
259 * @param nodeID of the device to role call
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;
267 currentRoleCall.attempts++;
269 currentRoleCall.currentNodeID = nodeID;
270 currentRoleCall.lastRequestMillis = System.currentTimeMillis();
272 logger.warn("Invalid node ID for role call: {}", nodeID);
276 private void sendMessage(Message message) {
277 stickHandler.sendMessage(message, PlugwiseMessagePriority.UPDATE_AND_DISCOVERY);
281 protected void startBackgroundDiscovery() {
282 logger.debug("Starting Plugwise device background discovery");
284 Runnable discoveryRunnable = () -> {
285 logger.debug("Discover nodes (background discovery)");
289 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
290 if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
291 discoveryJob = scheduler.scheduleWithFixedDelay(discoveryRunnable, 0, DISCOVERY_INTERVAL, TimeUnit.SECONDS);
295 private void startDiscoveryWatchJob() {
296 logger.debug("Starting Plugwise discovery watch job");
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;
312 Iterator<Entry<MACAddress, @Nullable DiscoveredNode>> it = discoveredNodes.entrySet().iterator();
313 while (it.hasNext()) {
314 Entry<MACAddress, @Nullable DiscoveredNode> entry = it.next();
315 DiscoveredNode node = entry.getValue();
316 if (node != null && (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);
321 } else if (node != null && node.attempts >= MESSAGE_RETRY_ATTEMPTS) {
322 logger.debug("Giving up on information request for node ({})", node.macAddress);
327 if (!currentRoleCall.isRoleCalling && discoveredNodes.isEmpty()) {
328 logger.debug("Discovery no longer needs to be watched");
329 stopDiscoveryWatchJob();
333 ScheduledFuture<?> localWatchJob = watchJob;
334 if (localWatchJob == null || localWatchJob.isCancelled()) {
335 watchJob = scheduler.scheduleWithFixedDelay(watchRunnable, WATCH_INTERVAL, WATCH_INTERVAL,
341 protected void startScan() {
342 logger.debug("Discover nodes (manual discovery)");
347 public void stickStatusChanged(ThingStatus status) {
348 if (status.equals(ThingStatus.ONLINE)) {
349 logger.debug("Discover nodes (Stick online)");
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);
362 stopDiscoveryWatchJob();
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);
374 private void updateInformation(MACAddress macAddress) {
375 sendMessage(new InformationRequestMessage(macAddress));