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