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.Objects;
20 import java.util.Properties;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import javax.measure.quantity.Energy;
25 import javax.measure.quantity.Volume;
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;
51 import com.google.gson.Gson;
52 import com.google.gson.GsonBuilder;
53 import com.google.gson.JsonObject;
54 import com.google.gson.JsonParser;
57 * The {@link MeterHandler} is responsible for handling data and measurements of a meter thing
59 * @author Jerome Luckenbach - Initial contribution
62 public class MeterHandler extends BaseThingHandler {
64 private final Logger logger = LoggerFactory.getLogger(MeterHandler.class);
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";
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();
75 private @NonNullByDefault({}) String resourceID;
76 private @NonNullByDefault({}) String meterID;
77 private @NonNullByDefault({}) ExpiringCache<@Nullable MeterState> cache;
79 private @Nullable ScheduledFuture<?> pollingJob;
81 public MeterHandler(Thing thing) {
86 public void handleCommand(ChannelUID channelUID, Command command) {
88 if (command instanceof RefreshType) {
89 updateMeter(channelUID, cache.getValue());
91 logger.debug("The pixometer binding is read-only and can not handle command {}", command);
93 } catch (IOException e) {
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
99 public void initialize() {
100 logger.debug("Initializing Pixometer handler '{}'", getThing().getUID());
101 updateStatus(ThingStatus.UNKNOWN);
103 PixometerMeterConfiguration config = getConfigAs(PixometerMeterConfiguration.class);
104 setRessourceID(config.resourceId);
106 cache = new ExpiringCache<>(Duration.ofMinutes(60), this::refreshCache);
108 Bridge b = this.getBridge();
110 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
111 "Could not find bridge (pixometer config). Did you choose one?");
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");
122 updateMeter(cache.getValue());
123 } catch (RuntimeException r) {
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
126 }, 2, pollingPeriod, TimeUnit.MINUTES);
127 logger.debug("Refresh job scheduled to run every {} minutes for '{}'", pollingPeriod, getThing().getUID());
131 * @return returns the auth token or null for error handling if the bridge was not found.
133 private @Nullable String getTokenFromBridge() {
134 Bridge b = this.getBridge();
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137 "Could not find bridge (pixometer config). Did you choose one?");
141 return new StringBuilder("Bearer ").append(((AccountHandler) b.getHandler()).getAuthToken()).toString();
145 public void dispose() {
146 if (pollingJob != null) {
147 pollingJob.cancel(true);
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());
161 * Requests the corresponding meter data and stores the meterId internally for later usage
163 * @param token The current active auth token
165 private void obtainMeterId() {
167 String token = getTokenFromBridge();
170 throw new IOException(
171 "Auth token has not been delivered.\n API request can't get executed without authentication.");
174 String url = getApiString(API_METER_ENDPOINT);
176 Properties urlHeader = new Properties();
177 urlHeader.put("CONTENT-TYPE", "application/json");
178 urlHeader.put("Authorization", token);
180 String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
181 JsonObject responseJson = (JsonObject) jsonParser.parse(urlResponse);
183 if (responseJson.has("meter_id")) {
184 setMeterID(responseJson.get("meter_id").toString());
185 updateStatus(ThingStatus.ONLINE);
189 String errorMsg = String.format("Invalid Api Response ( %s )", responseJson);
191 throw new IOException(errorMsg);
192 } catch (IOException e) {
193 String errorMsg = String.format("Could not initialize Thing ( %s ). %s", this.getThing().getUID(),
196 logger.debug(errorMsg, e);
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
202 * Checks if a channel is linked and redirects to the updateMeter method if link is existing
204 * @param channelUID the channel requested for refresh
205 * @param meterState a meterState instance with current values
207 private void updateMeter(ChannelUID channelUID, @Nullable MeterState meterState) throws IOException {
208 if (!isLinked(channelUID)) {
209 throw new IOException("Channel is not linked.");
211 updateMeter(meterState);
215 * updates all corresponding channels
217 * @param token The current active access token
219 private void updateMeter(@Nullable MeterState meterState) {
221 if (meterState == null) {
222 throw new IOException("Meter state has not been delivered to update method. Can't update channels.");
225 ThingTypeUID thingtype = getThing().getThingTypeUID();
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);
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(Objects.requireNonNull(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;