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.handler;
15 import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
17 import java.math.BigDecimal;
19 import java.util.Optional;
21 import javax.measure.Unit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
26 import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
27 import org.openhab.binding.modbus.sunspec.internal.SunSpecConfiguration;
28 import org.openhab.binding.modbus.sunspec.internal.dto.ModelBlock;
29 import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
30 import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
31 import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
32 import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
33 import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
34 import org.openhab.core.io.transport.modbus.PollTask;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.ThingStatusInfo;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.ThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link AbstractSunSpecHandler} is the base class for any sunspec handlers
52 * Common things are handled here:
54 * - loads the configuration either from the configuration file or
55 * from the properties that have been set by the auto discovery
56 * - sets up a regular poller to the device
57 * - handles incoming messages from the device:
58 * - common properties are parsed and published
59 * - other values are submitted to child implementations
60 * - handles disposal of the device by removing any handlers
61 * - implements some tool methods
63 * @author Nagy Attila Gabor - Initial contribution
66 public abstract class AbstractSunSpecHandler extends BaseThingHandler {
71 private final Logger logger = LoggerFactory.getLogger(AbstractSunSpecHandler.class);
74 * Configuration instance
76 protected @Nullable SunSpecConfiguration config = null;
79 * This is the task used to poll the device
81 private volatile @Nullable PollTask pollTask = null;
84 * Communication interface to the slave endpoint we're connecting to
86 protected volatile @Nullable ModbusCommunicationInterface comms = null;
89 * This is the slave id, we store this once initialization is complete
91 private volatile int slaveId;
94 * Instances of this handler should get a reference to the modbus manager
96 * @param thing the thing to handle
98 public AbstractSunSpecHandler(Thing thing) {
103 * Handle incoming commands. This binding is read-only by default
106 public void handleCommand(ChannelUID channelUID, Command command) {
107 // Currently we do not support any commands
112 * Load the config object of the block
113 * Connect to the slave bridge
114 * Start the periodic polling
117 public void initialize() {
118 config = getConfigAs(SunSpecConfiguration.class);
119 logger.debug("Initializing thing with properties: {}", thing.getProperties());
125 * This method starts the operation of this handler
126 * Load the config object of the block
127 * Connect to the slave bridge
128 * Start the periodic polling
130 private void startUp() {
133 if (comms == null || config == null) {
134 logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
138 if (pollTask != null) {
142 // Try properties first
144 ModelBlock mainBlock = getAddressFromProperties();
146 if (mainBlock == null) {
147 mainBlock = getAddressFromConfig();
150 if (mainBlock != null) {
151 publishUniqueAddress(mainBlock);
152 updateStatus(ThingStatus.UNKNOWN);
153 registerPollTask(mainBlock);
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
156 "SunSpec item should either have the address and length configuration set or should been created by auto discovery");
162 * Load and parse configuration from the properties
163 * These will be set by the auto discovery process
165 private @Nullable ModelBlock getAddressFromProperties() {
166 Map<String, String> properties = thing.getProperties();
167 if (!properties.containsKey(PROPERTY_BLOCK_ADDRESS) || !properties.containsKey(PROPERTY_BLOCK_LENGTH)) {
171 ModelBlock block = new ModelBlock();
172 block.address = (int) Double.parseDouble(thing.getProperties().getOrDefault(PROPERTY_BLOCK_ADDRESS, ""));
173 block.length = (int) Double.parseDouble(thing.getProperties().getOrDefault(PROPERTY_BLOCK_LENGTH, ""));
175 } catch (NumberFormatException ex) {
176 logger.debug("Could not parse address and length properties, error: {}", ex.getMessage());
182 * Load configuration from main configuration
184 private @Nullable ModelBlock getAddressFromConfig() {
186 SunSpecConfiguration myconfig = config;
187 if (myconfig == null) {
190 ModelBlock block = new ModelBlock();
191 block.address = myconfig.address;
192 block.length = myconfig.length;
197 * Publish the unique address property if it has not been set before
199 private void publishUniqueAddress(ModelBlock block) {
200 Map<String, String> properties = getThing().getProperties();
201 if (properties.containsKey(PROPERTY_UNIQUE_ADDRESS) && !properties.get(PROPERTY_UNIQUE_ADDRESS).isEmpty()) {
202 logger.debug("Current unique address is: {}", properties.get(PROPERTY_UNIQUE_ADDRESS));
206 ModbusEndpointThingHandler handler = getEndpointThingHandler();
207 if (handler == null) {
210 getThing().setProperty(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
214 * Dispose the binding correctly
217 public void dispose() {
222 * Unregister the poll task and release the endpoint reference
224 private void tearDown() {
225 unregisterPollTask();
226 unregisterEndpoint();
230 * Returns the current slave id from the bridge
232 public int getSlaveId() {
237 * Get the endpoint handler from the bridge this handler is connected to
238 * Checks that we're connected to the right type of bridge
240 * @return the endpoint handler or null if the bridge does not exist
242 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
243 Bridge bridge = getBridge();
244 if (bridge == null) {
245 logger.debug("Bridge is null");
248 if (bridge.getStatus() != ThingStatus.ONLINE) {
249 logger.debug("Bridge is not online");
253 ThingHandler handler = bridge.getHandler();
254 if (handler == null) {
255 logger.debug("Bridge handler is null");
259 if (handler instanceof ModbusEndpointThingHandler thingHandler) {
262 logger.debug("Unexpected bridge handler: {}", handler);
268 * Get a reference to the modbus endpoint
270 private void connectEndpoint() {
275 ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
276 if (slaveEndpointThingHandler == null) {
277 @SuppressWarnings("null")
278 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
279 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
280 String.format("Bridge '%s' is offline", label));
281 logger.debug("No bridge handler available -- aborting init for {}", label);
286 slaveId = slaveEndpointThingHandler.getSlaveId();
287 comms = slaveEndpointThingHandler.getCommunicationInterface();
288 } catch (EndpointNotInitializedException e) {
289 // this will be handled below as endpoint remains null
293 @SuppressWarnings("null")
294 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
295 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
296 String.format("Bridge '%s' not completely initialized", label));
297 logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
303 * Remove the endpoint if exists
305 private void unregisterEndpoint() {
306 // Comms will be close()'d by endpoint thing handler
312 * This is where we set up our regular poller
314 private synchronized void registerPollTask(ModelBlock mainBlock) {
315 if (pollTask != null) {
316 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
317 throw new IllegalStateException("pollTask should be unregistered before registering a new one!");
320 ModbusCommunicationInterface mycomms = comms;
322 SunSpecConfiguration myconfig = config;
323 if (myconfig == null || mycomms == null) {
324 throw new IllegalStateException("registerPollTask called without proper configuration");
327 logger.debug("Setting up regular polling");
329 ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
330 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
332 long refreshMillis = myconfig.getRefreshMillis();
333 pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
334 result.getRegisters().ifPresent(this::handlePolledData);
335 if (getThing().getStatus() != ThingStatus.ONLINE) {
336 updateStatus(ThingStatus.ONLINE);
338 }, this::handleError);
342 * This method should handle incoming poll data, and update the channels
343 * with the values received
345 protected abstract void handlePolledData(ModbusRegisterArray registers);
348 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
349 super.bridgeStatusChanged(bridgeStatusInfo);
351 logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
352 if (getThing().getStatus() == ThingStatus.ONLINE) {
354 } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
360 * Unregister poll task.
362 * No-op in case no poll task is registered, or if the initialization is incomplete.
364 private synchronized void unregisterPollTask() {
366 PollTask task = pollTask;
370 logger.debug("Unregistering polling from ModbusManager");
372 ModbusCommunicationInterface mycomms = comms;
373 if (mycomms != null) {
374 mycomms.unregisterRegularPoll(task);
380 * Handle errors received during communication
382 protected void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
383 // Ignore all incoming data and errors if configuration is not correct
384 if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
387 String msg = failure.getCause().getMessage();
388 String cls = failure.getCause().getClass().getName();
389 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
390 String.format("Error with read: %s: %s", cls, msg));
394 * Returns true, if we're in a CONFIGURATION_ERROR state
398 protected boolean hasConfigurationError() {
399 ThingStatusInfo statusInfo = getThing().getStatusInfo();
400 return statusInfo.getStatus() == ThingStatus.OFFLINE
401 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
405 * Reset communication status to ONLINE if we're in an OFFLINE state
407 protected void resetCommunicationError() {
408 ThingStatusInfo statusInfo = thing.getStatusInfo();
409 if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
410 && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
411 updateStatus(ThingStatus.ONLINE);
416 * Returns the channel UID for the specified group and channel id
418 * @param string the channel group
419 * @param string the channel id in that group
420 * @return the globally unique channel uid
422 ChannelUID channelUID(String group, String id) {
423 return new ChannelUID(getThing().getUID(), group, id);
427 * Returns value multiplied by the 10 on the power of scaleFactory
429 * @param value the value to alter
430 * @param scaleFactor the scale factor to use (may be negative)
431 * @return the scaled value as a DecimalType
433 protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
434 if (value.isEmpty() || scaleFactor.isEmpty()) {
435 return UnDefType.UNDEF;
437 return getScaled(value.get().longValue(), scaleFactor.get(), unit);
441 * Returns value multiplied by the 10 on the power of scaleFactory
443 * @param value the value to alter
444 * @param scaleFactor the scale factor to use (may be negative)
445 * @return the scaled value as a DecimalType
447 protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
448 return getScaled(value, Optional.of(scaleFactor), unit);
452 * Returns value multiplied by the 10 on the power of scaleFactory
454 * @param value the value to alter
455 * @param scaleFactor the scale factor to use (may be negative)
456 * @return the scaled value as a DecimalType
458 protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
459 if (scaleFactor == 0) {
460 return new QuantityType<>(value.longValue(), unit);
462 return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);