]> git.basschouten.com Git - openhab-addons.git/blob
8138b564da0d97b8e2d51389bb727f6097dc4ca6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.modbus.sunspec.internal.handler;
14
15 import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.Map;
19 import java.util.Optional;
20
21 import javax.measure.Unit;
22
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;
49
50 /**
51  * The {@link AbstractSunSpecHandler} is the base class for any sunspec handlers
52  * Common things are handled here:
53  *
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
62  *
63  * @author Nagy Attila Gabor - Initial contribution
64  */
65 @NonNullByDefault
66 public abstract class AbstractSunSpecHandler extends BaseThingHandler {
67
68     /**
69      * Logger instance
70      */
71     private final Logger logger = LoggerFactory.getLogger(AbstractSunSpecHandler.class);
72
73     /**
74      * Configuration instance
75      */
76     protected @Nullable SunSpecConfiguration config = null;
77
78     /**
79      * This is the task used to poll the device
80      */
81     private volatile @Nullable PollTask pollTask = null;
82
83     /**
84      * Communication interface to the slave endpoint we're connecting to
85      */
86     protected volatile @Nullable ModbusCommunicationInterface comms = null;
87
88     /**
89      * This is the slave id, we store this once initialization is complete
90      */
91     private volatile int slaveId;
92
93     /**
94      * Instances of this handler should get a reference to the modbus manager
95      *
96      * @param thing the thing to handle
97      */
98     public AbstractSunSpecHandler(Thing thing) {
99         super(thing);
100     }
101
102     /**
103      * Handle incoming commands. This binding is read-only by default
104      */
105     @Override
106     public void handleCommand(ChannelUID channelUID, Command command) {
107         // Currently we do not support any commands
108     }
109
110     /**
111      * Initialization:
112      * Load the config object of the block
113      * Connect to the slave bridge
114      * Start the periodic polling
115      */
116     @Override
117     public void initialize() {
118         config = getConfigAs(SunSpecConfiguration.class);
119         logger.debug("Initializing thing with properties: {}", thing.getProperties());
120
121         startUp();
122     }
123
124     /*
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
129      */
130     private void startUp() {
131         connectEndpoint();
132
133         if (comms == null || config == null) {
134             logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
135             return;
136         }
137
138         if (pollTask != null) {
139             return;
140         }
141
142         // Try properties first
143         @Nullable
144         ModelBlock mainBlock = getAddressFromProperties();
145
146         if (mainBlock == null) {
147             mainBlock = getAddressFromConfig();
148         }
149
150         if (mainBlock != null) {
151             publishUniqueAddress(mainBlock);
152             updateStatus(ThingStatus.UNKNOWN);
153             registerPollTask(mainBlock);
154         } else {
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");
157             return;
158         }
159     }
160
161     /**
162      * Load and parse configuration from the properties
163      * These will be set by the auto discovery process
164      */
165     private @Nullable ModelBlock getAddressFromProperties() {
166         Map<String, String> properties = thing.getProperties();
167         if (!properties.containsKey(PROPERTY_BLOCK_ADDRESS) || !properties.containsKey(PROPERTY_BLOCK_LENGTH)) {
168             return null;
169         }
170         try {
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, ""));
174             return block;
175         } catch (NumberFormatException ex) {
176             logger.debug("Could not parse address and length properties, error: {}", ex.getMessage());
177             return null;
178         }
179     }
180
181     /**
182      * Load configuration from main configuration
183      */
184     private @Nullable ModelBlock getAddressFromConfig() {
185         @Nullable
186         SunSpecConfiguration myconfig = config;
187         if (myconfig == null) {
188             return null;
189         }
190         ModelBlock block = new ModelBlock();
191         block.address = myconfig.address;
192         block.length = myconfig.length;
193         return block;
194     }
195
196     /**
197      * Publish the unique address property if it has not been set before
198      */
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));
203             return;
204         }
205
206         ModbusEndpointThingHandler handler = getEndpointThingHandler();
207         if (handler == null) {
208             return;
209         }
210         getThing().setProperty(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
211     }
212
213     /**
214      * Dispose the binding correctly
215      */
216     @Override
217     public void dispose() {
218         tearDown();
219     }
220
221     /**
222      * Unregister the poll task and release the endpoint reference
223      */
224     private void tearDown() {
225         unregisterPollTask();
226         unregisterEndpoint();
227     }
228
229     /**
230      * Returns the current slave id from the bridge
231      */
232     public int getSlaveId() {
233         return slaveId;
234     }
235
236     /**
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
239      *
240      * @return the endpoint handler or null if the bridge does not exist
241      */
242     private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
243         Bridge bridge = getBridge();
244         if (bridge == null) {
245             logger.debug("Bridge is null");
246             return null;
247         }
248         if (bridge.getStatus() != ThingStatus.ONLINE) {
249             logger.debug("Bridge is not online");
250             return null;
251         }
252
253         ThingHandler handler = bridge.getHandler();
254         if (handler == null) {
255             logger.debug("Bridge handler is null");
256             return null;
257         }
258
259         if (handler instanceof ModbusEndpointThingHandler thingHandler) {
260             return thingHandler;
261         } else {
262             logger.debug("Unexpected bridge handler: {}", handler);
263             return null;
264         }
265     }
266
267     /**
268      * Get a reference to the modbus endpoint
269      */
270     private void connectEndpoint() {
271         if (comms != null) {
272             return;
273         }
274
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);
282             return;
283         }
284
285         try {
286             slaveId = slaveEndpointThingHandler.getSlaveId();
287             comms = slaveEndpointThingHandler.getCommunicationInterface();
288         } catch (EndpointNotInitializedException e) {
289             // this will be handled below as endpoint remains null
290         }
291
292         if (comms == 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);
298             return;
299         }
300     }
301
302     /**
303      * Remove the endpoint if exists
304      */
305     private void unregisterEndpoint() {
306         // Comms will be close()'d by endpoint thing handler
307         comms = null;
308     }
309
310     /**
311      * Register poll task
312      * This is where we set up our regular poller
313      */
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!");
318         }
319         @Nullable
320         ModbusCommunicationInterface mycomms = comms;
321         @Nullable
322         SunSpecConfiguration myconfig = config;
323         if (myconfig == null || mycomms == null) {
324             throw new IllegalStateException("registerPollTask called without proper configuration");
325         }
326
327         logger.debug("Setting up regular polling");
328
329         ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
330                 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
331
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);
337             }
338         }, this::handleError);
339     }
340
341     /**
342      * This method should handle incoming poll data, and update the channels
343      * with the values received
344      */
345     protected abstract void handlePolledData(ModbusRegisterArray registers);
346
347     @Override
348     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
349         super.bridgeStatusChanged(bridgeStatusInfo);
350
351         logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
352         if (getThing().getStatus() == ThingStatus.ONLINE) {
353             startUp();
354         } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
355             tearDown();
356         }
357     }
358
359     /**
360      * Unregister poll task.
361      *
362      * No-op in case no poll task is registered, or if the initialization is incomplete.
363      */
364     private synchronized void unregisterPollTask() {
365         @Nullable
366         PollTask task = pollTask;
367         if (task == null) {
368             return;
369         }
370         logger.debug("Unregistering polling from ModbusManager");
371         @Nullable
372         ModbusCommunicationInterface mycomms = comms;
373         if (mycomms != null) {
374             mycomms.unregisterRegularPoll(task);
375         }
376         pollTask = null;
377     }
378
379     /**
380      * Handle errors received during communication
381      */
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) {
385             return;
386         }
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));
391     }
392
393     /**
394      * Returns true, if we're in a CONFIGURATION_ERROR state
395      *
396      * @return
397      */
398     protected boolean hasConfigurationError() {
399         ThingStatusInfo statusInfo = getThing().getStatusInfo();
400         return statusInfo.getStatus() == ThingStatus.OFFLINE
401                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
402     }
403
404     /**
405      * Reset communication status to ONLINE if we're in an OFFLINE state
406      */
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);
412         }
413     }
414
415     /**
416      * Returns the channel UID for the specified group and channel id
417      *
418      * @param string the channel group
419      * @param string the channel id in that group
420      * @return the globally unique channel uid
421      */
422     ChannelUID channelUID(String group, String id) {
423         return new ChannelUID(getThing().getUID(), group, id);
424     }
425
426     /**
427      * Returns value multiplied by the 10 on the power of scaleFactory
428      *
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
432      */
433     protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
434         if (value.isEmpty() || scaleFactor.isEmpty()) {
435             return UnDefType.UNDEF;
436         }
437         return getScaled(value.get().longValue(), scaleFactor.get(), unit);
438     }
439
440     /**
441      * Returns value multiplied by the 10 on the power of scaleFactory
442      *
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
446      */
447     protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
448         return getScaled(value, Optional.of(scaleFactor), unit);
449     }
450
451     /**
452      * Returns value multiplied by the 10 on the power of scaleFactory
453      *
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
457      */
458     protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
459         if (scaleFactor == 0) {
460             return new QuantityType<>(value.longValue(), unit);
461         }
462         return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);
463     }
464 }