2 * Copyright (c) 2010-2023 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.modbus.sunspec.internal.discovery;
15 import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
17 import java.util.HashMap;
19 import java.util.Optional;
20 import java.util.Queue;
21 import java.util.concurrent.ConcurrentLinkedQueue;
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;
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
53 * @author Nagy Attila Gabor - Initial contribution
56 public class SunspecDiscoveryProcess {
61 private final Logger logger = LoggerFactory.getLogger(SunspecDiscoveryProcess.class);
64 * The handler instance for this device
66 private final ModbusEndpointThingHandler handler;
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
73 private final ModbusDiscoveryListener listener;
76 * The endpoint's slave id
81 * Number of maximum retries
83 private static final int maxTries = 3;
86 * List of start addresses to try
88 private Queue<Integer> possibleAddresses;
91 * This is the base address where the next block should be searched for
93 private int baseAddress = 40000;
96 * Count of valid Sunspec blocks found
98 private int blocksFound = 0;
101 * Parser for commonblock
103 private final CommonModelParser commonBlockParser;
106 * The last common block found. This is used
107 * to get the details of any found devices
109 private @Nullable CommonModelBlock lastCommonBlock = null;
112 * Communication interface to the endpoint
114 private ModbusCommunicationInterface comms;
117 * New instances of this class should get a reference to the handler
119 * @throws EndpointNotInitializedException
121 public SunspecDiscoveryProcess(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener)
122 throws EndpointNotInitializedException {
123 this.handler = handler;
125 ModbusCommunicationInterface localComms = handler.getCommunicationInterface();
126 if (localComms != null) {
127 this.comms = localComms;
129 throw new EndpointNotInitializedException();
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);
143 * Start model detection
145 * @param uid the thing type to look for
146 * @throws EndpointNotInitializedException
148 public void detectModel() {
150 if (possibleAddresses.isEmpty()) {
154 // Try the next address from the possibles
155 baseAddress = possibleAddresses.poll();
156 logger.trace("Beginning scan for SunSpec device at address {}", baseAddress);
158 ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
159 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address
160 SUNSPEC_ID_SIZE, // number or words to return
163 comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::headerReceived),
168 * We received the first two words, that should equal to SunS
170 private void headerReceived(ModbusRegisterArray registers) {
171 logger.trace("Received response from device {}", registers.toString());
173 Optional<DecimalType> id = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.UINT32);
175 if (!id.isPresent() || id.get().longValue() != SUNSPEC_ID) {
176 logger.debug("Could not find SunSpec DID at address {}, received: {}, expected: {}", baseAddress, id,
182 logger.trace("Header looks correct");
183 baseAddress += SUNSPEC_ID_SIZE;
189 * Look for a valid model block at the current base address
191 private void lookForModelBlock() {
193 ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
194 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address
195 MODEL_HEADER_SIZE, // number or words to return
198 comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::modelBlockReceived),
203 * We received a model block header
205 private void modelBlockReceived(ModbusRegisterArray registers) {
206 logger.debug("Received response from device {}", registers.toString());
208 Optional<DecimalType> moduleID = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.UINT16);
209 Optional<DecimalType> blockLength = ModbusBitUtilities.extractStateFromRegisters(registers, 1,
212 if (!moduleID.isPresent() || !blockLength.isPresent()) {
213 logger.info("Could not find valid module id or block length field.");
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);
225 if (block.moduleID == FINAL_BLOCK) {
228 baseAddress += block.length;
229 if (block.moduleID == COMMON_BLOCK) {
230 readCommonBlock(block); // This is an asynchronous task
233 createDiscoveryResult(block);
241 * Start reading common block
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
251 comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::parseCommonBlock),
256 * We've read the details of a common block now parse it, and
257 * store for later use
261 private void parseCommonBlock(ModbusRegisterArray registers) {
262 logger.trace("Got common block data: {}", registers);
263 lastCommonBlock = commonBlockParser.parse(registers);
264 lookForModelBlock(); // Continue parsing
268 * Create a discovery result from a model block
270 * @param block the block we've found
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);
278 CommonModelBlock commonBlock = lastCommonBlock;
280 if (commonBlock == null) {
282 "Found model block without a preceding common block. Can't add device because details are unkown");
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);
291 ThingUID thingUID = new ThingUID(thingTypeUID, handler.getUID(), Integer.toString(block.address));
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);
302 DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
303 .withRepresentationProperty(PROPERTY_UNIQUE_ADDRESS).withBridge(handler.getUID())
304 .withLabel(commonBlock.manufacturer + " " + commonBlock.model).build();
306 listener.thingDiscovered(result);
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
313 private void parsingFinished() {
314 listener.discoveryFinished();
318 * Handle errors received during communication
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.
328 "Seems like slave device does not report an end block. Continuing with the dectected blocks");
334 String cls = failure.getCause().getClass().getName();
335 String msg = failure.getCause().getMessage();
337 logger.warn("Error with read at address {}: {} {}", baseAddress, cls, msg);