2 * Copyright (c) 2010-2021 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.Executor;
24 import java.util.concurrent.Future;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.util.ssl.SslContextFactory;
31 import org.eclipse.jetty.websocket.api.Session;
32 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
33 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
34 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
35 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
36 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
37 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
38 import org.eclipse.jetty.websocket.client.WebSocketClient;
39 import org.openhab.binding.tibber.internal.config.TibberConfiguration;
40 import org.openhab.core.common.ThreadPoolManager;
41 import org.openhab.core.io.net.http.HttpUtil;
42 import org.openhab.core.library.types.DateTimeType;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.library.unit.Units;
47 import org.openhab.core.thing.ChannelUID;
48 import org.openhab.core.thing.Thing;
49 import org.openhab.core.thing.ThingStatus;
50 import org.openhab.core.thing.ThingStatusDetail;
51 import org.openhab.core.thing.ThingStatusInfo;
52 import org.openhab.core.thing.binding.BaseThingHandler;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
58 import com.google.gson.JsonObject;
59 import com.google.gson.JsonParser;
60 import com.google.gson.JsonSyntaxException;
63 * The {@link TibberHandler} is responsible for handling queries to/from Tibber API.
65 * @author Stian Kjoglum - Initial contribution
68 public class TibberHandler extends BaseThingHandler {
69 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
70 private final Logger logger = LoggerFactory.getLogger(TibberHandler.class);
71 private final Properties httpHeader = new Properties();
72 private final SslContextFactory sslContextFactory = new SslContextFactory(true);
73 private final Executor websocketExecutor = ThreadPoolManager.getPool("tibber.websocket");
74 private TibberConfiguration tibberConfig = new TibberConfiguration();
75 private @Nullable TibberWebSocketListener socket;
76 private @Nullable Session session;
77 private @Nullable WebSocketClient client;
78 private @Nullable ScheduledFuture<?> pollingJob;
79 private @Nullable Future<?> sessionFuture;
80 private String rtEnabled = "false";
82 public TibberHandler(Thing thing) {
87 public void initialize() {
88 updateStatus(ThingStatus.UNKNOWN);
89 tibberConfig = getConfigAs(TibberConfiguration.class);
91 getTibberParameters();
92 startRefresh(tibberConfig.getRefresh());
96 public void handleCommand(ChannelUID channelUID, Command command) {
97 if (command instanceof RefreshType) {
98 startRefresh(tibberConfig.getRefresh());
100 logger.debug("Tibber API is read-only and does not handle commands");
104 public void getTibberParameters() {
106 httpHeader.put("cache-control", "no-cache");
107 httpHeader.put("content-type", JSON_CONTENT_TYPE);
108 httpHeader.put("Authorization", "Bearer " + tibberConfig.getToken());
110 TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
111 InputStream connectionStream = tibberQuery.connectionInputStream(tibberConfig.getHomeid());
112 String response = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, connectionStream, null,
115 if (!response.contains("error") && !response.contains("<html>")) {
116 updateStatus(ThingStatus.ONLINE);
118 getURLInput(BASE_URL);
120 InputStream inputStream = tibberQuery.getRealtimeInputStream(tibberConfig.getHomeid());
121 String jsonResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, inputStream, null,
124 JsonObject object = (JsonObject) new JsonParser().parse(jsonResponse);
125 rtEnabled = object.getAsJsonObject("data").getAsJsonObject("viewer").getAsJsonObject("home")
126 .getAsJsonObject("features").get("realTimeConsumptionEnabled").toString();
128 if ("true".equals(rtEnabled)) {
129 logger.debug("Pulse associated with HomeId: Live stream will be started");
132 logger.debug("No Pulse associated with HomeId: No live stream will be started");
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
136 "Problems connecting/communicating with server: " + response);
138 } catch (IOException | JsonSyntaxException e) {
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
143 public void getURLInput(String url) throws IOException {
144 TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
146 InputStream inputStream = tibberQuery.getInputStream(tibberConfig.getHomeid());
147 String jsonResponse = HttpUtil.executeUrl("POST", url, httpHeader, inputStream, null, REQUEST_TIMEOUT);
148 logger.debug("API response: {}", jsonResponse);
150 if (!jsonResponse.contains("error") && !jsonResponse.contains("<html>")) {
151 if (getThing().getStatus() == ThingStatus.OFFLINE || getThing().getStatus() == ThingStatus.INITIALIZING) {
152 updateStatus(ThingStatus.ONLINE);
155 JsonObject object = (JsonObject) new JsonParser().parse(jsonResponse);
157 if (jsonResponse.contains("total")) {
159 JsonObject myObject = object.getAsJsonObject("data").getAsJsonObject("viewer")
160 .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
161 .getAsJsonObject("current");
163 updateState(CURRENT_TOTAL, new DecimalType(myObject.get("total").toString()));
164 String timestamp = myObject.get("startsAt").toString().substring(1, 20);
165 updateState(CURRENT_STARTSAT, new DateTimeType(timestamp));
167 } catch (JsonSyntaxException e) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
169 "Error communicating with Tibber API: " + e.getMessage());
172 if (jsonResponse.contains("daily")) {
174 JsonObject myObject = (JsonObject) object.getAsJsonObject("data").getAsJsonObject("viewer")
175 .getAsJsonObject("home").getAsJsonObject("daily").getAsJsonArray("nodes").get(0);
177 String timestampDailyFrom = myObject.get("from").toString().substring(1, 20);
178 updateState(DAILY_FROM, new DateTimeType(timestampDailyFrom));
180 String timestampDailyTo = myObject.get("to").toString().substring(1, 20);
181 updateState(DAILY_TO, new DateTimeType(timestampDailyTo));
183 updateChannel(DAILY_COST, myObject.get("cost").toString());
184 updateChannel(DAILY_CONSUMPTION, myObject.get("consumption").toString());
186 } catch (JsonSyntaxException e) {
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
188 "Error communicating with Tibber API: " + e.getMessage());
191 if (jsonResponse.contains("hourly")) {
193 JsonObject myObject = (JsonObject) object.getAsJsonObject("data").getAsJsonObject("viewer")
194 .getAsJsonObject("home").getAsJsonObject("hourly").getAsJsonArray("nodes").get(0);
196 String timestampHourlyFrom = myObject.get("from").toString().substring(1, 20);
197 updateState(HOURLY_FROM, new DateTimeType(timestampHourlyFrom));
199 String timestampHourlyTo = myObject.get("to").toString().substring(1, 20);
200 updateState(HOURLY_TO, new DateTimeType(timestampHourlyTo));
202 updateChannel(HOURLY_COST, myObject.get("cost").toString());
203 updateChannel(HOURLY_CONSUMPTION, myObject.get("consumption").toString());
205 } catch (JsonSyntaxException e) {
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
207 "Error communicating with Tibber API: " + e.getMessage());
210 } else if (jsonResponse.contains("error")) {
211 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
212 "Error in response from Tibber API: " + jsonResponse);
214 Thread.sleep(300 * 1000);
216 } catch (InterruptedException e) {
217 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
220 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
221 "Unexpected response from Tibber: " + jsonResponse);
223 Thread.sleep(300 * 1000);
225 } catch (InterruptedException e) {
226 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
231 public void startRefresh(int refresh) {
232 if (pollingJob == null) {
233 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
236 } catch (IOException e) {
237 logger.warn("IO Exception: {}", e.getMessage());
239 }, 1, refresh, TimeUnit.MINUTES);
243 public void updateRequest() throws IOException {
244 getURLInput(BASE_URL);
245 if ("true".equals(rtEnabled) && !isConnected()) {
246 logger.debug("Attempting to reopen Websocket connection");
251 public void updateChannel(String channelID, String channelValue) {
252 if (!channelValue.contains("null")) {
253 if (channelID.contains("consumption") || channelID.contains("Consumption")
254 || channelID.contains("accumulatedProduction")) {
255 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.KILOWATT_HOUR));
256 } else if (channelID.contains("power") || channelID.contains("Power")) {
257 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.WATT));
258 } else if (channelID.contains("voltage")) {
259 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.VOLT));
260 } else if (channelID.contains("live_current")) {
261 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.AMPERE));
263 updateState(channelID, new DecimalType(channelValue));
268 public void thingStatusChanged(ThingStatusInfo thingStatusInfo) {
269 logger.debug("Thing Status updated to {} for device: {}", thingStatusInfo.getStatus(), getThing().getUID());
270 if (thingStatusInfo.getStatus() != ThingStatus.ONLINE) {
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
272 "Unable to communicate with Tibber API");
277 public void dispose() {
278 ScheduledFuture<?> pollingJob = this.pollingJob;
279 if (pollingJob != null) {
280 pollingJob.cancel(true);
281 this.pollingJob = null;
285 WebSocketClient client = this.client;
286 if (client != null) {
288 logger.debug("Stopping and Terminating Websocket connection");
291 } catch (Exception e) {
292 logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
302 logger.debug("Open: connection is already open");
304 sslContextFactory.setTrustAll(true);
305 sslContextFactory.setEndpointIdentificationAlgorithm(null);
307 WebSocketClient client = this.client;
308 if (client == null) {
309 client = new WebSocketClient(sslContextFactory, websocketExecutor);
310 this.client = client;
313 TibberWebSocketListener socket = this.socket;
314 if (socket == null) {
315 socket = new TibberWebSocketListener();
316 this.socket = socket;
319 ClientUpgradeRequest newRequest = new ClientUpgradeRequest();
320 newRequest.setHeader("Authorization", "Bearer " + tibberConfig.getToken());
321 newRequest.setSubProtocols("graphql-subscriptions");
324 logger.debug("Starting Websocket connection");
326 } catch (Exception e) {
327 logger.warn("Websocket Start Exception: {}", e.getMessage());
330 logger.debug("Connecting Websocket connection");
331 sessionFuture = client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest);
332 } catch (IOException e) {
333 logger.warn("Websocket Connect Exception: {}", e.getMessage());
334 } catch (URISyntaxException e) {
335 logger.warn("Websocket URI Exception: {}", e.getMessage());
340 public void close() {
341 Session session = this.session;
342 if (session != null) {
343 String disconnect = "{\"type\":\"connection_terminate\",\"payload\":null}";
345 TibberWebSocketListener socket = this.socket;
346 if (socket != null) {
347 logger.debug("Sending websocket disconnect message");
348 socket.sendMessage(disconnect);
350 logger.debug("Socket unable to send disconnect message: Socket is null");
352 } catch (IOException e) {
353 logger.warn("Websocket Close Exception: {}", e.getMessage());
355 session.close(0, "Tibber websocket disposed");
359 Future<?> sessionFuture = this.sessionFuture;
360 if (sessionFuture != null && !sessionFuture.isDone()) {
361 sessionFuture.cancel(true);
363 WebSocketClient client = this.client;
364 if (client != null) {
367 } catch (Exception e) {
368 logger.warn("Failed to stop websocket client: {}", e.getMessage());
373 public boolean isConnected() {
374 Session session = this.session;
375 return session != null && session.isOpen();
380 public class TibberWebSocketListener {
383 public void onConnect(Session wssession) {
384 TibberHandler.this.session = wssession;
385 TibberWebSocketListener socket = TibberHandler.this.socket;
386 String connection = "{\"type\":\"connection_init\", \"payload\":\"token=" + tibberConfig.getToken() + "\"}";
388 if (socket != null) {
389 logger.debug("Sending websocket connect message");
390 socket.sendMessage(connection);
392 logger.debug("Socket unable to send connect message: Socket is null");
394 } catch (IOException e) {
395 logger.warn("Send Message Exception: {}", e.getMessage());
400 public void onClose(int statusCode, String reason) {
401 logger.debug("Closing a WebSocket due to {}", reason);
402 WebSocketClient client = TibberHandler.this.client;
403 if (client != null && client.isRunning()) {
405 logger.debug("Stopping and Terminating Websocket connection");
408 } catch (Exception e) {
409 logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
412 TibberHandler.this.session = null;
413 TibberHandler.this.client = null;
414 TibberHandler.this.socket = null;
418 public void onWebSocketError(Throwable e) {
419 String message = e.getMessage();
420 logger.debug("Error during websocket communication: {}", message);
421 onClose(0, message != null ? message : "null");
425 public void onMessage(String message) {
426 if (message.contains("connection_ack")) {
427 logger.debug("Connected to Server");
429 } else if (message.contains("error") || message.contains("terminate")) {
430 logger.debug("Error/terminate received from server: {}", message);
432 } else if (message.contains("liveMeasurement")) {
433 JsonObject object = (JsonObject) new JsonParser().parse(message);
434 JsonObject myObject = object.getAsJsonObject("payload").getAsJsonObject("data")
435 .getAsJsonObject("liveMeasurement");
436 if (myObject.has("timestamp")) {
437 String liveTimestamp = myObject.get("timestamp").toString().substring(1, 20);
438 updateState(LIVE_TIMESTAMP, new DateTimeType(liveTimestamp));
440 if (myObject.has("power")) {
441 updateChannel(LIVE_POWER, myObject.get("power").toString());
443 if (myObject.has("lastMeterConsumption")) {
444 updateChannel(LIVE_LASTMETERCONSUMPTION, myObject.get("lastMeterConsumption").toString());
446 if (myObject.has("accumulatedConsumption")) {
447 updateChannel(LIVE_ACCUMULATEDCONSUMPTION, myObject.get("accumulatedConsumption").toString());
449 if (myObject.has("accumulatedCost")) {
450 updateChannel(LIVE_ACCUMULATEDCOST, myObject.get("accumulatedCost").toString());
452 if (myObject.has("currency")) {
453 updateState(LIVE_CURRENCY, new StringType(myObject.get("currency").toString()));
455 if (myObject.has("minPower")) {
456 updateChannel(LIVE_MINPOWER, myObject.get("minPower").toString());
458 if (myObject.has("averagePower")) {
459 updateChannel(LIVE_AVERAGEPOWER, myObject.get("averagePower").toString());
461 if (myObject.has("maxPower")) {
462 updateChannel(LIVE_MAXPOWER, myObject.get("maxPower").toString());
464 if (myObject.has("voltagePhase1")) {
465 updateChannel(LIVE_VOLTAGE1, myObject.get("voltagePhase1").toString());
467 if (myObject.has("voltagePhase2")) {
468 updateChannel(LIVE_VOLTAGE2, myObject.get("voltagePhase2").toString());
470 if (myObject.has("voltagePhase3")) {
471 updateChannel(LIVE_VOLTAGE3, myObject.get("voltagePhase3").toString());
473 if (myObject.has("currentPhase1")) {
474 updateChannel(LIVE_CURRENT1, myObject.get("currentPhase1").toString());
476 if (myObject.has("currentPhase2")) {
477 updateChannel(LIVE_CURRENT2, myObject.get("currentPhase2").toString());
479 if (myObject.has("currentPhase3")) {
480 updateChannel(LIVE_CURRENT3, myObject.get("currentPhase3").toString());
482 if (myObject.has("powerProduction")) {
483 updateChannel(LIVE_POWERPRODUCTION, myObject.get("powerProduction").toString());
485 if (myObject.has("accumulatedProduction")) {
486 updateChannel(LIVE_ACCUMULATEDPRODUCTION, myObject.get("accumulatedProduction").toString());
488 if (myObject.has("minPowerProduction")) {
489 updateChannel(LIVE_MINPOWERPRODUCTION, myObject.get("minPowerProduction").toString());
491 if (myObject.has("maxPowerProduction")) {
492 updateChannel(LIVE_MAXPOWERPRODUCTION, myObject.get("maxPowerProduction").toString());
495 logger.debug("Unknown live response from Tibber");
499 private void sendMessage(String message) throws IOException {
500 logger.debug("Send message: {}", message);
501 Session session = TibberHandler.this.session;
502 if (session != null) {
503 session.getRemote().sendString(message);
507 public void startSubscription() {
508 String query = "{\"id\":\"1\",\"type\":\"start\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":null,\"query\":\"subscription {\\n liveMeasurement(homeId:\\\""
509 + tibberConfig.getHomeid()
510 + "\\\") {\\n timestamp\\n power\\n lastMeterConsumption\\n accumulatedConsumption\\n accumulatedCost\\n currency\\n minPower\\n averagePower\\n maxPower\\n"
511 + "voltagePhase1\\n voltagePhase2\\n voltagePhase3\\n currentPhase1\\n currentPhase2\\n currentPhase3\\n powerProduction\\n accumulatedProduction\\n minPowerProduction\\n maxPowerProduction\\n }\\n }\\n\"}}";
513 TibberWebSocketListener socket = TibberHandler.this.socket;
514 if (socket != null) {
515 socket.sendMessage(query);
517 logger.debug("Socket unable to send subscription message: Socket is null");
519 } catch (IOException e) {
520 logger.warn("Send Message Exception: {}", e.getMessage());