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
97 * @param managerRef the modbus manager
99 public AbstractSunSpecHandler(Thing thing) {
104 * Handle incoming commands. This binding is read-only by default
107 public void handleCommand(ChannelUID channelUID, Command command) {
108 // Currently we do not support any commands
113 * Load the config object of the block
114 * Connect to the slave bridge
115 * Start the periodic polling
118 public void initialize() {
119 config = getConfigAs(SunSpecConfiguration.class);
120 logger.debug("Initializing thing with properties: {}", thing.getProperties());
126 * This method starts the operation of this handler
127 * Load the config object of the block
128 * Connect to the slave bridge
129 * Start the periodic polling
131 private void startUp() {
134 if (comms == null || config == null) {
135 logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
139 if (pollTask != null) {
143 // Try properties first
145 ModelBlock mainBlock = getAddressFromProperties();
147 if (mainBlock == null) {
148 mainBlock = getAddressFromConfig();
151 if (mainBlock != null) {
152 publishUniqueAddress(mainBlock);
153 updateStatus(ThingStatus.UNKNOWN);
154 registerPollTask(mainBlock);
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157 "SunSpec item should either have the address and length configuration set or should been created by auto discovery");
163 * Load and parse configuration from the properties
164 * These will be set by the auto discovery process
166 private @Nullable ModelBlock getAddressFromProperties() {
167 Map<String, String> properties = thing.getProperties();
168 if (!properties.containsKey(PROPERTY_BLOCK_ADDRESS) || !properties.containsKey(PROPERTY_BLOCK_LENGTH)) {
172 ModelBlock block = new ModelBlock();
173 block.address = (int) Double.parseDouble(thing.getProperties().getOrDefault(PROPERTY_BLOCK_ADDRESS, ""));
174 block.length = (int) Double.parseDouble(thing.getProperties().getOrDefault(PROPERTY_BLOCK_LENGTH, ""));
176 } catch (NumberFormatException ex) {
177 logger.debug("Could not parse address and length properties, error: {}", ex.getMessage());
183 * Load configuration from main configuration
185 private @Nullable ModelBlock getAddressFromConfig() {
187 SunSpecConfiguration myconfig = config;
188 if (myconfig == null) {
191 ModelBlock block = new ModelBlock();
192 block.address = myconfig.address;
193 block.length = myconfig.length;
198 * Publish the unique address property if it has not been set before
200 private void publishUniqueAddress(ModelBlock block) {
201 Map<String, String> properties = getThing().getProperties();
202 if (properties.containsKey(PROPERTY_UNIQUE_ADDRESS) && !properties.get(PROPERTY_UNIQUE_ADDRESS).isEmpty()) {
203 logger.debug("Current unique address is: {}", properties.get(PROPERTY_UNIQUE_ADDRESS));
207 ModbusEndpointThingHandler handler = getEndpointThingHandler();
208 if (handler == null) {
211 getThing().setProperty(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
215 * Dispose the binding correctly
218 public void dispose() {
223 * Unregister the poll task and release the endpoint reference
225 private void tearDown() {
226 unregisterPollTask();
227 unregisterEndpoint();
231 * Returns the current slave id from the bridge
233 public int getSlaveId() {
238 * Get the endpoint handler from the bridge this handler is connected to
239 * Checks that we're connected to the right type of bridge
241 * @return the endpoint handler or null if the bridge does not exist
243 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
244 Bridge bridge = getBridge();
245 if (bridge == null) {
246 logger.debug("Bridge is null");
249 if (bridge.getStatus() != ThingStatus.ONLINE) {
250 logger.debug("Bridge is not online");
254 ThingHandler handler = bridge.getHandler();
255 if (handler == null) {
256 logger.debug("Bridge handler is null");
260 if (handler instanceof ModbusEndpointThingHandler) {
261 ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
262 return slaveEndpoint;
264 logger.debug("Unexpected bridge handler: {}", handler);
270 * Get a reference to the modbus endpoint
272 private void connectEndpoint() {
277 ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
278 if (slaveEndpointThingHandler == null) {
279 @SuppressWarnings("null")
280 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
281 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
282 String.format("Bridge '%s' is offline", label));
283 logger.debug("No bridge handler available -- aborting init for {}", label);
288 slaveId = slaveEndpointThingHandler.getSlaveId();
289 comms = slaveEndpointThingHandler.getCommunicationInterface();
290 } catch (EndpointNotInitializedException e) {
291 // this will be handled below as endpoint remains null
295 @SuppressWarnings("null")
296 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
297 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
298 String.format("Bridge '%s' not completely initialized", label));
299 logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
305 * Remove the endpoint if exists
307 private void unregisterEndpoint() {
308 // Comms will be close()'d by endpoint thing handler
314 * This is where we set up our regular poller
316 private synchronized void registerPollTask(ModelBlock mainBlock) {
317 if (pollTask != null) {
318 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
319 throw new IllegalStateException("pollTask should be unregistered before registering a new one!");
322 ModbusCommunicationInterface mycomms = comms;
324 SunSpecConfiguration myconfig = config;
325 if (myconfig == null || mycomms == null) {
326 throw new IllegalStateException("registerPollTask called without proper configuration");
329 logger.debug("Setting up regular polling");
331 ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
332 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
334 long refreshMillis = myconfig.getRefreshMillis();
335 pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
336 result.getRegisters().ifPresent(this::handlePolledData);
337 if (getThing().getStatus() != ThingStatus.ONLINE) {
338 updateStatus(ThingStatus.ONLINE);
340 }, this::handleError);
344 * This method should handle incoming poll data, and update the channels
345 * with the values received
347 protected abstract void handlePolledData(ModbusRegisterArray registers);
350 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
351 super.bridgeStatusChanged(bridgeStatusInfo);
353 logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
354 if (getThing().getStatus() == ThingStatus.ONLINE) {
356 } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
362 * Unregister poll task.
364 * No-op in case no poll task is registered, or if the initialization is incomplete.
366 private synchronized void unregisterPollTask() {
368 PollTask task = pollTask;
372 logger.debug("Unregistering polling from ModbusManager");
374 ModbusCommunicationInterface mycomms = comms;
375 if (mycomms != null) {
376 mycomms.unregisterRegularPoll(task);
382 * Handle errors received during communication
384 protected void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
385 // Ignore all incoming data and errors if configuration is not correct
386 if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
389 String msg = failure.getCause().getMessage();
390 String cls = failure.getCause().getClass().getName();
391 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
392 String.format("Error with read: %s: %s", cls, msg));
396 * Returns true, if we're in a CONFIGURATION_ERROR state
400 protected boolean hasConfigurationError() {
401 ThingStatusInfo statusInfo = getThing().getStatusInfo();
402 return statusInfo.getStatus() == ThingStatus.OFFLINE
403 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
407 * Reset communication status to ONLINE if we're in an OFFLINE state
409 protected void resetCommunicationError() {
410 ThingStatusInfo statusInfo = thing.getStatusInfo();
411 if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
412 && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
413 updateStatus(ThingStatus.ONLINE);
418 * Returns the channel UID for the specified group and channel id
420 * @param string the channel group
421 * @param string the channel id in that group
422 * @return the globally unique channel uid
424 ChannelUID channelUID(String group, String id) {
425 return new ChannelUID(getThing().getUID(), group, id);
429 * Returns value multiplied by the 10 on the power of scaleFactory
431 * @param value the value to alter
432 * @param scaleFactor the scale factor to use (may be negative)
433 * @return the scaled value as a DecimalType
435 protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
436 if (!value.isPresent() || !scaleFactor.isPresent()) {
437 return UnDefType.UNDEF;
439 return getScaled(value.get().longValue(), scaleFactor.get(), unit);
443 * Returns value multiplied by the 10 on the power of scaleFactory
445 * @param value the value to alter
446 * @param scaleFactor the scale factor to use (may be negative)
447 * @return the scaled value as a DecimalType
449 protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
450 return getScaled(value, Optional.of(scaleFactor), unit);
454 * Returns value multiplied by the 10 on the power of scaleFactory
456 * @param value the value to alter
457 * @param scaleFactor the scale factor to use (may be negative)
458 * @return the scaled value as a DecimalType
460 protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
461 if (scaleFactor == 0) {
462 return new QuantityType<>(value.longValue(), unit);
464 return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);