2 * Copyright (c) 2010-2022 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.awattar.internal.handler;
15 import static org.eclipse.jetty.http.HttpMethod.GET;
16 import static org.eclipse.jetty.http.HttpStatus.OK_200;
17 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.BINDING_ID;
19 import java.time.Instant;
20 import java.time.LocalDate;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.util.SortedMap;
24 import java.util.TreeMap;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
35 import org.openhab.binding.awattar.internal.AwattarPrice;
36 import org.openhab.binding.awattar.internal.dto.AwattarApiData;
37 import org.openhab.binding.awattar.internal.dto.Datum;
38 import org.openhab.core.i18n.TimeZoneProvider;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.Gson;
50 import com.google.gson.JsonSyntaxException;
53 * The {@link AwattarBridgeHandler} is responsible for retrieving data from the aWATTar API.
55 * The API provides hourly prices for the current day and, starting from 14:00, hourly prices for the next day.
56 * Check the documentation at https://www.awattar.de/services/api
60 * @author Wolfgang Klimt - Initial contribution
63 public class AwattarBridgeHandler extends BaseBridgeHandler {
64 private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class);
65 private final HttpClient httpClient;
67 private ScheduledFuture<?> dataRefresher;
69 private static final String URLDE = "https://api.awattar.de/v1/marketdata";
70 private static final String URLAT = "https://api.awattar.at/v1/marketdata";
73 // This cache stores price data for up to two days
75 private SortedMap<Long, AwattarPrice> priceMap;
76 private final int dataRefreshInterval = 60;
77 private double vatFactor = 0;
78 private long lastUpdated = 0;
79 private double basePrice = 0;
80 private long minTimestamp = 0;
81 private long maxTimestamp = 0;
83 private TimeZoneProvider timeZoneProvider;
85 public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
87 this.httpClient = httpClient;
89 this.timeZoneProvider = timeZoneProvider;
90 zone = timeZoneProvider.getTimeZone();
94 public void initialize() {
95 updateStatus(ThingStatus.UNKNOWN);
96 AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class);
97 vatFactor = 1 + (config.vatPercent / 100);
98 basePrice = config.basePrice;
99 zone = timeZoneProvider.getTimeZone();
100 switch (config.country) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
109 "@text/error.unsupported.country");
113 dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, dataRefreshInterval * 1000,
114 TimeUnit.MILLISECONDS);
118 public void dispose() {
119 ScheduledFuture<?> localRefresher = dataRefresher;
120 if (localRefresher != null) {
121 localRefresher.cancel(true);
123 dataRefresher = null;
128 public void refreshIfNeeded() {
132 updateStatus(ThingStatus.ONLINE);
135 private void getPrices() {
137 // we start one day in the past to cover ranges that already started yesterday
138 ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1);
139 long start = zdt.toInstant().toEpochMilli();
140 // Starting from midnight yesterday we add three days so that the range covers the whole next day.
141 zdt = zdt.plusDays(3);
142 long end = zdt.toInstant().toEpochMilli();
144 StringBuilder request = new StringBuilder(url);
145 request.append("?start=").append(start).append("&end=").append(end);
147 logger.trace("aWATTar API request: = '{}'", request);
148 ContentResponse contentResponse = httpClient.newRequest(request.toString()).method(GET)
149 .timeout(10, TimeUnit.SECONDS).send();
150 int httpStatus = contentResponse.getStatus();
151 String content = contentResponse.getContentAsString();
152 logger.trace("aWATTar API response: status = {}, content = '{}'", httpStatus, content);
154 switch (httpStatus) {
156 Gson gson = new Gson();
157 SortedMap<Long, AwattarPrice> result = new TreeMap<>();
160 AwattarApiData apiData = gson.fromJson(content, AwattarApiData.class);
161 if (apiData != null) {
162 for (Datum d : apiData.data) {
163 result.put(d.startTimestamp,
164 new AwattarPrice(d.marketprice / 10.0, d.startTimestamp, d.endTimestamp, zone));
165 updateMin(d.startTimestamp);
166 updateMax(d.endTimestamp);
169 updateStatus(ThingStatus.ONLINE);
170 lastUpdated = Instant.now().toEpochMilli();
172 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
173 "@text/error.invalid.data");
178 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
179 "@text/warn.awattar.statuscode");
181 } catch (JsonSyntaxException e) {
182 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.json");
183 } catch (InterruptedException e) {
184 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.interrupted");
185 } catch (ExecutionException e) {
186 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.execution");
187 } catch (TimeoutException e) {
188 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.timeout");
192 private boolean needRefresh() {
193 if (getThing().getStatus() != ThingStatus.ONLINE) {
196 SortedMap<Long, AwattarPrice> localMap = priceMap;
197 if (localMap == null) {
200 return localMap.lastKey() < Instant.now().toEpochMilli() + 9 * 3600 * 1000;
203 private void refresh() {
207 public double getVatFactor() {
211 public double getBasePrice() {
215 public long getLastUpdated() {
219 public ZoneId getTimeZone() {
224 public synchronized SortedMap<Long, AwattarPrice> getPriceMap() {
225 if (priceMap == null) {
232 public AwattarPrice getPriceFor(long timestamp) {
233 SortedMap<Long, AwattarPrice> priceMap = getPriceMap();
234 if (priceMap == null) {
237 if (!containsPriceFor(timestamp)) {
240 for (AwattarPrice price : priceMap.values()) {
241 if (timestamp >= price.getStartTimestamp() && timestamp < price.getEndTimestamp()) {
248 public boolean containsPriceFor(long timestamp) {
249 return minTimestamp <= timestamp && maxTimestamp >= timestamp;
253 public void handleCommand(ChannelUID channelUID, Command command) {
254 if (command instanceof RefreshType) {
257 logger.debug("Binding {} only supports refresh command", BINDING_ID);
261 private void updateMin(long ts) {
262 minTimestamp = (minTimestamp == 0) ? ts : Math.min(minTimestamp, ts);
265 private void updateMax(long ts) {
266 maxTimestamp = (maxTimestamp == 0) ? ts : Math.max(ts, maxTimestamp);