]> git.basschouten.com Git - openhab-addons.git/blob
10648fbd67fdbcbd21ff6c6fd1b90149c7dc0707
[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.pixometer.handler;
14
15 import static org.openhab.binding.pixometer.internal.PixometerBindingConstants.*;
16
17 import java.io.IOException;
18 import java.time.Duration;
19 import java.util.Objects;
20 import java.util.Properties;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
24 import javax.measure.quantity.Energy;
25 import javax.measure.quantity.Volume;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.pixometer.internal.config.PixometerMeterConfiguration;
30 import org.openhab.binding.pixometer.internal.config.ReadingInstance;
31 import org.openhab.binding.pixometer.internal.data.MeterState;
32 import org.openhab.binding.pixometer.internal.serializer.CustomReadingInstanceDeserializer;
33 import org.openhab.core.cache.ExpiringCache;
34 import org.openhab.core.io.net.http.HttpUtil;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.library.unit.Units;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingStatusInfo;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 import com.google.gson.Gson;
52 import com.google.gson.GsonBuilder;
53 import com.google.gson.JsonObject;
54 import com.google.gson.JsonParser;
55
56 /**
57  * The {@link MeterHandler} is responsible for handling data and measurements of a meter thing
58  *
59  * @author Jerome Luckenbach - Initial contribution
60  */
61 @NonNullByDefault
62 public class MeterHandler extends BaseThingHandler {
63
64     private final Logger logger = LoggerFactory.getLogger(MeterHandler.class);
65
66     private static final String API_VERSION = "v1";
67     private static final String API_METER_ENDPOINT = "meters";
68     private static final String API_READINGS_ENDPOINT = "readings";
69
70     private final GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(ReadingInstance.class,
71             new CustomReadingInstanceDeserializer());
72     private final Gson gson = gsonBuilder.create();
73
74     private @NonNullByDefault({}) String resourceID;
75     private @NonNullByDefault({}) String meterID;
76     private @NonNullByDefault({}) ExpiringCache<@Nullable MeterState> cache;
77
78     private @Nullable ScheduledFuture<?> pollingJob;
79
80     public MeterHandler(Thing thing) {
81         super(thing);
82     }
83
84     @Override
85     public void handleCommand(ChannelUID channelUID, Command command) {
86         try {
87             if (command instanceof RefreshType) {
88                 updateMeter(channelUID, cache.getValue());
89             } else {
90                 logger.debug("The pixometer binding is read-only and can not handle command {}", command);
91             }
92         } catch (IOException e) {
93             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
94         }
95     }
96
97     @Override
98     public void initialize() {
99         logger.debug("Initializing Pixometer handler '{}'", getThing().getUID());
100         updateStatus(ThingStatus.UNKNOWN);
101
102         PixometerMeterConfiguration config = getConfigAs(PixometerMeterConfiguration.class);
103         setRessourceID(config.resourceId);
104
105         cache = new ExpiringCache<>(Duration.ofMinutes(60), this::refreshCache);
106
107         Bridge b = this.getBridge();
108         if (b == null) {
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110                     "Could not find bridge (pixometer config). Did you choose one?");
111             return;
112         }
113
114         obtainMeterId();
115
116         // Start polling job with the interval, that has been set up in the bridge
117         int pollingPeriod = Integer.parseInt(b.getConfiguration().get(CONFIG_BRIDGE_REFRESH).toString());
118         pollingJob = scheduler.scheduleWithFixedDelay(() -> {
119             logger.debug("Try to refresh meter data");
120             try {
121                 updateMeter(cache.getValue());
122             } catch (RuntimeException r) {
123                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
124             }
125         }, 2, pollingPeriod, TimeUnit.MINUTES);
126         logger.debug("Refresh job scheduled to run every {} minutes for '{}'", pollingPeriod, getThing().getUID());
127     }
128
129     /**
130      * @return returns the auth token or null for error handling if the bridge was not found.
131      */
132     private @Nullable String getTokenFromBridge() {
133         Bridge b = this.getBridge();
134         if (b == null) {
135             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
136                     "Could not find bridge (pixometer config). Did you choose one?");
137             return null;
138         }
139
140         return new StringBuilder("Bearer ").append(((AccountHandler) b.getHandler()).getAuthToken()).toString();
141     }
142
143     @Override
144     public void dispose() {
145         if (pollingJob != null) {
146             pollingJob.cancel(true);
147         }
148         super.dispose();
149     }
150
151     @Override
152     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
153         logger.debug("Bridge Status updated to {} for device: {}", bridgeStatusInfo.getStatus(), getThing().getUID());
154         if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
155             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, bridgeStatusInfo.getDescription());
156         }
157     }
158
159     /**
160      * Requests the corresponding meter data and stores the meterId internally for later usage
161      *
162      * @param token The current active auth token
163      */
164     private void obtainMeterId() {
165         try {
166             String token = getTokenFromBridge();
167
168             if (token == null) {
169                 throw new IOException(
170                         "Auth token has not been delivered.\n API request can't get executed without authentication.");
171             }
172
173             String url = getApiString(API_METER_ENDPOINT);
174
175             Properties urlHeader = new Properties();
176             urlHeader.put("CONTENT-TYPE", "application/json");
177             urlHeader.put("Authorization", token);
178
179             String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
180             JsonObject responseJson = (JsonObject) JsonParser.parseString(urlResponse);
181
182             if (responseJson.has("meter_id")) {
183                 setMeterID(responseJson.get("meter_id").toString());
184                 updateStatus(ThingStatus.ONLINE);
185                 return;
186             }
187
188             String errorMsg = String.format("Invalid Api Response ( %s )", responseJson);
189
190             throw new IOException(errorMsg);
191         } catch (IOException e) {
192             String errorMsg = String.format("Could not initialize Thing ( %s ). %s", this.getThing().getUID(),
193                     e.getMessage());
194
195             logger.debug(errorMsg, e);
196             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
197         }
198     }
199
200     /**
201      * Checks if a channel is linked and redirects to the updateMeter method if link is existing
202      *
203      * @param channelUID the channel requested for refresh
204      * @param meterState a meterState instance with current values
205      */
206     private void updateMeter(ChannelUID channelUID, @Nullable MeterState meterState) throws IOException {
207         if (!isLinked(channelUID)) {
208             throw new IOException("Channel is not linked.");
209         }
210         updateMeter(meterState);
211     }
212
213     /**
214      * updates all corresponding channels
215      *
216      * @param token The current active access token
217      */
218     private void updateMeter(@Nullable MeterState meterState) {
219         try {
220             if (meterState == null) {
221                 throw new IOException("Meter state has not been delivered to update method. Can't update channels.");
222             }
223
224             ThingTypeUID thingtype = getThing().getThingTypeUID();
225
226             if (THING_TYPE_ENERGYMETER.equals(thingtype)) {
227                 QuantityType<Energy> state = new QuantityType<>(meterState.getReadingValue(), Units.KILOWATT_HOUR);
228                 updateState(CHANNEL_LAST_READING_VALUE, state);
229             }
230
231             if (thingtype.equals(THING_TYPE_GASMETER) || thingtype.equals(THING_TYPE_WATERMETER)) {
232                 QuantityType<Volume> state = new QuantityType<>(meterState.getReadingValue(), SIUnits.CUBIC_METRE);
233                 updateState(CHANNEL_LAST_READING_VALUE, state);
234             }
235
236             updateState(CHANNEL_LAST_READING_DATE, meterState.getLastReadingDate());
237             updateState(CHANNEL_LAST_REFRESH_DATE, meterState.getLastRefreshTime());
238         } catch (IOException e) {
239             logger.debug("Exception while updating Meter {}: {}", getThing().getUID(), e.getMessage(), e);
240         }
241     }
242
243     private @Nullable MeterState refreshCache() {
244         try {
245             String url = getApiString(API_READINGS_ENDPOINT);
246
247             Properties urlHeader = new Properties();
248             urlHeader.put("CONTENT-TYPE", "application/json");
249             urlHeader.put("Authorization", getTokenFromBridge());
250
251             String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
252
253             ReadingInstance latestReading = gson.fromJson(JsonParser.parseString(urlResponse), ReadingInstance.class);
254
255             return new MeterState(Objects.requireNonNull(latestReading));
256         } catch (IOException e) {
257             logger.debug("Exception while refreshing cache for Meter {}: {}", getThing().getUID(), e.getMessage(), e);
258             return null;
259         }
260     }
261
262     /**
263      * Generates a url string based on the given api endpoint
264      *
265      * @param endpoint The choosen api endpoint
266      * @return The generated url string
267      */
268     private String getApiString(String endpoint) {
269         StringBuilder sb = new StringBuilder(API_BASE_URL);
270         sb.append(API_VERSION).append("/");
271
272         switch (endpoint) {
273             case API_METER_ENDPOINT:
274                 sb.append(API_METER_ENDPOINT).append("/");
275                 sb.append(this.getRessourceID()).append("/?");
276                 break;
277             case API_READINGS_ENDPOINT:
278                 sb.append(API_READINGS_ENDPOINT).append("/");
279                 sb.append("?meter_ressource_id=").append(this.getRessourceID());
280                 sb.append("&o=-reading_date").append("&");
281                 break;
282         }
283
284         sb.append("format=json");
285         return sb.toString();
286     }
287
288     /**
289      * Getters and Setters
290      */
291
292     public String getRessourceID() {
293         return resourceID;
294     }
295
296     private void setRessourceID(String ressourceID) {
297         this.resourceID = ressourceID;
298     }
299
300     public String getMeterID() {
301         return meterID;
302     }
303
304     private void setMeterID(String meterID) {
305         this.meterID = meterID;
306     }
307 }