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.tibber.internal.handler;
15 import static org.openhab.binding.tibber.internal.TibberBindingConstants.*;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.math.BigDecimal;
21 import java.net.URISyntaxException;
22 import java.util.Properties;
23 import java.util.concurrent.Future;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.util.ssl.SslContextFactory;
30 import org.eclipse.jetty.websocket.api.Session;
31 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
32 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
33 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
34 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
35 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
36 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
37 import org.eclipse.jetty.websocket.client.WebSocketClient;
38 import org.openhab.binding.tibber.internal.config.TibberConfiguration;
39 import org.openhab.core.io.net.http.HttpUtil;
40 import org.openhab.core.library.types.DateTimeType;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.library.unit.Units;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.ThingStatusInfo;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
56 import com.google.gson.JsonArray;
57 import com.google.gson.JsonObject;
58 import com.google.gson.JsonParser;
59 import com.google.gson.JsonSyntaxException;
62 * The {@link TibberHandler} is responsible for handling queries to/from Tibber API.
64 * @author Stian Kjoglum - Initial contribution
67 public class TibberHandler extends BaseThingHandler {
68 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
69 private final Logger logger = LoggerFactory.getLogger(TibberHandler.class);
70 private final Properties httpHeader = new Properties();
71 private TibberConfiguration tibberConfig = new TibberConfiguration();
72 private @Nullable SslContextFactory sslContextFactory;
73 private @Nullable TibberWebSocketListener socket;
74 private @Nullable Session session;
75 private @Nullable WebSocketClient client;
76 private @Nullable ScheduledFuture<?> pollingJob;
77 private @Nullable Future<?> sessionFuture;
78 private String rtEnabled = "false";
80 public TibberHandler(Thing thing) {
85 public void initialize() {
86 updateStatus(ThingStatus.UNKNOWN);
87 tibberConfig = getConfigAs(TibberConfiguration.class);
89 getTibberParameters();
90 startRefresh(tibberConfig.getRefresh());
94 public void handleCommand(ChannelUID channelUID, Command command) {
95 if (command instanceof RefreshType) {
96 startRefresh(tibberConfig.getRefresh());
98 logger.debug("Tibber API is read-only and does not handle commands");
102 public void getTibberParameters() {
103 String response = "";
105 httpHeader.put("cache-control", "no-cache");
106 httpHeader.put("content-type", JSON_CONTENT_TYPE);
107 httpHeader.put("Authorization", "Bearer " + tibberConfig.getToken());
109 TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
110 InputStream connectionStream = tibberQuery.connectionInputStream(tibberConfig.getHomeid());
111 response = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, connectionStream, null, REQUEST_TIMEOUT);
113 if (!response.contains("error") && !response.contains("<html>")) {
114 updateStatus(ThingStatus.ONLINE);
115 getURLInput(BASE_URL);
117 InputStream inputStream = tibberQuery.getRealtimeInputStream(tibberConfig.getHomeid());
118 String jsonResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, inputStream, null,
121 JsonObject object = (JsonObject) JsonParser.parseString(jsonResponse);
122 rtEnabled = object.getAsJsonObject("data").getAsJsonObject("viewer").getAsJsonObject("home")
123 .getAsJsonObject("features").get("realTimeConsumptionEnabled").toString();
125 if ("true".equals(rtEnabled)) {
126 logger.debug("Pulse associated with HomeId: Live stream will be started");
129 logger.debug("No Pulse associated with HomeId: No live stream will be started");
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
133 "Problems connecting/communicating with server: " + response);
135 } catch (IOException | JsonSyntaxException e) {
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
140 public void getURLInput(String url) throws IOException {
141 String jsonResponse = "";
142 TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
144 InputStream inputStream = tibberQuery.getInputStream(tibberConfig.getHomeid());
145 jsonResponse = HttpUtil.executeUrl("POST", url, httpHeader, inputStream, null, REQUEST_TIMEOUT);
146 logger.debug("API response: {}", jsonResponse);
148 if (!jsonResponse.contains("error") && !jsonResponse.contains("<html>")) {
149 if (getThing().getStatus() == ThingStatus.OFFLINE || getThing().getStatus() == ThingStatus.INITIALIZING) {
150 updateStatus(ThingStatus.ONLINE);
153 JsonObject rootJsonObject = (JsonObject) JsonParser.parseString(jsonResponse);
155 if (jsonResponse.contains("total")) {
157 JsonObject current = rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
158 .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
159 .getAsJsonObject("current");
161 updateState(CURRENT_TOTAL, new DecimalType(current.get("total").toString()));
162 String timestamp = current.get("startsAt").toString().substring(1, 20);
163 updateState(CURRENT_STARTSAT, new DateTimeType(timestamp));
164 updateState(CURRENT_LEVEL,
165 new StringType(current.get("level").toString().replaceAll("^\"|\"$", "")));
167 JsonArray tomorrow = rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
168 .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
169 .getAsJsonArray("tomorrow");
170 updateState(TOMORROW_PRICES, new StringType(tomorrow.toString()));
171 } catch (JsonSyntaxException e) {
172 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
173 "Error communicating with Tibber API: " + e.getMessage());
176 if (jsonResponse.contains("daily") && !jsonResponse.contains("\"daily\":{\"nodes\":[]")
177 && !jsonResponse.contains("\"daily\":null")) {
179 JsonObject myObject = (JsonObject) rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
180 .getAsJsonObject("home").getAsJsonObject("daily").getAsJsonArray("nodes").get(0);
182 String timestampDailyFrom = myObject.get("from").toString().substring(1, 20);
183 updateState(DAILY_FROM, new DateTimeType(timestampDailyFrom));
185 String timestampDailyTo = myObject.get("to").toString().substring(1, 20);
186 updateState(DAILY_TO, new DateTimeType(timestampDailyTo));
188 updateChannel(DAILY_COST, myObject.get("cost").toString());
189 updateChannel(DAILY_CONSUMPTION, myObject.get("consumption").toString());
191 } catch (JsonSyntaxException e) {
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193 "Error communicating with Tibber API: " + e.getMessage());
196 if (jsonResponse.contains("hourly") && !jsonResponse.contains("\"hourly\":{\"nodes\":[]")
197 && !jsonResponse.contains("\"hourly\":null")) {
199 JsonObject myObject = (JsonObject) rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
200 .getAsJsonObject("home").getAsJsonObject("hourly").getAsJsonArray("nodes").get(0);
202 String timestampHourlyFrom = myObject.get("from").toString().substring(1, 20);
203 updateState(HOURLY_FROM, new DateTimeType(timestampHourlyFrom));
205 String timestampHourlyTo = myObject.get("to").toString().substring(1, 20);
206 updateState(HOURLY_TO, new DateTimeType(timestampHourlyTo));
208 updateChannel(HOURLY_COST, myObject.get("cost").toString());
209 updateChannel(HOURLY_CONSUMPTION, myObject.get("consumption").toString());
211 } catch (JsonSyntaxException e) {
212 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
213 "Error communicating with Tibber API: " + e.getMessage());
216 } else if (jsonResponse.contains("error")) {
217 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
218 "Error in response from Tibber API: " + jsonResponse);
220 Thread.sleep(300 * 1000);
222 } catch (InterruptedException e) {
223 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
227 "Unexpected response from Tibber: " + jsonResponse);
229 Thread.sleep(300 * 1000);
231 } catch (InterruptedException e) {
232 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
237 public void startRefresh(int refresh) {
238 if (pollingJob == null) {
239 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
242 } catch (IOException e) {
243 logger.warn("IO Exception: {}", e.getMessage());
245 }, 1, refresh, TimeUnit.MINUTES);
249 public void updateRequest() throws IOException {
250 getURLInput(BASE_URL);
251 if ("true".equals(rtEnabled) && !isConnected()) {
252 logger.debug("Attempting to reopen Websocket connection");
257 public void updateChannel(String channelID, String channelValue) {
258 if (!channelValue.contains("null")) {
259 if (channelID.contains("consumption") || channelID.contains("Consumption")
260 || channelID.contains("accumulatedProduction")) {
261 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.KILOWATT_HOUR));
262 } else if (channelID.contains("power") || channelID.contains("Power")) {
263 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.WATT));
264 } else if (channelID.contains("voltage")) {
265 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.VOLT));
266 } else if (channelID.contains("current")) {
267 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.AMPERE));
269 updateState(channelID, new DecimalType(channelValue));
274 public void thingStatusChanged(ThingStatusInfo thingStatusInfo) {
275 logger.debug("Thing Status updated to {} for device: {}", thingStatusInfo.getStatus(), getThing().getUID());
276 if (thingStatusInfo.getStatus() != ThingStatus.ONLINE) {
277 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
278 "Unable to communicate with Tibber API");
283 public void dispose() {
284 ScheduledFuture<?> pollingJob = this.pollingJob;
285 if (pollingJob != null) {
286 pollingJob.cancel(true);
287 this.pollingJob = null;
291 WebSocketClient client = this.client;
292 if (client != null) {
294 logger.debug("DISPOSE - Stopping and Terminating Websocket connection");
296 } catch (Exception e) {
297 logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
307 WebSocketClient client = this.client;
308 if (client == null || !client.isRunning() || !isConnected()) {
309 if (client != null) {
312 } catch (Exception e) {
313 logger.warn("OPEN FRAME - Failed to stop websocket client: {}", e.getMessage());
317 sslContextFactory = new SslContextFactory.Client(true);
318 sslContextFactory.setTrustAll(true);
319 sslContextFactory.setEndpointIdentificationAlgorithm(null);
321 client = new WebSocketClient(sslContextFactory);
322 client.setMaxIdleTimeout(30 * 1000);
323 this.client = client;
325 TibberWebSocketListener socket = this.socket;
326 if (socket == null) {
327 logger.debug("New socket being created");
328 socket = new TibberWebSocketListener();
329 this.socket = socket;
332 ClientUpgradeRequest newRequest = new ClientUpgradeRequest();
333 newRequest.setHeader("Authorization", "Bearer " + tibberConfig.getToken());
334 newRequest.setSubProtocols("graphql-subscriptions");
337 logger.debug("Starting Websocket connection");
339 } catch (Exception e) {
340 logger.warn("Websocket Start Exception: {}", e.getMessage());
343 logger.debug("Connecting Websocket connection");
344 sessionFuture = client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest);
346 Thread.sleep(10 * 1000);
347 } catch (InterruptedException e) {
349 Session session = this.session;
350 if (!session.isOpen()) {
352 logger.warn("Unable to establish websocket session");
354 logger.debug("Websocket session established");
356 } catch (IOException e) {
357 logger.warn("Websocket Connect Exception: {}", e.getMessage());
358 } catch (URISyntaxException e) {
359 logger.warn("Websocket URI Exception: {}", e.getMessage());
362 logger.warn("Open: Websocket client already running");
366 public void close() {
367 Session session = this.session;
368 if (session != null) {
369 String disconnect = "{\"type\":\"connection_terminate\",\"payload\":null}";
371 TibberWebSocketListener socket = this.socket;
372 if (socket != null) {
373 logger.debug("Sending websocket disconnect message");
374 socket.sendMessage(disconnect);
376 logger.warn("Socket unable to send disconnect message: Socket is null");
378 } catch (IOException e) {
379 logger.warn("Websocket Close Exception: {}", e.getMessage());
383 } catch (Exception e) {
384 logger.warn("Unable to disconnect session");
389 Future<?> sessionFuture = this.sessionFuture;
390 if (sessionFuture != null && !sessionFuture.isDone()) {
391 sessionFuture.cancel(true);
393 WebSocketClient client = this.client;
394 if (client != null) {
397 } catch (Exception e) {
398 logger.warn("CLOSE FRAME - Failed to stop websocket client: {}", e.getMessage());
404 public boolean isConnected() {
405 Session session = this.session;
406 return session != null && session.isOpen();
411 public class TibberWebSocketListener {
414 public void onConnect(Session wssession) {
415 TibberHandler.this.session = wssession;
416 TibberWebSocketListener socket = TibberHandler.this.socket;
417 String connection = "{\"type\":\"connection_init\", \"payload\":\"token=" + tibberConfig.getToken() + "\"}";
419 if (socket != null) {
420 logger.debug("Sending websocket connect message");
421 socket.sendMessage(connection);
423 logger.debug("Socket unable to send connect message: Socket is null");
425 } catch (IOException e) {
426 logger.warn("Send Message Exception: {}", e.getMessage());
431 public void onClose(int statusCode, String reason) {
432 logger.debug("Closing a WebSocket due to {}", reason);
433 WebSocketClient client = TibberHandler.this.client;
434 if (client != null && client.isRunning()) {
436 logger.debug("ONCLOSE - Stopping and Terminating Websocket connection");
438 } catch (Exception e) {
439 logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
445 public void onWebSocketError(Throwable e) {
446 String message = e.getMessage();
447 logger.debug("Error during websocket communication: {}", message);
452 public void onMessage(String message) {
453 if (message.contains("connection_ack")) {
454 logger.debug("Connected to Server");
456 } else if (message.contains("error") || message.contains("terminate")) {
457 logger.debug("Error/terminate received from server: {}", message);
459 } else if (message.contains("liveMeasurement")) {
460 JsonObject object = (JsonObject) JsonParser.parseString(message);
461 JsonObject myObject = object.getAsJsonObject("payload").getAsJsonObject("data")
462 .getAsJsonObject("liveMeasurement");
463 if (myObject.has("timestamp")) {
464 String liveTimestamp = myObject.get("timestamp").toString().substring(1, 20);
465 updateState(LIVE_TIMESTAMP, new DateTimeType(liveTimestamp));
467 if (myObject.has("power")) {
468 updateChannel(LIVE_POWER, myObject.get("power").toString());
470 if (myObject.has("lastMeterConsumption")) {
471 updateChannel(LIVE_LASTMETERCONSUMPTION, myObject.get("lastMeterConsumption").toString());
473 if (myObject.has("accumulatedConsumption")) {
474 updateChannel(LIVE_ACCUMULATEDCONSUMPTION, myObject.get("accumulatedConsumption").toString());
476 if (myObject.has("accumulatedCost")) {
477 updateChannel(LIVE_ACCUMULATEDCOST, myObject.get("accumulatedCost").toString());
479 if (myObject.has("currency")) {
480 updateState(LIVE_CURRENCY, new StringType(myObject.get("currency").toString()));
482 if (myObject.has("minPower")) {
483 updateChannel(LIVE_MINPOWER, myObject.get("minPower").toString());
485 if (myObject.has("averagePower")) {
486 updateChannel(LIVE_AVERAGEPOWER, myObject.get("averagePower").toString());
488 if (myObject.has("maxPower")) {
489 updateChannel(LIVE_MAXPOWER, myObject.get("maxPower").toString());
491 if (myObject.has("voltagePhase1")) {
492 updateChannel(LIVE_VOLTAGE1, myObject.get("voltagePhase1").toString());
494 if (myObject.has("voltagePhase2")) {
495 updateChannel(LIVE_VOLTAGE2, myObject.get("voltagePhase2").toString());
497 if (myObject.has("voltagePhase3")) {
498 updateChannel(LIVE_VOLTAGE3, myObject.get("voltagePhase3").toString());
500 if (myObject.has("currentL1")) {
501 updateChannel(LIVE_CURRENT1, myObject.get("currentL1").toString());
503 if (myObject.has("currentL2")) {
504 updateChannel(LIVE_CURRENT2, myObject.get("currentL2").toString());
506 if (myObject.has("currentL3")) {
507 updateChannel(LIVE_CURRENT3, myObject.get("currentL3").toString());
509 if (myObject.has("powerProduction")) {
510 updateChannel(LIVE_POWERPRODUCTION, myObject.get("powerProduction").toString());
512 if (myObject.has("accumulatedProduction")) {
513 updateChannel(LIVE_ACCUMULATEDPRODUCTION, myObject.get("accumulatedProduction").toString());
515 if (myObject.has("minPowerProduction")) {
516 updateChannel(LIVE_MINPOWERPRODUCTION, myObject.get("minPowerProduction").toString());
518 if (myObject.has("maxPowerProduction")) {
519 updateChannel(LIVE_MAXPOWERPRODUCTION, myObject.get("maxPowerProduction").toString());
522 logger.debug("Unknown live response from Tibber");
526 private void sendMessage(String message) throws IOException {
527 logger.debug("Send message: {}", message);
528 Session session = TibberHandler.this.session;
529 if (session != null) {
530 session.getRemote().sendString(message);
534 public void startSubscription() {
535 String query = "{\"id\":\"1\",\"type\":\"start\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":null,\"query\":\"subscription {\\n liveMeasurement(homeId:\\\""
536 + tibberConfig.getHomeid()
537 + "\\\") {\\n timestamp\\n power\\n lastMeterConsumption\\n accumulatedConsumption\\n accumulatedCost\\n currency\\n minPower\\n averagePower\\n maxPower\\n"
538 + "voltagePhase1\\n voltagePhase2\\n voltagePhase3\\n currentL1\\n currentL2\\n currentL3\\n powerProduction\\n accumulatedProduction\\n minPowerProduction\\n maxPowerProduction\\n }\\n }\\n\"}}";
540 TibberWebSocketListener socket = TibberHandler.this.socket;
541 if (socket != null) {
542 socket.sendMessage(query);
544 logger.debug("Socket unable to send subscription message: Socket is null");
546 } catch (IOException e) {
547 logger.warn("Send Message Exception: {}", e.getMessage());