2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pixometer.handler;
15 import static org.openhab.binding.pixometer.internal.PixometerBindingConstants.*;
17 import java.io.IOException;
18 import java.time.Duration;
19 import java.util.Properties;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import javax.measure.quantity.Energy;
24 import javax.measure.quantity.Volume;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.pixometer.internal.config.PixometerMeterConfiguration;
29 import org.openhab.binding.pixometer.internal.config.ReadingInstance;
30 import org.openhab.binding.pixometer.internal.data.MeterState;
31 import org.openhab.binding.pixometer.internal.serializer.CustomReadingInstanceDeserializer;
32 import org.openhab.core.cache.ExpiringCache;
33 import org.openhab.core.io.net.http.HttpUtil;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.unit.SIUnits;
36 import org.openhab.core.library.unit.SmartHomeUnits;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingStatusInfo;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.Gson;
51 import com.google.gson.GsonBuilder;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonParser;
56 * The {@link MeterHandler} is responsible for handling data and measurements of a meter thing
58 * @author Jerome Luckenbach - Initial contribution
61 public class MeterHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(MeterHandler.class);
65 private static final String API_VERSION = "v1";
66 private static final String API_METER_ENDPOINT = "meters";
67 private static final String API_READINGS_ENDPOINT = "readings";
69 private final GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(ReadingInstance.class,
70 new CustomReadingInstanceDeserializer());
71 private final Gson gson = gsonBuilder.create();
72 private final JsonParser jsonParser = new JsonParser();
74 private @NonNullByDefault({}) String resourceID;
75 private @NonNullByDefault({}) String meterID;
76 private @NonNullByDefault({}) ExpiringCache<@Nullable MeterState> cache;
78 private @Nullable ScheduledFuture<?> pollingJob;
80 public MeterHandler(Thing thing) {
85 public void handleCommand(ChannelUID channelUID, Command command) {
87 if (command instanceof RefreshType) {
88 updateMeter(channelUID, cache.getValue());
90 logger.debug("The pixometer binding is read-only and can not handle command {}", command);
92 } catch (IOException e) {
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
98 public void initialize() {
99 logger.debug("Initializing Pixometer handler '{}'", getThing().getUID());
100 updateStatus(ThingStatus.UNKNOWN);
102 PixometerMeterConfiguration config = getConfigAs(PixometerMeterConfiguration.class);
103 setRessourceID(config.resourceId);
105 cache = new ExpiringCache<>(Duration.ofMinutes(60), this::refreshCache);
107 Bridge b = this.getBridge();
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110 "Could not find bridge (pixometer config). Did you choose one?");
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");
121 updateMeter(cache.getValue());
122 } catch (RuntimeException r) {
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
125 }, 2, pollingPeriod, TimeUnit.MINUTES);
126 logger.debug("Refresh job scheduled to run every {} minutes for '{}'", pollingPeriod, getThing().getUID());
130 * @return returns the auth token or null for error handling if the bridge was not found.
132 private @Nullable String getTokenFromBridge() {
133 Bridge b = this.getBridge();
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
136 "Could not find bridge (pixometer config). Did you choose one?");
140 return new StringBuilder("Bearer ").append(((AccountHandler) b.getHandler()).getAuthToken()).toString();
144 public void dispose() {
145 if (pollingJob != null) {
146 pollingJob.cancel(true);
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());
160 * Requests the corresponding meter data and stores the meterId internally for later usage
162 * @param token The current active auth token
164 private void obtainMeterId() {
166 String token = getTokenFromBridge();
169 throw new IOException(
170 "Auth token has not been delivered.\n API request can't get executed without authentication.");
173 String url = getApiString(API_METER_ENDPOINT);
175 Properties urlHeader = new Properties();
176 urlHeader.put("CONTENT-TYPE", "application/json");
177 urlHeader.put("Authorization", token);
179 String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
180 JsonObject responseJson = (JsonObject) jsonParser.parse(urlResponse);
182 if (responseJson.has("meter_id")) {
183 setMeterID(responseJson.get("meter_id").toString());
184 updateStatus(ThingStatus.ONLINE);
188 String errorMsg = String.format("Invalid Api Response ( %s )", responseJson);
190 throw new IOException(errorMsg);
191 } catch (IOException e) {
192 String errorMsg = String.format("Could not initialize Thing ( %s ). %s", this.getThing().getUID(),
195 logger.debug(errorMsg, e);
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
201 * Checks if a channel is linked and redirects to the updateMeter method if link is existing
203 * @param channelUID the channel requested for refresh
204 * @param meterState a meterState instance with current values
206 private void updateMeter(ChannelUID channelUID, @Nullable MeterState meterState) throws IOException {
207 if (!isLinked(channelUID)) {
208 throw new IOException("Channel is not linked.");
210 updateMeter(meterState);
214 * updates all corresponding channels
216 * @param token The current active access token
218 private void updateMeter(@Nullable MeterState meterState) {
220 if (meterState == null) {
221 throw new IOException("Meter state has not been delivered to update method. Can't update channels.");
224 ThingTypeUID thingtype = getThing().getThingTypeUID();
226 if (THING_TYPE_ENERGYMETER.equals(thingtype)) {
227 QuantityType<Energy> state = new QuantityType<>(meterState.getReadingValue(),
228 SmartHomeUnits.KILOWATT_HOUR);
229 updateState(CHANNEL_LAST_READING_VALUE, state);
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);
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);
244 private @Nullable MeterState refreshCache() {
246 String url = getApiString(API_READINGS_ENDPOINT);
248 Properties urlHeader = new Properties();
249 urlHeader.put("CONTENT-TYPE", "application/json");
250 urlHeader.put("Authorization", getTokenFromBridge());
252 String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
254 ReadingInstance latestReading = gson.fromJson(new JsonParser().parse(urlResponse), ReadingInstance.class);
256 return new MeterState(latestReading);
257 } catch (IOException e) {
258 logger.debug("Exception while refreshing cache for Meter {}: {}", getThing().getUID(), e.getMessage(), e);
264 * Generates a url string based on the given api endpoint
266 * @param endpoint The choosen api endpoint
267 * @return The generated url string
269 private String getApiString(String endpoint) {
270 StringBuilder sb = new StringBuilder(API_BASE_URL);
271 sb.append(API_VERSION).append("/");
274 case API_METER_ENDPOINT:
275 sb.append(API_METER_ENDPOINT).append("/");
276 sb.append(this.getRessourceID()).append("/?");
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("&");
285 sb.append("format=json");
286 return sb.toString();
290 * Getters and Setters
293 public String getRessourceID() {
297 private void setRessourceID(String ressourceID) {
298 this.resourceID = ressourceID;
301 public String getMeterID() {
305 private void setMeterID(String meterID) {
306 this.meterID = meterID;