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 thingHandler) {
263 logger.debug("Unexpected bridge handler: {}", handler);
269 * Get a reference to the modbus endpoint
271 private void connectEndpoint() {
276 ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
277 if (slaveEndpointThingHandler == null) {
278 @SuppressWarnings("null")
279 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
280 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
281 String.format("Bridge '%s' is offline", label));
282 logger.debug("No bridge handler available -- aborting init for {}", label);
287 slaveId = slaveEndpointThingHandler.getSlaveId();
288 comms = slaveEndpointThingHandler.getCommunicationInterface();
289 } catch (EndpointNotInitializedException e) {
290 // this will be handled below as endpoint remains null
294 @SuppressWarnings("null")
295 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
296 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
297 String.format("Bridge '%s' not completely initialized", label));
298 logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
304 * Remove the endpoint if exists
306 private void unregisterEndpoint() {
307 // Comms will be close()'d by endpoint thing handler
313 * This is where we set up our regular poller
315 private synchronized void registerPollTask(ModelBlock mainBlock) {
316 if (pollTask != null) {
317 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
318 throw new IllegalStateException("pollTask should be unregistered before registering a new one!");
321 ModbusCommunicationInterface mycomms = comms;
323 SunSpecConfiguration myconfig = config;
324 if (myconfig == null || mycomms == null) {
325 throw new IllegalStateException("registerPollTask called without proper configuration");
328 logger.debug("Setting up regular polling");
330 ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
331 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
333 long refreshMillis = myconfig.getRefreshMillis();
334 pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
335 result.getRegisters().ifPresent(this::handlePolledData);
336 if (getThing().getStatus() != ThingStatus.ONLINE) {
337 updateStatus(ThingStatus.ONLINE);
339 }, this::handleError);
343 * This method should handle incoming poll data, and update the channels
344 * with the values received
346 protected abstract void handlePolledData(ModbusRegisterArray registers);
349 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
350 super.bridgeStatusChanged(bridgeStatusInfo);
352 logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
353 if (getThing().getStatus() == ThingStatus.ONLINE) {
355 } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
361 * Unregister poll task.
363 * No-op in case no poll task is registered, or if the initialization is incomplete.
365 private synchronized void unregisterPollTask() {
367 PollTask task = pollTask;
371 logger.debug("Unregistering polling from ModbusManager");
373 ModbusCommunicationInterface mycomms = comms;
374 if (mycomms != null) {
375 mycomms.unregisterRegularPoll(task);
381 * Handle errors received during communication
383 protected void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
384 // Ignore all incoming data and errors if configuration is not correct
385 if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
388 String msg = failure.getCause().getMessage();
389 String cls = failure.getCause().getClass().getName();
390 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
391 String.format("Error with read: %s: %s", cls, msg));
395 * Returns true, if we're in a CONFIGURATION_ERROR state
399 protected boolean hasConfigurationError() {
400 ThingStatusInfo statusInfo = getThing().getStatusInfo();
401 return statusInfo.getStatus() == ThingStatus.OFFLINE
402 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
406 * Reset communication status to ONLINE if we're in an OFFLINE state
408 protected void resetCommunicationError() {
409 ThingStatusInfo statusInfo = thing.getStatusInfo();
410 if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
411 && ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
412 updateStatus(ThingStatus.ONLINE);
417 * Returns the channel UID for the specified group and channel id
419 * @param string the channel group
420 * @param string the channel id in that group
421 * @return the globally unique channel uid
423 ChannelUID channelUID(String group, String id) {
424 return new ChannelUID(getThing().getUID(), group, id);
428 * Returns value multiplied by the 10 on the power of scaleFactory
430 * @param value the value to alter
431 * @param scaleFactor the scale factor to use (may be negative)
432 * @return the scaled value as a DecimalType
434 protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
435 if (value.isEmpty() || scaleFactor.isEmpty()) {
436 return UnDefType.UNDEF;
438 return getScaled(value.get().longValue(), scaleFactor.get(), unit);
442 * Returns value multiplied by the 10 on the power of scaleFactory
444 * @param value the value to alter
445 * @param scaleFactor the scale factor to use (may be negative)
446 * @return the scaled value as a DecimalType
448 protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
449 return getScaled(value, Optional.of(scaleFactor), unit);
453 * Returns value multiplied by the 10 on the power of scaleFactory
455 * @param value the value to alter
456 * @param scaleFactor the scale factor to use (may be negative)
457 * @return the scaled value as a DecimalType
459 protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
460 if (scaleFactor == 0) {
461 return new QuantityType<>(value.longValue(), unit);
463 return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);