]> git.basschouten.com Git - openhab-addons.git/blob
6f23d701bbe1ba13f8998b9f05076ec7abad3834
[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      * @param managerRef the modbus manager
98      */
99     public AbstractSunSpecHandler(Thing thing) {
100         super(thing);
101     }
102
103     /**
104      * Handle incoming commands. This binding is read-only by default
105      */
106     @Override
107     public void handleCommand(ChannelUID channelUID, Command command) {
108         // Currently we do not support any commands
109     }
110
111     /**
112      * Initialization:
113      * Load the config object of the block
114      * Connect to the slave bridge
115      * Start the periodic polling
116      */
117     @Override
118     public void initialize() {
119         config = getConfigAs(SunSpecConfiguration.class);
120         logger.debug("Initializing thing with properties: {}", thing.getProperties());
121
122         startUp();
123     }
124
125     /*
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
130      */
131     private void startUp() {
132
133         connectEndpoint();
134
135         if (comms == null || config == null) {
136             logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
137             return;
138         }
139
140         if (pollTask != null) {
141             return;
142         }
143
144         // Try properties first
145         @Nullable
146         ModelBlock mainBlock = getAddressFromProperties();
147
148         if (mainBlock == null) {
149             mainBlock = getAddressFromConfig();
150         }
151
152         if (mainBlock != null) {
153             publishUniqueAddress(mainBlock);
154             updateStatus(ThingStatus.UNKNOWN);
155             registerPollTask(mainBlock);
156         } else {
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");
159             return;
160         }
161     }
162
163     /**
164      * Load and parse configuration from the properties
165      * These will be set by the auto discovery process
166      */
167     private @Nullable ModelBlock getAddressFromProperties() {
168         Map<String, String> properties = thing.getProperties();
169         if (!properties.containsKey(PROPERTY_BLOCK_ADDRESS) || !properties.containsKey(PROPERTY_BLOCK_LENGTH)) {
170             return null;
171         }
172         try {
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, ""));
176             return block;
177         } catch (NumberFormatException ex) {
178             logger.debug("Could not parse address and length properties, error: {}", ex.getMessage());
179             return null;
180         }
181     }
182
183     /**
184      * Load configuration from main configuration
185      */
186     private @Nullable ModelBlock getAddressFromConfig() {
187         @Nullable
188         SunSpecConfiguration myconfig = config;
189         if (myconfig == null) {
190             return null;
191         }
192         ModelBlock block = new ModelBlock();
193         block.address = myconfig.address;
194         block.length = myconfig.length;
195         return block;
196     }
197
198     /**
199      * Publish the unique address property if it has not been set before
200      */
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));
205             return;
206         }
207
208         ModbusEndpointThingHandler handler = getEndpointThingHandler();
209         if (handler == null) {
210             return;
211         }
212         getThing().setProperty(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
213     }
214
215     /**
216      * Dispose the binding correctly
217      */
218     @Override
219     public void dispose() {
220         tearDown();
221     }
222
223     /**
224      * Unregister the poll task and release the endpoint reference
225      */
226     private void tearDown() {
227         unregisterPollTask();
228         unregisterEndpoint();
229     }
230
231     /**
232      * Returns the current slave id from the bridge
233      */
234     public int getSlaveId() {
235         return slaveId;
236     }
237
238     /**
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
241      *
242      * @return the endpoint handler or null if the bridge does not exist
243      */
244     private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
245         Bridge bridge = getBridge();
246         if (bridge == null) {
247             logger.debug("Bridge is null");
248             return null;
249         }
250         if (bridge.getStatus() != ThingStatus.ONLINE) {
251             logger.debug("Bridge is not online");
252             return null;
253         }
254
255         ThingHandler handler = bridge.getHandler();
256         if (handler == null) {
257             logger.debug("Bridge handler is null");
258             return null;
259         }
260
261         if (handler instanceof ModbusEndpointThingHandler) {
262             ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
263             return slaveEndpoint;
264         } else {
265             logger.debug("Unexpected bridge handler: {}", handler);
266             return null;
267         }
268     }
269
270     /**
271      * Get a reference to the modbus endpoint
272      */
273     private void connectEndpoint() {
274         if (comms != null) {
275             return;
276         }
277
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);
285             return;
286         }
287
288         try {
289             slaveId = slaveEndpointThingHandler.getSlaveId();
290             comms = slaveEndpointThingHandler.getCommunicationInterface();
291         } catch (EndpointNotInitializedException e) {
292             // this will be handled below as endpoint remains null
293         }
294
295         if (comms == 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);
301             return;
302         }
303     }
304
305     /**
306      * Remove the endpoint if exists
307      */
308     private void unregisterEndpoint() {
309         // Comms will be close()'d by endpoint thing handler
310         comms = null;
311     }
312
313     /**
314      * Register poll task
315      * This is where we set up our regular poller
316      */
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!");
321         }
322         @Nullable
323         ModbusCommunicationInterface mycomms = comms;
324         @Nullable
325         SunSpecConfiguration myconfig = config;
326         if (myconfig == null || mycomms == null) {
327             throw new IllegalStateException("registerPollTask called without proper configuration");
328         }
329
330         logger.debug("Setting up regular polling");
331
332         ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
333                 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
334
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);
340             }
341         }, this::handleError);
342     }
343
344     /**
345      * This method should handle incoming poll data, and update the channels
346      * with the values received
347      */
348     protected abstract void handlePolledData(ModbusRegisterArray registers);
349
350     @Override
351     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
352         super.bridgeStatusChanged(bridgeStatusInfo);
353
354         logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
355         if (getThing().getStatus() == ThingStatus.ONLINE) {
356             startUp();
357         } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
358             tearDown();
359         }
360     }
361
362     /**
363      * Unregister poll task.
364      *
365      * No-op in case no poll task is registered, or if the initialization is incomplete.
366      */
367     private synchronized void unregisterPollTask() {
368         @Nullable
369         PollTask task = pollTask;
370         if (task == null) {
371             return;
372         }
373         logger.debug("Unregistering polling from ModbusManager");
374         @Nullable
375         ModbusCommunicationInterface mycomms = comms;
376         if (mycomms != null) {
377             mycomms.unregisterRegularPoll(task);
378         }
379         pollTask = null;
380     }
381
382     /**
383      * Handle errors received during communication
384      */
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) {
388             return;
389         }
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));
394     }
395
396     /**
397      * Returns true, if we're in a CONFIGURATION_ERROR state
398      *
399      * @return
400      */
401     protected boolean hasConfigurationError() {
402         ThingStatusInfo statusInfo = getThing().getStatusInfo();
403         return statusInfo.getStatus() == ThingStatus.OFFLINE
404                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
405     }
406
407     /**
408      * Reset communication status to ONLINE if we're in an OFFLINE state
409      */
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);
415         }
416     }
417
418     /**
419      * Returns the channel UID for the specified group and channel id
420      *
421      * @param string the channel group
422      * @param string the channel id in that group
423      * @return the globally unique channel uid
424      */
425     ChannelUID channelUID(String group, String id) {
426         return new ChannelUID(getThing().getUID(), group, id);
427     }
428
429     /**
430      * Returns value multiplied by the 10 on the power of scaleFactory
431      *
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
435      */
436     protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
437         if (!value.isPresent() || !scaleFactor.isPresent()) {
438             return UnDefType.UNDEF;
439         }
440         return getScaled(value.get().longValue(), scaleFactor.get(), unit);
441     }
442
443     /**
444      * Returns value multiplied by the 10 on the power of scaleFactory
445      *
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
449      */
450     protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
451         return getScaled(value, Optional.of(scaleFactor), unit);
452     }
453
454     /**
455      * Returns value multiplied by the 10 on the power of scaleFactory
456      *
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
460      */
461     protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
462         if (scaleFactor == 0) {
463             return new QuantityType<>(value.longValue(), unit);
464         }
465         return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);
466     }
467 }