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.lcn.internal;
15 import java.util.Collections;
16 import java.util.HashMap;
17 import java.util.LinkedList;
18 import java.util.List;
20 import java.util.Queue;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ConcurrentLinkedQueue;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
29 import java.util.stream.Stream;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.lcn.internal.common.LcnAddrMod;
34 import org.openhab.binding.lcn.internal.connection.Connection;
35 import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaAckSubHandler;
36 import org.openhab.binding.lcn.internal.subhandler.LcnModuleMetaFirmwareSubHandler;
37 import org.openhab.core.config.discovery.AbstractDiscoveryService;
38 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
39 import org.openhab.core.config.discovery.DiscoveryService;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingTypeUID;
42 import org.openhab.core.thing.ThingUID;
43 import org.openhab.core.thing.binding.ThingHandler;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * Scans all LCN segments for LCN modules.
52 * 1. Send "Leerkomando" to the broadcast address with request for Ack set
53 * 2. For every received Ack, send the following requests to the module:
54 * - serial number request (SN)
55 * - module's name first part request (NM1)
56 * - module's name second part request (NM2)
57 * 3. When all three messages have been received, fire thingDiscovered()
59 * @author Fabian Wolter - Initial Contribution
62 public class LcnModuleDiscoveryService extends AbstractDiscoveryService
63 implements DiscoveryService, ThingHandlerService {
64 private final Logger logger = LoggerFactory.getLogger(LcnModuleDiscoveryService.class);
65 private static final Pattern NAME_PATTERN = Pattern
66 .compile("=M(?<segId>\\d{3})(?<modId>\\d{3}).N(?<part>[1-2]{1})(?<name>.*)");
67 private static final String SEGMENT_ID = "segmentId";
68 private static final String MODULE_ID = "moduleId";
69 private static final int MODULE_NAME_PART_COUNT = 2;
70 private static final int DISCOVERY_TIMEOUT_SEC = 90;
71 private static final int ACK_TIMEOUT_MS = 1000;
72 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
73 .unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_MODULE).collect(Collectors.toSet()));
74 private @Nullable PckGatewayHandler bridgeHandler;
75 private final Map<LcnAddrMod, @Nullable Map<Integer, String>> moduleNames = new HashMap<>();
76 private final Map<LcnAddrMod, DiscoveryResultBuilder> discoveryResultBuilders = new ConcurrentHashMap<>();
77 private final List<LcnAddrMod> successfullyDiscovered = new LinkedList<>();
78 private final Queue<@Nullable LcnAddrMod> serialNumberRequestQueue = new ConcurrentLinkedQueue<>();
79 private final Queue<@Nullable LcnAddrMod> moduleNameRequestQueue = new ConcurrentLinkedQueue<>();
80 private @Nullable volatile ScheduledFuture<?> queueProcessor;
81 private @Nullable ScheduledFuture<?> builderTask;
83 public LcnModuleDiscoveryService() {
84 super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SEC, false);
88 public void setThingHandler(@Nullable ThingHandler handler) {
89 if (handler instanceof PckGatewayHandler) {
90 this.bridgeHandler = (PckGatewayHandler) handler;
95 public @Nullable ThingHandler getThingHandler() {
100 public void deactivate() {
106 protected void startScan() {
107 synchronized (this) {
108 PckGatewayHandler localBridgeHandler = bridgeHandler;
109 if (localBridgeHandler == null) {
110 logger.warn("Bridge handler not set");
114 ScheduledFuture<?> localBuilderTask = builderTask;
115 if (localBridgeHandler.getConnection() == null && localBuilderTask != null) {
116 localBuilderTask.cancel(true);
119 localBridgeHandler.registerPckListener(data -> {
122 if ((matcher = LcnModuleMetaAckSubHandler.PATTERN_POS.matcher(data)).matches()
123 || (matcher = LcnModuleMetaFirmwareSubHandler.PATTERN.matcher(data)).matches()
124 || (matcher = NAME_PATTERN.matcher(data)).matches()) {
125 synchronized (LcnModuleDiscoveryService.this) {
126 Connection connection = localBridgeHandler.getConnection();
128 if (connection == null) {
132 LcnAddrMod addr = new LcnAddrMod(
133 localBridgeHandler.toLogicalSegmentId(Integer.parseInt(matcher.group("segId"))),
134 Integer.parseInt(matcher.group("modId")));
136 if (matcher.pattern() == LcnModuleMetaAckSubHandler.PATTERN_POS) {
137 // Received an ACK frame
139 // The module could send an Ack with a response to another command. So, ignore the Ack, when
140 // we received our data already.
141 if (!discoveryResultBuilders.containsKey(addr)) {
142 serialNumberRequestQueue.add(addr);
143 rescheduleQueueProcessor(); // delay request of serial until all modules finished ACKing
146 Map<Integer, String> localNameParts = moduleNames.get(addr);
147 if (localNameParts == null || localNameParts.size() != MODULE_NAME_PART_COUNT) {
148 moduleNameRequestQueue.add(addr);
149 rescheduleQueueProcessor(); // delay request of names until all modules finished ACKing
151 } else if (matcher.pattern() == LcnModuleMetaFirmwareSubHandler.PATTERN) {
152 // Received a firmware version info frame
154 ThingUID bridgeUid = localBridgeHandler.getThing().getUID();
155 String serialNumber = matcher.group("sn");
157 String thingID = String.format("S%03dM%03d", addr.getSegmentId(), addr.getModuleId());
159 ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_MODULE, bridgeUid, thingID);
161 Map<String, Object> properties = new HashMap<>(3);
162 properties.put(SEGMENT_ID, addr.getSegmentId());
163 properties.put(MODULE_ID, addr.getModuleId());
164 properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
166 DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid)
167 .withProperties(properties).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
168 .withBridge(bridgeUid);
170 discoveryResultBuilders.put(addr, discoveryResult);
171 } else if (matcher.pattern() == NAME_PATTERN) {
172 // Received part of a module's name frame
174 final int part = Integer.parseInt(matcher.group("part")) - 1;
175 final String name = matcher.group("name");
177 moduleNames.compute(addr, (partNumber, namePart) -> {
178 Map<Integer, String> namePartMapping = namePart;
179 if (namePartMapping == null) {
180 namePartMapping = new HashMap<>();
183 namePartMapping.put(part, name);
185 return namePartMapping;
192 builderTask = scheduler.scheduleWithFixedDelay(() -> {
193 synchronized (LcnModuleDiscoveryService.this) {
194 discoveryResultBuilders.entrySet().stream().filter(e -> {
195 Map<Integer, String> localNameParts = moduleNames.get(e.getKey());
196 return localNameParts != null && localNameParts.size() == MODULE_NAME_PART_COUNT;
197 }).filter(e -> !successfullyDiscovered.contains(e.getKey())).forEach(e -> {
198 StringBuilder thingName = new StringBuilder();
199 if (e.getKey().getSegmentId() != 0) {
200 thingName.append("Segment " + e.getKey().getSegmentId() + " ");
203 thingName.append("Module " + e.getKey().getModuleId() + ": ");
204 Map<Integer, String> localNameParts = moduleNames.get(e.getKey());
205 if (localNameParts != null) {
206 thingName.append(localNameParts.get(0));
207 thingName.append(localNameParts.get(1));
209 thingDiscovered(e.getValue().withLabel(thingName.toString()).build());
210 successfullyDiscovered.add(e.getKey());
214 }, 500, 500, TimeUnit.MILLISECONDS);
216 localBridgeHandler.sendModuleDiscoveryCommand();
220 private synchronized void rescheduleQueueProcessor() {
221 // delay serial number and module name requests to not clog the bus
222 ScheduledFuture<?> localQueueProcessor = queueProcessor;
223 if (localQueueProcessor != null) {
224 localQueueProcessor.cancel(true);
226 queueProcessor = scheduler.scheduleWithFixedDelay(() -> {
227 PckGatewayHandler localBridgeHandler = bridgeHandler;
228 if (localBridgeHandler != null) {
229 LcnAddrMod serial = serialNumberRequestQueue.poll();
230 if (serial != null) {
231 localBridgeHandler.sendSerialNumberRequest(serial);
234 LcnAddrMod name = moduleNameRequestQueue.poll();
236 localBridgeHandler.sendModuleNameRequest(name);
239 // stop scan when all LCN modules have been requested
240 if (serial == null && name == null) {
241 scheduler.schedule(this::stopScan, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
244 }, ACK_TIMEOUT_MS, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
248 public synchronized void stopScan() {
249 ScheduledFuture<?> localBuilderTask = builderTask;
250 if (localBuilderTask != null) {
251 localBuilderTask.cancel(true);
253 ScheduledFuture<?> localQueueProcessor = queueProcessor;
254 if (localQueueProcessor != null) {
255 localQueueProcessor.cancel(true);
257 PckGatewayHandler localBridgeHandler = bridgeHandler;
258 if (localBridgeHandler != null) {
259 localBridgeHandler.removeAllPckListeners();
261 successfullyDiscovered.clear();