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