2 * Copyright (c) 2010-2023 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();
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.parseString(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(), Units.KILOWATT_HOUR);
228 updateState(CHANNEL_LAST_READING_VALUE, state);
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);
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);
243 private @Nullable MeterState refreshCache() {
245 String url = getApiString(API_READINGS_ENDPOINT);
247 Properties urlHeader = new Properties();
248 urlHeader.put("CONTENT-TYPE", "application/json");
249 urlHeader.put("Authorization", getTokenFromBridge());
251 String urlResponse = HttpUtil.executeUrl("GET", url, urlHeader, null, null, 2000);
253 ReadingInstance latestReading = gson.fromJson(JsonParser.parseString(urlResponse), ReadingInstance.class);
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);
263 * Generates a url string based on the given api endpoint
265 * @param endpoint The choosen api endpoint
266 * @return The generated url string
268 private String getApiString(String endpoint) {
269 StringBuilder sb = new StringBuilder(API_BASE_URL);
270 sb.append(API_VERSION).append("/");
273 case API_METER_ENDPOINT:
274 sb.append(API_METER_ENDPOINT).append("/");
275 sb.append(this.getRessourceID()).append("/?");
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("&");
284 sb.append("format=json");
285 return sb.toString();
289 * Getters and Setters
292 public String getRessourceID() {
296 private void setRessourceID(String ressourceID) {
297 this.resourceID = ressourceID;
300 public String getMeterID() {
304 private void setMeterID(String meterID) {
305 this.meterID = meterID;