]> git.basschouten.com Git - openhab-addons.git/blob
393f040b6a2b8935ece1a68a3a1ee2e8346e0458
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.lcn.internal;
14
15 import java.util.Collections;
16 import java.util.HashMap;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Queue;
21 import java.util.Set;
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;
30
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;
47
48 /**
49  * Scans all LCN segments for LCN modules.
50  *
51  * Scan approach:
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()
58  *
59  * @author Fabian Wolter - Initial Contribution
60  */
61 @NonNullByDefault
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;
82
83     public LcnModuleDiscoveryService() {
84         super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SEC, false);
85     }
86
87     @Override
88     public void setThingHandler(@Nullable ThingHandler handler) {
89         if (handler instanceof PckGatewayHandler) {
90             this.bridgeHandler = (PckGatewayHandler) handler;
91         }
92     }
93
94     @Override
95     public @Nullable ThingHandler getThingHandler() {
96         return bridgeHandler;
97     }
98
99     @Override
100     public void deactivate() {
101         stopScan();
102         super.deactivate();
103     }
104
105     @Override
106     protected void startScan() {
107         synchronized (this) {
108             PckGatewayHandler localBridgeHandler = bridgeHandler;
109             if (localBridgeHandler == null) {
110                 logger.warn("Bridge handler not set");
111                 return;
112             }
113
114             ScheduledFuture<?> localBuilderTask = builderTask;
115             if (localBridgeHandler.getConnection() == null && localBuilderTask != null) {
116                 localBuilderTask.cancel(true);
117             }
118
119             localBridgeHandler.registerPckListener(data -> {
120                 Matcher matcher;
121
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();
127
128                         if (connection == null) {
129                             return;
130                         }
131
132                         LcnAddrMod addr = new LcnAddrMod(
133                                 localBridgeHandler.toLogicalSegmentId(Integer.parseInt(matcher.group("segId"))),
134                                 Integer.parseInt(matcher.group("modId")));
135
136                         if (matcher.pattern() == LcnModuleMetaAckSubHandler.PATTERN_POS) {
137                             // Received an ACK frame
138
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
144                             }
145
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
150                             }
151                         } else if (matcher.pattern() == LcnModuleMetaFirmwareSubHandler.PATTERN) {
152                             // Received a firmware version info frame
153
154                             ThingUID bridgeUid = localBridgeHandler.getThing().getUID();
155                             String serialNumber = matcher.group("sn");
156
157                             String thingID = String.format("S%03dM%03d", addr.getSegmentId(), addr.getModuleId());
158
159                             ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_MODULE, bridgeUid, thingID);
160
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);
165
166                             DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid)
167                                     .withProperties(properties).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
168                                     .withBridge(bridgeUid);
169
170                             discoveryResultBuilders.put(addr, discoveryResult);
171                         } else if (matcher.pattern() == NAME_PATTERN) {
172                             // Received part of a module's name frame
173
174                             final int part = Integer.parseInt(matcher.group("part")) - 1;
175                             final String name = matcher.group("name");
176
177                             moduleNames.compute(addr, (partNumber, namePart) -> {
178                                 Map<Integer, String> namePartMapping = namePart;
179                                 if (namePartMapping == null) {
180                                     namePartMapping = new HashMap<>();
181                                 }
182
183                                 namePartMapping.put(part, name);
184
185                                 return namePartMapping;
186                             });
187                         }
188                     }
189                 }
190             });
191
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() + " ");
201                         }
202
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));
208
209                             thingDiscovered(e.getValue().withLabel(thingName.toString()).build());
210                             successfullyDiscovered.add(e.getKey());
211                         }
212                     });
213                 }
214             }, 500, 500, TimeUnit.MILLISECONDS);
215
216             localBridgeHandler.sendModuleDiscoveryCommand();
217         }
218     }
219
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);
225         }
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);
232                 }
233
234                 LcnAddrMod name = moduleNameRequestQueue.poll();
235                 if (name != null) {
236                     localBridgeHandler.sendModuleNameRequest(name);
237                 }
238
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);
242                 }
243             }
244         }, ACK_TIMEOUT_MS, ACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
245     }
246
247     @Override
248     public synchronized void stopScan() {
249         ScheduledFuture<?> localBuilderTask = builderTask;
250         if (localBuilderTask != null) {
251             localBuilderTask.cancel(true);
252         }
253         ScheduledFuture<?> localQueueProcessor = queueProcessor;
254         if (localQueueProcessor != null) {
255             localQueueProcessor.cancel(true);
256         }
257         PckGatewayHandler localBridgeHandler = bridgeHandler;
258         if (localBridgeHandler != null) {
259             localBridgeHandler.removeAllPckListeners();
260         }
261         successfullyDiscovered.clear();
262         moduleNames.clear();
263
264         super.stopScan();
265     }
266 }