]> git.basschouten.com Git - openhab-addons.git/blob
c9b08516fab55359f7cbfd281891c56a6ebabd72
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.modbus.sunspec.internal.discovery;
14
15 import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
16
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.Optional;
20 import java.util.Queue;
21 import java.util.concurrent.ConcurrentLinkedQueue;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.modbus.discovery.ModbusDiscoveryListener;
26 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
27 import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
28 import org.openhab.binding.modbus.sunspec.internal.dto.CommonModelBlock;
29 import org.openhab.binding.modbus.sunspec.internal.dto.ModelBlock;
30 import org.openhab.binding.modbus.sunspec.internal.parser.CommonModelParser;
31 import org.openhab.core.config.discovery.DiscoveryResult;
32 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
33 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
34 import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
35 import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
36 import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
37 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
38 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
39 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
40 import org.openhab.core.io.transport.modbus.exception.ModbusSlaveErrorResponseException;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.thing.ThingUID;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * This class is used by the SunspecDiscoveryParticipant to detect
49  * the model blocks defined by the given device.
50  * It scans trough the defined model items and notifies the
51  * discovery service about the discovered devices
52  *
53  * @author Nagy Attila Gabor - Initial contribution
54  */
55 @NonNullByDefault
56 public class SunspecDiscoveryProcess {
57
58     /**
59      * Logger instance
60      */
61     private final Logger logger = LoggerFactory.getLogger(SunspecDiscoveryProcess.class);
62
63     /**
64      * The handler instance for this device
65      */
66     private final ModbusEndpointThingHandler handler;
67
68     /**
69      * Listener for the discovered devices. We get this
70      * from the main discovery service, and it is used to
71      * submit any discovered Sunspec devices
72      */
73     private final ModbusDiscoveryListener listener;
74
75     /**
76      * The endpoint's slave id
77      */
78     private int slaveId;
79
80     /**
81      * Number of maximum retries
82      */
83     private static final int maxTries = 3;
84
85     /**
86      * List of start addresses to try
87      */
88     private Queue<Integer> possibleAddresses;
89
90     /**
91      * This is the base address where the next block should be searched for
92      */
93     private int baseAddress = 40000;
94
95     /**
96      * Count of valid Sunspec blocks found
97      */
98     private int blocksFound = 0;
99
100     /**
101      * Parser for commonblock
102      */
103     private final CommonModelParser commonBlockParser;
104
105     /**
106      * The last common block found. This is used
107      * to get the details of any found devices
108      */
109     private @Nullable CommonModelBlock lastCommonBlock = null;
110
111     /**
112      * Communication interface to the endpoint
113      */
114     private ModbusCommunicationInterface comms;
115
116     /**
117      * New instances of this class should get a reference to the handler
118      *
119      * @throws EndpointNotInitializedException
120      */
121     public SunspecDiscoveryProcess(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener)
122             throws EndpointNotInitializedException {
123         this.handler = handler;
124
125         ModbusCommunicationInterface localComms = handler.getCommunicationInterface();
126         if (localComms != null) {
127             this.comms = localComms;
128         } else {
129             throw new EndpointNotInitializedException();
130         }
131         slaveId = handler.getSlaveId();
132         this.listener = listener;
133         commonBlockParser = new CommonModelParser();
134         possibleAddresses = new ConcurrentLinkedQueue<>();
135         // Preferred and alternate base registers
136         // @see SunSpec Information Model Overview
137         possibleAddresses.add(40000);
138         possibleAddresses.add(50000);
139         possibleAddresses.add(0);
140     }
141
142     /**
143      * Start model detection
144      *
145      * @param uid the thing type to look for
146      * @throws EndpointNotInitializedException
147      */
148     public void detectModel() {
149
150         if (possibleAddresses.isEmpty()) {
151             parsingFinished();
152             return;
153         }
154         // Try the next address from the possibles
155         baseAddress = possibleAddresses.poll();
156         logger.trace("Beginning scan for SunSpec device at address {}", baseAddress);
157
158         ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
159                 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address
160                 SUNSPEC_ID_SIZE, // number or words to return
161                 maxTries);
162
163         comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::headerReceived),
164                 this::handleError);
165     }
166
167     /**
168      * We received the first two words, that should equal to SunS
169      */
170     private void headerReceived(ModbusRegisterArray registers) {
171         logger.trace("Received response from device {}", registers.toString());
172
173         Optional<DecimalType> id = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.UINT32);
174
175         if (!id.isPresent() || id.get().longValue() != SUNSPEC_ID) {
176             logger.debug("Could not find SunSpec DID at address {}, received: {}, expected: {}", baseAddress, id,
177                     SUNSPEC_ID);
178             detectModel();
179             return;
180         }
181
182         logger.trace("Header looks correct");
183         baseAddress += SUNSPEC_ID_SIZE;
184
185         lookForModelBlock();
186     }
187
188     /**
189      * Look for a valid model block at the current base address
190      */
191     private void lookForModelBlock() {
192
193         ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
194                 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address
195                 MODEL_HEADER_SIZE, // number or words to return
196                 maxTries);
197
198         comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::modelBlockReceived),
199                 this::handleError);
200     }
201
202     /**
203      * We received a model block header
204      */
205     private void modelBlockReceived(ModbusRegisterArray registers) {
206         logger.debug("Received response from device {}", registers.toString());
207
208         Optional<DecimalType> moduleID = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.UINT16);
209         Optional<DecimalType> blockLength = ModbusBitUtilities.extractStateFromRegisters(registers, 1,
210                 ValueType.UINT16);
211
212         if (!moduleID.isPresent() || !blockLength.isPresent()) {
213             logger.info("Could not find valid module id or block length field.");
214             parsingFinished();
215             return;
216         }
217         ModelBlock block = new ModelBlock();
218         block.address = baseAddress;
219         block.moduleID = moduleID.get().intValue();
220         block.length = blockLength.get().intValue() + MODEL_HEADER_SIZE;
221         logger.debug("SunSpec detector found block {}", block);
222
223         blocksFound++;
224
225         if (block.moduleID == FINAL_BLOCK) {
226             parsingFinished();
227         } else {
228             baseAddress += block.length;
229             if (block.moduleID == COMMON_BLOCK) {
230                 readCommonBlock(block); // This is an asynchronous task
231                 return;
232             } else {
233                 createDiscoveryResult(block);
234                 lookForModelBlock();
235             }
236
237         }
238     }
239
240     /**
241      * Start reading common block
242      *
243      * @param block
244      */
245     private void readCommonBlock(ModelBlock block) {
246         ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
247                 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, block.address, // Start address
248                 block.length, // number or words to return
249                 maxTries);
250
251         comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::parseCommonBlock),
252                 this::handleError);
253     }
254
255     /**
256      * We've read the details of a common block now parse it, and
257      * store for later use
258      *
259      * @param registers
260      */
261     private void parseCommonBlock(ModbusRegisterArray registers) {
262         logger.trace("Got common block data: {}", registers);
263         lastCommonBlock = commonBlockParser.parse(registers);
264         lookForModelBlock(); // Continue parsing
265     }
266
267     /**
268      * Create a discovery result from a model block
269      *
270      * @param block the block we've found
271      */
272     private void createDiscoveryResult(ModelBlock block) {
273         if (!SUPPORTED_THING_TYPES_UIDS.containsKey(block.moduleID)) {
274             logger.debug("ModuleID {} is not supported, skipping this block", block.moduleID);
275             return;
276         }
277
278         CommonModelBlock commonBlock = lastCommonBlock;
279
280         if (commonBlock == null) {
281             logger.warn(
282                     "Found model block without a preceding common block. Can't add device because details are unkown");
283             return;
284         }
285
286         ThingTypeUID thingTypeUID = SUPPORTED_THING_TYPES_UIDS.get(block.moduleID);
287         if (thingTypeUID == null) {
288             logger.warn("Found model block but no corresponding thing type UID present: {}", block.moduleID);
289             return;
290         }
291         ThingUID thingUID = new ThingUID(thingTypeUID, handler.getUID(), Integer.toString(block.address));
292
293         Map<String, Object> properties = new HashMap<>();
294         properties.put(PROPERTY_VENDOR, commonBlock.manufacturer);
295         properties.put(PROPERTY_MODEL, commonBlock.model);
296         properties.put(PROPERTY_SERIAL_NUMBER, commonBlock.serialNumber);
297         properties.put(PROPERTY_VERSION, commonBlock.version);
298         properties.put(PROPERTY_BLOCK_ADDRESS, block.address);
299         properties.put(PROPERTY_BLOCK_LENGTH, block.length);
300         properties.put(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
301
302         DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
303                 .withRepresentationProperty(PROPERTY_UNIQUE_ADDRESS).withBridge(handler.getUID())
304                 .withLabel(commonBlock.manufacturer + " " + commonBlock.model).build();
305
306         listener.thingDiscovered(result);
307     }
308
309     /**
310      * Parsing of model blocks finished
311      * Now we have to report back to the handler the common block and the block we were looking for
312      */
313     private void parsingFinished() {
314         listener.discoveryFinished();
315     }
316
317     /**
318      * Handle errors received during communication
319      */
320     private void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
321         if (blocksFound > 1 && failure.getCause() instanceof ModbusSlaveErrorResponseException) {
322             int code = ((ModbusSlaveErrorResponseException) failure.getCause()).getExceptionCode();
323             if (code == ModbusSlaveErrorResponseException.ILLEGAL_DATA_ACCESS
324                     || code == ModbusSlaveErrorResponseException.ILLEGAL_DATA_VALUE) {
325                 // It is very likely that the slave does not report an end block (0xffff) after the main blocks
326                 // so we treat this situation as normal.
327                 logger.debug(
328                         "Seems like slave device does not report an end block. Continuing with the dectected blocks");
329                 parsingFinished();
330                 return;
331             }
332         }
333
334         String cls = failure.getCause().getClass().getName();
335         String msg = failure.getCause().getMessage();
336
337         logger.warn("Error with read at address {}: {} {}", baseAddress, cls, msg);
338
339         detectModel();
340     }
341 }