]> git.basschouten.com Git - openhab-addons.git/blob
3a23c0df2824396db2218644cb137de4c54cdf7c
[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 thingHandler) {
261             return thingHandler;
262         } else {
263             logger.debug("Unexpected bridge handler: {}", handler);
264             return null;
265         }
266     }
267
268     /**
269      * Get a reference to the modbus endpoint
270      */
271     private void connectEndpoint() {
272         if (comms != null) {
273             return;
274         }
275
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);
283             return;
284         }
285
286         try {
287             slaveId = slaveEndpointThingHandler.getSlaveId();
288             comms = slaveEndpointThingHandler.getCommunicationInterface();
289         } catch (EndpointNotInitializedException e) {
290             // this will be handled below as endpoint remains null
291         }
292
293         if (comms == 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);
299             return;
300         }
301     }
302
303     /**
304      * Remove the endpoint if exists
305      */
306     private void unregisterEndpoint() {
307         // Comms will be close()'d by endpoint thing handler
308         comms = null;
309     }
310
311     /**
312      * Register poll task
313      * This is where we set up our regular poller
314      */
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!");
319         }
320         @Nullable
321         ModbusCommunicationInterface mycomms = comms;
322         @Nullable
323         SunSpecConfiguration myconfig = config;
324         if (myconfig == null || mycomms == null) {
325             throw new IllegalStateException("registerPollTask called without proper configuration");
326         }
327
328         logger.debug("Setting up regular polling");
329
330         ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
331                 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
332
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);
338             }
339         }, this::handleError);
340     }
341
342     /**
343      * This method should handle incoming poll data, and update the channels
344      * with the values received
345      */
346     protected abstract void handlePolledData(ModbusRegisterArray registers);
347
348     @Override
349     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
350         super.bridgeStatusChanged(bridgeStatusInfo);
351
352         logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
353         if (getThing().getStatus() == ThingStatus.ONLINE) {
354             startUp();
355         } else if (getThing().getStatus() == ThingStatus.OFFLINE) {
356             tearDown();
357         }
358     }
359
360     /**
361      * Unregister poll task.
362      *
363      * No-op in case no poll task is registered, or if the initialization is incomplete.
364      */
365     private synchronized void unregisterPollTask() {
366         @Nullable
367         PollTask task = pollTask;
368         if (task == null) {
369             return;
370         }
371         logger.debug("Unregistering polling from ModbusManager");
372         @Nullable
373         ModbusCommunicationInterface mycomms = comms;
374         if (mycomms != null) {
375             mycomms.unregisterRegularPoll(task);
376         }
377         pollTask = null;
378     }
379
380     /**
381      * Handle errors received during communication
382      */
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) {
386             return;
387         }
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));
392     }
393
394     /**
395      * Returns true, if we're in a CONFIGURATION_ERROR state
396      *
397      * @return
398      */
399     protected boolean hasConfigurationError() {
400         ThingStatusInfo statusInfo = getThing().getStatusInfo();
401         return statusInfo.getStatus() == ThingStatus.OFFLINE
402                 && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
403     }
404
405     /**
406      * Reset communication status to ONLINE if we're in an OFFLINE state
407      */
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);
413         }
414     }
415
416     /**
417      * Returns the channel UID for the specified group and channel id
418      *
419      * @param string the channel group
420      * @param string the channel id in that group
421      * @return the globally unique channel uid
422      */
423     ChannelUID channelUID(String group, String id) {
424         return new ChannelUID(getThing().getUID(), group, id);
425     }
426
427     /**
428      * Returns value multiplied by the 10 on the power of scaleFactory
429      *
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
433      */
434     protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
435         if (value.isEmpty() || scaleFactor.isEmpty()) {
436             return UnDefType.UNDEF;
437         }
438         return getScaled(value.get().longValue(), scaleFactor.get(), unit);
439     }
440
441     /**
442      * Returns value multiplied by the 10 on the power of scaleFactory
443      *
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
447      */
448     protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
449         return getScaled(value, Optional.of(scaleFactor), unit);
450     }
451
452     /**
453      * Returns value multiplied by the 10 on the power of scaleFactory
454      *
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
458      */
459     protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
460         if (scaleFactor == 0) {
461             return new QuantityType<>(value.longValue(), unit);
462         }
463         return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);
464     }
465 }