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