]> git.basschouten.com Git - openhab-addons.git/blob
cf34f3a81c028cedfb37f3b9de5ce86c3f963aa5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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     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(), Units.KILOWATT_HOUR);
229                 updateState(CHANNEL_LAST_READING_VALUE, state);
230             }
231
232             if (thingtype.equals(THING_TYPE_GASMETER) || thingtype.equals(THING_TYPE_WATERMETER)) {
233                 QuantityType<Volume> state = new QuantityType<>(meterState.getReadingValue(), SIUnits.CUBIC_METRE);
234                 updateState(CHANNEL_LAST_READING_VALUE, state);
235             }
236
237             updateState(CHANNEL_LAST_READING_DATE, meterState.getLastReadingDate());
238             updateState(CHANNEL_LAST_REFRESH_DATE, meterState.getLastRefreshTime());
239         } catch (IOException e) {
240             logger.debug("Exception while updating Meter {}: {}", getThing().getUID(), e.getMessage(), e);
241         }
242     }
243
244     private @Nullable MeterState refreshCache() {
245         try {
246             String url = getApiString(API_READINGS_ENDPOINT);
247
248             Properties urlHeader = new Properties();
249             urlHeader.put("CONTENT-TYPE", "application/json");
250             urlHeader.put("Authorization", getTokenFromBridge());
251
252             String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
253
254             ReadingInstance latestReading = gson.fromJson(new JsonParser().parse(urlResponse), ReadingInstance.class);
255
256             return new MeterState(Objects.requireNonNull(latestReading));
257         } catch (IOException e) {
258             logger.debug("Exception while refreshing cache for Meter {}: {}", getThing().getUID(), e.getMessage(), e);
259             return null;
260         }
261     }
262
263     /**
264      * Generates a url string based on the given api endpoint
265      *
266      * @param endpoint The choosen api endpoint
267      * @return The generated url string
268      */
269     private String getApiString(String endpoint) {
270         StringBuilder sb = new StringBuilder(API_BASE_URL);
271         sb.append(API_VERSION).append("/");
272
273         switch (endpoint) {
274             case API_METER_ENDPOINT:
275                 sb.append(API_METER_ENDPOINT).append("/");
276                 sb.append(this.getRessourceID()).append("/?");
277                 break;
278             case API_READINGS_ENDPOINT:
279                 sb.append(API_READINGS_ENDPOINT).append("/");
280                 sb.append("?meter_ressource_id=").append(this.getRessourceID());
281                 sb.append("&o=-reading_date").append("&");
282                 break;
283         }
284
285         sb.append("format=json");
286         return sb.toString();
287     }
288
289     /**
290      * Getters and Setters
291      */
292
293     public String getRessourceID() {
294         return resourceID;
295     }
296
297     private void setRessourceID(String ressourceID) {
298         this.resourceID = ressourceID;
299     }
300
301     public String getMeterID() {
302         return meterID;
303     }
304
305     private void setMeterID(String meterID) {
306         this.meterID = meterID;
307     }
308 }