]> git.basschouten.com Git - openhab-addons.git/blob
47c21c9560d3dc2bc337044a01036b4305313744
[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.dsmr.internal.handler;
14
15 import java.util.Collections;
16 import java.util.List;
17 import java.util.Map.Entry;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.dsmr.internal.device.cosem.CosemObject;
24 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
25 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
26 import org.openhab.binding.dsmr.internal.meter.DSMRMeter;
27 import org.openhab.binding.dsmr.internal.meter.DSMRMeterConfiguration;
28 import org.openhab.binding.dsmr.internal.meter.DSMRMeterConstants;
29 import org.openhab.binding.dsmr.internal.meter.DSMRMeterDescriptor;
30 import org.openhab.binding.dsmr.internal.meter.DSMRMeterType;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.ThingStatusInfo;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.openhab.core.types.State;
40 import org.openhab.core.types.UnDefType;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * The MeterHandler will create logic DSMR meter ThingTypes
46  *
47  * @author M. Volaart - Initial contribution
48  * @author Hilbrand Bouwkamp - Separated thing state update cycle from meter values received cycle
49  */
50 @NonNullByDefault
51 public class DSMRMeterHandler extends BaseThingHandler implements P1TelegramListener {
52
53     private final Logger logger = LoggerFactory.getLogger(DSMRMeterHandler.class);
54
55     /**
56      * The DSMRMeter instance.
57      */
58     private @NonNullByDefault({}) DSMRMeter meter;
59
60     /**
61      * Last received cosem objects.
62      */
63     private List<CosemObject> lastReceivedValues = Collections.emptyList();
64
65     /**
66      * Reference to the meter watchdog.
67      */
68     private @NonNullByDefault({}) ScheduledFuture<?> meterWatchdog;
69
70     /**
71      * The M-bus channel this meter is on, or if the channel is irrelevant is set to unknown channel
72      */
73     private int channel = DSMRMeterConstants.UNKNOWN_CHANNEL;
74
75     /**
76      * Creates a new MeterHandler for the given Thing.
77      *
78      * @param thing {@link Thing} to create the MeterHandler for
79      */
80     public DSMRMeterHandler(Thing thing) {
81         super(thing);
82     }
83
84     /**
85      * DSMR Meter don't support handling commands
86      */
87     @Override
88     public void handleCommand(ChannelUID channelUID, Command command) {
89         if (command == RefreshType.REFRESH) {
90             updateState();
91         }
92     }
93
94     /**
95      * Initializes a DSMR Meter
96      *
97      * This method will load the corresponding configuration
98      */
99     @Override
100     public void initialize() {
101         logger.debug("Initialize MeterHandler for Thing {}", getThing().getUID());
102         DSMRMeterType meterType;
103
104         try {
105             meterType = DSMRMeterType.valueOf(getThing().getThingTypeUID().getId().toUpperCase());
106         } catch (IllegalArgumentException iae) {
107             logger.warn(
108                     "{} could not be initialized due to an invalid meterType {}. Delete this Thing if the problem persists.",
109                     getThing(), getThing().getThingTypeUID().getId().toUpperCase());
110             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR,
111                     "@text/error.configuration.invalidmetertype");
112             return;
113         }
114         DSMRMeterConfiguration meterConfig = getConfigAs(DSMRMeterConfiguration.class);
115         channel = meterType.meterKind.isChannelRelevant() ? meterConfig.channel : DSMRMeterConstants.UNKNOWN_CHANNEL;
116         DSMRMeterDescriptor meterDescriptor = new DSMRMeterDescriptor(meterType, channel);
117         meter = new DSMRMeter(meterDescriptor);
118         meterWatchdog = scheduler.scheduleWithFixedDelay(this::updateState, meterConfig.refresh, meterConfig.refresh,
119                 TimeUnit.SECONDS);
120         updateStatus(ThingStatus.UNKNOWN);
121     }
122
123     @Override
124     public void dispose() {
125         if (meterWatchdog != null) {
126             meterWatchdog.cancel(false);
127             meterWatchdog = null;
128         }
129     }
130
131     /**
132      * Updates the state of all channels from the last received Cosem values from the meter. The lastReceivedValues are
133      * cleared after processing here so when it does contain values the next time this method is called and it contains
134      * values those are new values.
135      */
136     private synchronized void updateState() {
137         logger.trace("Update state for device: {}", getThing().getThingTypeUID().getId());
138         if (!lastReceivedValues.isEmpty()) {
139             for (final CosemObject cosemObject : lastReceivedValues) {
140                 for (final Entry<String, ? extends State> entry : cosemObject.getCosemValues().entrySet()) {
141                     final String channel = cosemObject.getType().name().toLowerCase()
142                             /* CosemObject has a specific sub channel if key not empty */
143                             + (entry.getKey().isEmpty() ? "" : "_" + entry.getKey());
144
145                     final State newState = entry.getValue();
146                     logger.debug("Updating state for channel {} to value {}", channel, newState);
147                     updateState(channel, newState);
148                 }
149             }
150             if (getThing().getStatus() != ThingStatus.ONLINE) {
151                 updateStatus(ThingStatus.ONLINE);
152             }
153             lastReceivedValues = Collections.emptyList();
154         }
155     }
156
157     /**
158      * Callback for received meter values. When this method is called but the telegram has no values for this meter this
159      * meter is set to offline because something is wrong, possible the meter has been removed.
160      *
161      * @param telegram The received telegram
162      */
163     @Override
164     public void telegramReceived(P1Telegram telegram) {
165         lastReceivedValues = Collections.emptyList();
166         final DSMRMeter localMeter = meter;
167
168         if (localMeter == null) {
169             return;
170         }
171         List<CosemObject> filteredValues = localMeter.filterMeterValues(telegram.getCosemObjects(), channel);
172
173         if (filteredValues.isEmpty()) {
174             if (getThing().getStatus() == ThingStatus.ONLINE) {
175                 setDeviceOffline(ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.thing.nodata");
176             }
177         } else {
178             if (logger.isTraceEnabled()) {
179                 logger.trace("Received {} objects for {}", filteredValues.size(), getThing().getThingTypeUID().getId());
180             }
181             lastReceivedValues = filteredValues;
182             if (getThing().getStatus() != ThingStatus.ONLINE) {
183                 updateState();
184             }
185         }
186     }
187
188     @Override
189     public synchronized void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
190         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
191                 && getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
192             // Set status to offline --> Thing will become online after receiving meter values
193             setDeviceOffline(ThingStatusDetail.NONE, null);
194         } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
195             setDeviceOffline(ThingStatusDetail.BRIDGE_OFFLINE, null);
196         }
197     }
198
199     /**
200      * @return Returns the {@link DSMRMeterDescriptor} this object is configured with
201      */
202     public @Nullable DSMRMeterDescriptor getMeterDescriptor() {
203         return meter == null ? null : meter.getMeterDescriptor();
204     }
205
206     /**
207      * Convenience method to set the meter off line.
208      *
209      * @param status off line status
210      * @param details off line detailed message
211      */
212     private void setDeviceOffline(ThingStatusDetail status, @Nullable String details) {
213         updateStatus(ThingStatus.OFFLINE, status, details);
214         getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.NULL));
215     }
216 }