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