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() {
135 if (comms == null || config == null) {
136 logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
140 if (pollTask != null) {
144 // Try properties first
146 ModelBlock mainBlock = getAddressFromProperties();
148 if (mainBlock == null) {
149 mainBlock = getAddressFromConfig();
152 if (mainBlock != null) {
153 publishUniqueAddress(mainBlock);
154 updateStatus(ThingStatus.UNKNOWN);
155 registerPollTask(mainBlock);
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
158 "SunSpec item should either have the address and length configuration set or should been created by auto discovery");
164 * Load and parse configuration from the properties
165 * These will be set by the auto discovery process
167 private @Nullable ModelBlock getAddressFromProperties() {
168 Map<String, String> properties = thing.getProperties();
169 if (!properties.containsKey(PROPERTY_BLOCK_ADDRESS) || !properties.containsKey(PROPERTY_BLOCK_LENGTH)) {
173 ModelBlock block = new ModelBlock();
174 block.address = (int) Double.parseDouble(thing.getProperties().getOrDefault(PROPERTY_BLOCK_ADDRESS, ""));
175 block.length = (int) Double.parseDouble(thing.getProperties().getOrDefault(PROPERTY_BLOCK_LENGTH, ""));
177 } catch (NumberFormatException ex) {
178 logger.debug("Could not parse address and length properties, error: {}", ex.getMessage());
184 * Load configuration from main configuration
186 private @Nullable ModelBlock getAddressFromConfig() {
188 SunSpecConfiguration myconfig = config;
189 if (myconfig == null) {
192 ModelBlock block = new ModelBlock();
193 block.address = myconfig.address;
194 block.length = myconfig.length;
199 * Publish the unique address property if it has not been set before
201 private void publishUniqueAddress(ModelBlock block) {
202 Map<String, String> properties = getThing().getProperties();
203 if (properties.containsKey(PROPERTY_UNIQUE_ADDRESS) && !properties.get(PROPERTY_UNIQUE_ADDRESS).isEmpty()) {
204 logger.debug("Current unique address is: {}", properties.get(PROPERTY_UNIQUE_ADDRESS));
208 ModbusEndpointThingHandler handler = getEndpointThingHandler();
209 if (handler == null) {
212 getThing().setProperty(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
216 * Dispose the binding correctly
219 public void dispose() {
224 * Unregister the poll task and release the endpoint reference
226 private void tearDown() {
227 unregisterPollTask();
228 unregisterEndpoint();
232 * Returns the current slave id from the bridge
234 public int getSlaveId() {
239 * Get the endpoint handler from the bridge this handler is connected to
240 * Checks that we're connected to the right type of bridge
242 * @return the endpoint handler or null if the bridge does not exist
244 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
245 Bridge bridge = getBridge();
246 if (bridge == null) {
247 logger.debug("Bridge is null");
250 if (bridge.getStatus() != ThingStatus.ONLINE) {
251 logger.debug("Bridge is not online");
255 ThingHandler handler = bridge.getHandler();
256 if (handler == null) {
257 logger.debug("Bridge handler is null");
261 if (handler instanceof ModbusEndpointThingHandler) {
262 ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
263 return slaveEndpoint;
265 logger.debug("Unexpected bridge handler: {}", handler);
271 * Get a reference to the modbus endpoint
273 private void connectEndpoint() {
278 ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
279 if (slaveEndpointThingHandler == null) {
280 @SuppressWarnings("null")
281 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
283 String.format("Bridge '%s' is offline", label));
284 logger.debug("No bridge handler available -- aborting init for {}", label);
289 slaveId = slaveEndpointThingHandler.getSlaveId();
290 comms = slaveEndpointThingHandler.getCommunicationInterface();
291 } catch (EndpointNotInitializedException e) {
292 // this will be handled below as endpoint remains null
296 @SuppressWarnings("null")
297 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
298 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
299 String.format("Bridge '%s' not completely initialized", label));
300 logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
306 * Remove the endpoint if exists
308 private void unregisterEndpoint() {
309 // Comms will be close()'d by endpoint thing handler
315 * This is where we set up our regular poller
317 private synchronized void registerPollTask(ModelBlock mainBlock) {
318 if (pollTask != null) {
319 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
320 throw new IllegalStateException("pollTask should be unregistered before registering a new one!");
323 ModbusCommunicationInterface mycomms = comms;
325 SunSpecConfiguration myconfig = config;
326 if (myconfig == null || mycomms == null) {
327 throw new IllegalStateException("registerPollTask called without proper configuration");
330 logger.debug("Setting up regular polling");
332 ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
333 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
335 long refreshMillis = myconfig.getRefreshMillis();
336 pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
337 result.getRegisters().ifPresent(this::handlePolledData);
338 if (getThing().getStatus() != ThingStatus.ONLINE) {
339 updateStatus(ThingStatus.ONLINE);
341 }, this::handleError);
345 * This method should handle incoming poll data, and update the channels
346 * with the values received
348 protected abstract void handlePolledData(ModbusRegisterArray registers);
351 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
352 super.bridgeStatusChanged(bridgeStatusInfo);
354 logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
355 if (getThing().getStatus() == ThingStatus.ONLINE) {
357 } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
363 * Unregister poll task.
365 * No-op in case no poll task is registered, or if the initialization is incomplete.
367 private synchronized void unregisterPollTask() {
369 PollTask task = pollTask;
373 logger.debug("Unregistering polling from ModbusManager");
375 ModbusCommunicationInterface mycomms = comms;
376 if (mycomms != null) {
377 mycomms.unregisterRegularPoll(task);
383 * Handle errors received during communication
385 protected void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
386 // Ignore all incoming data and errors if configuration is not correct
387 if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
390 String msg = failure.getCause().getMessage();
391 String cls = failure.getCause().getClass().getName();
392 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
393 String.format("Error with read: %s: %s", cls, msg));
397 * Returns true, if we're in a CONFIGURATION_ERROR state
401 protected boolean hasConfigurationError() {
402 ThingStatusInfo statusInfo = getThing().getStatusInfo();
403 return statusInfo.getStatus() == ThingStatus.OFFLINE
404 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
408 * Reset communication status to ONLINE if we're in an OFFLINE state
410 protected void resetCommunicationError() {
411 ThingStatusInfo statusInfo = thing.getStatusInfo();
412 if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
413 && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
414 updateStatus(ThingStatus.ONLINE);
419 * Returns the channel UID for the specified group and channel id
421 * @param string the channel group
422 * @param string the channel id in that group
423 * @return the globally unique channel uid
425 ChannelUID channelUID(String group, String id) {
426 return new ChannelUID(getThing().getUID(), group, id);
430 * Returns value multiplied by the 10 on the power of scaleFactory
432 * @param value the value to alter
433 * @param scaleFactor the scale factor to use (may be negative)
434 * @return the scaled value as a DecimalType
436 protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
437 if (!value.isPresent() || !scaleFactor.isPresent()) {
438 return UnDefType.UNDEF;
440 return getScaled(value.get().longValue(), scaleFactor.get(), unit);
444 * Returns value multiplied by the 10 on the power of scaleFactory
446 * @param value the value to alter
447 * @param scaleFactor the scale factor to use (may be negative)
448 * @return the scaled value as a DecimalType
450 protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
451 return getScaled(value, Optional.of(scaleFactor), unit);
455 * Returns value multiplied by the 10 on the power of scaleFactory
457 * @param value the value to alter
458 * @param scaleFactor the scale factor to use (may be negative)
459 * @return the scaled value as a DecimalType
461 protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
462 if (scaleFactor == 0) {
463 return new QuantityType<>(value.longValue(), unit);
465 return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);