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