]> git.basschouten.com Git - openhab-addons.git/blob
90ec06f1ed617a0f6d09c5f4881de4c9f7879e22
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tibber.internal.handler;
14
15 import static org.openhab.binding.tibber.internal.TibberBindingConstants.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.math.BigDecimal;
20 import java.net.URI;
21 import java.net.URISyntaxException;
22 import java.time.Instant;
23 import java.time.ZonedDateTime;
24 import java.time.format.DateTimeParseException;
25 import java.util.Properties;
26 import java.util.concurrent.Future;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29
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.http.HttpHeader;
34 import org.eclipse.jetty.util.ssl.SslContextFactory;
35 import org.eclipse.jetty.websocket.api.Session;
36 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
37 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
38 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
39 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
40 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
41 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
42 import org.eclipse.jetty.websocket.client.WebSocketClient;
43 import org.openhab.binding.tibber.internal.config.TibberConfiguration;
44 import org.openhab.core.io.net.http.HttpUtil;
45 import org.openhab.core.library.types.DateTimeType;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.library.types.StringType;
49 import org.openhab.core.library.unit.Units;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.ThingStatusInfo;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.TimeSeries;
60 import org.osgi.framework.FrameworkUtil;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 import com.google.gson.JsonArray;
65 import com.google.gson.JsonElement;
66 import com.google.gson.JsonObject;
67 import com.google.gson.JsonParser;
68 import com.google.gson.JsonSyntaxException;
69
70 /**
71  * The {@link TibberHandler} is responsible for handling queries to/from Tibber API.
72  *
73  * @author Stian Kjoglum - Initial contribution
74  */
75 @NonNullByDefault
76 public class TibberHandler extends BaseThingHandler {
77     private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
78     private final Logger logger = LoggerFactory.getLogger(TibberHandler.class);
79     private final Properties httpHeader = new Properties();
80     private TibberConfiguration tibberConfig = new TibberConfiguration();
81     private @Nullable SslContextFactory sslContextFactory;
82     private @Nullable TibberWebSocketListener socket;
83     private @Nullable Session session;
84     private @Nullable WebSocketClient client;
85     private @Nullable ScheduledFuture<?> pollingJob;
86     private @Nullable Future<?> sessionFuture;
87     private String rtEnabled = "false";
88     private @Nullable String subscriptionURL;
89     private @Nullable String versionString;
90
91     public TibberHandler(Thing thing) {
92         super(thing);
93     }
94
95     @Override
96     public void initialize() {
97         updateStatus(ThingStatus.UNKNOWN);
98         tibberConfig = getConfigAs(TibberConfiguration.class);
99
100         versionString = FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
101         logger.debug("Binding version: {}", versionString);
102
103         scheduler.execute(() -> {
104             getTibberParameters();
105             startRefresh(tibberConfig.getRefresh());
106         });
107     }
108
109     @Override
110     public void handleCommand(ChannelUID channelUID, Command command) {
111         if (command instanceof RefreshType) {
112             startRefresh(tibberConfig.getRefresh());
113         } else {
114             logger.debug("Tibber API is read-only and does not handle commands");
115         }
116     }
117
118     @Override
119     public void channelLinked(ChannelUID channelUID) {
120         if (channelUID.getAsString().contains("live_") && !isConnected() && "true".equals(rtEnabled)) {
121             try {
122                 startLiveStream();
123             } catch (IOException e) {
124                 logger.debug("Unable to start live data: {}", e.getMessage());
125             }
126         }
127     }
128
129     @Override
130     public void channelUnlinked(ChannelUID channelUID) {
131         if (channelUID.getAsString().contains("live_") && !liveChannelsLinked() && isConnected()) {
132             close();
133         }
134     }
135
136     public void getTibberParameters() {
137         String response = "";
138         try {
139             httpHeader.put("cache-control", "no-cache");
140             httpHeader.put("content-type", JSON_CONTENT_TYPE);
141             httpHeader.put(HttpHeader.USER_AGENT.asString(),
142                     "openHAB/Tibber " + versionString + " Tibber driver " + TIBBER_DRIVER);
143             httpHeader.put(HttpHeader.AUTHORIZATION.asString(), "Bearer " + tibberConfig.getToken());
144
145             TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
146             InputStream connectionStream = tibberQuery.connectionInputStream(tibberConfig.getHomeid());
147             response = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, connectionStream, null, REQUEST_TIMEOUT);
148
149             if (!response.contains("error") && !response.contains("<html>")) {
150                 updateStatus(ThingStatus.ONLINE);
151                 getURLInput(BASE_URL);
152
153                 InputStream inputStream = tibberQuery.getRealtimeInputStream(tibberConfig.getHomeid());
154                 String jsonResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, inputStream, null,
155                         REQUEST_TIMEOUT);
156
157                 JsonObject object = (JsonObject) JsonParser.parseString(jsonResponse);
158                 JsonObject dObject = object.getAsJsonObject("data");
159                 if (dObject != null) {
160                     JsonObject viewerObject = dObject.getAsJsonObject("viewer");
161                     if (viewerObject != null) {
162                         JsonObject homeObject = viewerObject.getAsJsonObject("home");
163                         if (homeObject != null) {
164                             JsonObject featuresObject = homeObject.getAsJsonObject("features");
165                             if (featuresObject != null) {
166                                 rtEnabled = featuresObject.get("realTimeConsumptionEnabled").toString();
167                             }
168                         }
169                     }
170                 }
171
172                 if (liveChannelsLinked() && "true".equals(rtEnabled)) {
173                     startLiveStream();
174                 }
175             } else {
176                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
177                         "Problems connecting/communicating with server: " + response);
178             }
179         } catch (IOException | JsonSyntaxException e) {
180             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
181         }
182     }
183
184     public void getURLInput(String url) throws IOException {
185         String jsonResponse = "";
186         TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
187
188         InputStream inputStream = tibberQuery.getInputStream(tibberConfig.getHomeid());
189         jsonResponse = HttpUtil.executeUrl("POST", url, httpHeader, inputStream, null, REQUEST_TIMEOUT);
190         logger.debug("API response: {}", jsonResponse);
191
192         if (!jsonResponse.contains("error") && !jsonResponse.contains("<html>")) {
193             if (getThing().getStatus() == ThingStatus.OFFLINE || getThing().getStatus() == ThingStatus.INITIALIZING) {
194                 updateStatus(ThingStatus.ONLINE);
195             }
196
197             JsonObject rootJsonObject = (JsonObject) JsonParser.parseString(jsonResponse);
198
199             if (jsonResponse.contains("total")) {
200                 try {
201                     JsonObject current = rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
202                             .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
203                             .getAsJsonObject("current");
204
205                     updateState(CURRENT_TOTAL, new DecimalType(current.get("total").toString()));
206                     String timestamp = current.get("startsAt").toString().substring(1, 20);
207                     updateState(CURRENT_STARTSAT, new DateTimeType(timestamp));
208                     updateState(CURRENT_LEVEL,
209                             new StringType(current.get("level").toString().replaceAll("^\"|\"$", "")));
210
211                     JsonArray tomorrow = rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
212                             .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
213                             .getAsJsonArray("tomorrow");
214                     updateState(TOMORROW_PRICES, new StringType(tomorrow.toString()));
215                     JsonArray today = rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
216                             .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
217                             .getAsJsonArray("today");
218                     updateState(TODAY_PRICES, new StringType(today.toString()));
219
220                     TimeSeries timeSeries = buildTimeSeries(today, tomorrow);
221                     sendTimeSeries(CURRENT_TOTAL, timeSeries);
222                 } catch (JsonSyntaxException | DateTimeParseException e) {
223                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
224                             "Error communicating with Tibber API: " + e.getMessage());
225                 }
226             }
227             if (jsonResponse.contains("daily") && !jsonResponse.contains("\"daily\":{\"nodes\":[]")
228                     && !jsonResponse.contains("\"daily\":null")) {
229                 try {
230                     JsonObject myObject = (JsonObject) rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
231                             .getAsJsonObject("home").getAsJsonObject("daily").getAsJsonArray("nodes").get(0);
232
233                     String timestampDailyFrom = myObject.get("from").toString().substring(1, 20);
234                     updateState(DAILY_FROM, new DateTimeType(timestampDailyFrom));
235
236                     String timestampDailyTo = myObject.get("to").toString().substring(1, 20);
237                     updateState(DAILY_TO, new DateTimeType(timestampDailyTo));
238
239                     updateChannel(DAILY_COST, myObject.get("cost").toString());
240                     updateChannel(DAILY_CONSUMPTION, myObject.get("consumption").toString());
241                 } catch (JsonSyntaxException e) {
242                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
243                             "Error communicating with Tibber API: " + e.getMessage());
244                 }
245             }
246             if (jsonResponse.contains("hourly") && !jsonResponse.contains("\"hourly\":{\"nodes\":[]")
247                     && !jsonResponse.contains("\"hourly\":null")) {
248                 try {
249                     JsonObject myObject = (JsonObject) rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
250                             .getAsJsonObject("home").getAsJsonObject("hourly").getAsJsonArray("nodes").get(0);
251
252                     String timestampHourlyFrom = myObject.get("from").toString().substring(1, 20);
253                     updateState(HOURLY_FROM, new DateTimeType(timestampHourlyFrom));
254
255                     String timestampHourlyTo = myObject.get("to").toString().substring(1, 20);
256                     updateState(HOURLY_TO, new DateTimeType(timestampHourlyTo));
257
258                     updateChannel(HOURLY_COST, myObject.get("cost").toString());
259                     updateChannel(HOURLY_CONSUMPTION, myObject.get("consumption").toString());
260                 } catch (JsonSyntaxException e) {
261                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
262                             "Error communicating with Tibber API: " + e.getMessage());
263                 }
264             }
265         } else if (jsonResponse.contains("error")) {
266             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
267                     "Error in response from Tibber API: " + jsonResponse);
268             try {
269                 Thread.sleep(300 * 1000);
270                 return;
271             } catch (InterruptedException e) {
272                 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
273             }
274         } else {
275             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
276                     "Unexpected response from Tibber: " + jsonResponse);
277             try {
278                 Thread.sleep(300 * 1000);
279                 return;
280             } catch (InterruptedException e) {
281                 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
282             }
283         }
284     }
285
286     /**
287      * Builds the {@link TimeSeries} that represents the future tibber prices.
288      *
289      * @param today The prices for today
290      * @param tomorrow The prices for tomorrow.
291      * @return The {@link TimeSeries} with future values.
292      */
293     private TimeSeries buildTimeSeries(JsonArray today, JsonArray tomorrow) {
294         final TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
295         mapTimeSeriesEntries(today, timeSeries);
296         mapTimeSeriesEntries(tomorrow, timeSeries);
297         return timeSeries;
298     }
299
300     private void mapTimeSeriesEntries(JsonArray prices, TimeSeries timeSeries) {
301         for (JsonElement entry : prices) {
302             JsonObject entryObject = entry.getAsJsonObject();
303             final Instant startsAt = ZonedDateTime.parse(entryObject.get("startsAt").getAsString()).toInstant();
304             final DecimalType value = new DecimalType(entryObject.get("total").getAsString());
305             timeSeries.add(startsAt, value);
306         }
307     }
308
309     public void startRefresh(int refresh) {
310         if (pollingJob == null) {
311             pollingJob = scheduler.scheduleWithFixedDelay(() -> {
312                 try {
313                     updateRequest();
314                 } catch (IOException e) {
315                     logger.warn("IO Exception: {}", e.getMessage());
316                 }
317             }, 1, refresh, TimeUnit.MINUTES);
318         }
319     }
320
321     public void updateRequest() throws IOException {
322         getURLInput(BASE_URL);
323         if (liveChannelsLinked() && "true".equals(rtEnabled) && !isConnected()) {
324             startLiveStream();
325         }
326     }
327
328     private boolean liveChannelsLinked() {
329         return getThing().getChannels().stream().map(Channel::getUID)
330                 .filter((channelUID -> channelUID.getAsString().contains("live_"))).anyMatch(this::isLinked);
331     };
332
333     private void getSubscriptionUrl() throws IOException {
334         TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
335         InputStream wsURL = tibberQuery.getWebsocketUrl();
336         String wsResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, wsURL, null, REQUEST_TIMEOUT);
337
338         JsonObject wsobject = (JsonObject) JsonParser.parseString(wsResponse);
339         JsonObject dataObject = wsobject.getAsJsonObject("data");
340         if (dataObject != null) {
341             JsonObject viewerObject = dataObject.getAsJsonObject("viewer");
342             if (viewerObject != null) {
343                 JsonElement subscriptionElement = viewerObject.get("websocketSubscriptionUrl");
344                 if (subscriptionElement != null) {
345                     subscriptionURL = subscriptionElement.toString().replaceAll("^\"|\"$", "");
346                 }
347             }
348         }
349     }
350
351     public void updateChannel(String channelID, String channelValue) {
352         if (!channelValue.contains("null")) {
353             if (channelID.contains("consumption") || channelID.contains("Consumption")
354                     || channelID.contains("accumulatedProduction")) {
355                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.KILOWATT_HOUR));
356             } else if (channelID.contains("power") || channelID.contains("Power")) {
357                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.WATT));
358             } else if (channelID.contains("voltage")) {
359                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.VOLT));
360             } else if (channelID.contains("current")) {
361                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.AMPERE));
362             } else {
363                 updateState(channelID, new DecimalType(channelValue));
364             }
365         }
366     }
367
368     public void thingStatusChanged(ThingStatusInfo thingStatusInfo) {
369         logger.debug("Thing Status updated to {} for device: {}", thingStatusInfo.getStatus(), getThing().getUID());
370         if (thingStatusInfo.getStatus() != ThingStatus.ONLINE) {
371             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
372                     "Unable to communicate with Tibber API");
373         }
374     }
375
376     @Override
377     public void dispose() {
378         ScheduledFuture<?> pollingJob = this.pollingJob;
379         if (pollingJob != null) {
380             pollingJob.cancel(true);
381             this.pollingJob = null;
382         }
383         if (isConnected()) {
384             close();
385             WebSocketClient client = this.client;
386             if (client != null) {
387                 try {
388                     logger.debug("DISPOSE - Stopping and Terminating Websocket connection");
389                     client.stop();
390                 } catch (Exception e) {
391                     logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
392                 }
393                 client.destroy();
394                 this.client = null;
395             }
396         }
397         super.dispose();
398     }
399
400     private void startLiveStream() throws IOException {
401         logger.debug("Attempting to open Websocket connection");
402         getSubscriptionUrl();
403
404         if (subscriptionURL == null || subscriptionURL.isBlank()) {
405             logger.debug("Unexpected subscription result from the server");
406             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
407                     "Unexpected subscription result from the server");
408         } else {
409             logger.debug("Connecting Subscription to: {}", subscriptionURL);
410             open();
411         }
412     }
413
414     public void open() {
415         WebSocketClient client = this.client;
416         if (client == null || !client.isRunning() || !isConnected()) {
417             if (client != null) {
418                 try {
419                     client.stop();
420                 } catch (Exception e) {
421                     logger.warn("OPEN FRAME - Failed to stop websocket client: {}", e.getMessage());
422                 }
423                 client.destroy();
424             }
425             sslContextFactory = new SslContextFactory.Client(true);
426             sslContextFactory.setTrustAll(true);
427             sslContextFactory.setEndpointIdentificationAlgorithm(null);
428
429             client = new WebSocketClient(new HttpClient(sslContextFactory));
430             client.setMaxIdleTimeout(30 * 1000);
431             this.client = client;
432
433             TibberWebSocketListener socket = this.socket;
434             if (socket == null) {
435                 logger.debug("New socket being created");
436                 socket = new TibberWebSocketListener();
437                 this.socket = socket;
438             }
439
440             ClientUpgradeRequest newRequest = new ClientUpgradeRequest();
441             newRequest.setHeader(HttpHeader.USER_AGENT.asString(),
442                     "openHAB/Tibber " + versionString + " Tibber driver " + TIBBER_DRIVER);
443             newRequest.setHeader(HttpHeader.AUTHORIZATION.asString(), "Bearer " + tibberConfig.getToken());
444             newRequest.setSubProtocols("graphql-transport-ws");
445
446             try {
447                 logger.debug("Starting Websocket connection");
448                 client.start();
449             } catch (Exception e) {
450                 logger.warn("Websocket Start Exception: {}", e.getMessage());
451             }
452             try {
453                 logger.debug("Connecting Websocket connection");
454                 sessionFuture = client.connect(socket, new URI(subscriptionURL), newRequest);
455                 try {
456                     Thread.sleep(10 * 1000);
457                 } catch (InterruptedException e) {
458                 }
459                 if (!isConnected()) {
460                     logger.warn("Unable to establish websocket session - Reattempting connection on next refresh");
461                 } else {
462                     logger.debug("Websocket session established");
463                 }
464             } catch (IOException e) {
465                 logger.warn("Websocket Connect Exception: {}", e.getMessage());
466             } catch (URISyntaxException e) {
467                 logger.warn("Websocket URI Exception: {}", e.getMessage());
468             }
469         } else {
470             logger.warn("Open: Websocket client already running");
471         }
472     }
473
474     public void close() {
475         Session session = this.session;
476         if (session != null) {
477             String disconnect = "{\"type\":\"connection_terminate\",\"payload\":null}";
478             try {
479                 TibberWebSocketListener socket = this.socket;
480                 if (socket != null) {
481                     logger.debug("Sending websocket disconnect message");
482                     socket.sendMessage(disconnect);
483                 } else {
484                     logger.warn("Socket unable to send disconnect message: Socket is null");
485                 }
486             } catch (IOException e) {
487                 logger.warn("Websocket Close Exception: {}", e.getMessage());
488             }
489             try {
490                 session.close();
491             } catch (Exception e) {
492                 logger.warn("Unable to disconnect session");
493             }
494             this.session = null;
495             this.socket = null;
496         }
497         Future<?> sessionFuture = this.sessionFuture;
498         if (sessionFuture != null && !sessionFuture.isDone()) {
499             sessionFuture.cancel(true);
500         }
501         WebSocketClient client = this.client;
502         if (client != null) {
503             try {
504                 client.stop();
505             } catch (Exception e) {
506                 logger.warn("CLOSE FRAME - Failed to stop websocket client: {}", e.getMessage());
507             }
508             client.destroy();
509         }
510     }
511
512     public boolean isConnected() {
513         Session session = this.session;
514         return session != null && session.isOpen();
515     }
516
517     @WebSocket
518     public class TibberWebSocketListener {
519
520         @OnWebSocketConnect
521         public void onConnect(Session wssession) {
522             TibberHandler.this.session = wssession;
523             TibberWebSocketListener socket = TibberHandler.this.socket;
524             String connection = "{\"type\":\"connection_init\", \"payload\":{\"token\":\"" + tibberConfig.getToken()
525                     + "\"}}";
526             try {
527                 if (socket != null) {
528                     logger.debug("Sending websocket connect message");
529                     socket.sendMessage(connection);
530                 } else {
531                     logger.debug("Socket unable to send connect message: Socket is null");
532                 }
533             } catch (IOException e) {
534                 logger.warn("Send Message Exception: {}", e.getMessage());
535             }
536         }
537
538         @OnWebSocketClose
539         public void onClose(int statusCode, String reason) {
540             logger.debug("Closing a WebSocket due to {}", reason);
541             WebSocketClient client = TibberHandler.this.client;
542             if (client != null && client.isRunning()) {
543                 try {
544                     logger.debug("ONCLOSE - Stopping and Terminating Websocket connection");
545                     client.stop();
546                 } catch (Exception e) {
547                     logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
548                 }
549             }
550         }
551
552         @OnWebSocketError
553         public void onWebSocketError(Throwable e) {
554             String message = e.getMessage();
555             logger.debug("Error during websocket communication: {}", message);
556             close();
557         }
558
559         @OnWebSocketMessage
560         public void onMessage(String message) {
561             if (message.contains("connection_ack")) {
562                 logger.debug("Connected to Server");
563                 startSubscription();
564             } else if (message.contains("error") || message.contains("terminate")) {
565                 logger.debug("Error/terminate received from server: {}", message);
566                 close();
567             } else if (message.contains("liveMeasurement")) {
568                 JsonObject object = (JsonObject) JsonParser.parseString(message);
569                 JsonObject myObject = object.getAsJsonObject("payload").getAsJsonObject("data")
570                         .getAsJsonObject("liveMeasurement");
571                 if (myObject.has("timestamp")) {
572                     String liveTimestamp = myObject.get("timestamp").toString().substring(1, 20);
573                     updateState(LIVE_TIMESTAMP, new DateTimeType(liveTimestamp));
574                 }
575                 if (myObject.has("power")) {
576                     updateChannel(LIVE_POWER, myObject.get("power").toString());
577                 }
578                 if (myObject.has("lastMeterConsumption")) {
579                     updateChannel(LIVE_LASTMETERCONSUMPTION, myObject.get("lastMeterConsumption").toString());
580                 }
581                 if (myObject.has("lastMeterProduction")) {
582                     updateChannel(LIVE_LASTMETERPRODUCTION, myObject.get("lastMeterProduction").toString());
583                 }
584                 if (myObject.has("accumulatedConsumption")) {
585                     updateChannel(LIVE_ACCUMULATEDCONSUMPTION, myObject.get("accumulatedConsumption").toString());
586                 }
587                 if (myObject.has("accumulatedConsumptionLastHour")) {
588                     updateChannel(LIVE_ACCUMULATEDCONSUMPTION_THIS_HOUR,
589                             myObject.get("accumulatedConsumptionLastHour").toString());
590                 }
591                 if (myObject.has("accumulatedCost")) {
592                     updateChannel(LIVE_ACCUMULATEDCOST, myObject.get("accumulatedCost").toString());
593                 }
594                 if (myObject.has("accumulatedReward")) {
595                     updateChannel(LIVE_ACCUMULATEREWARD, myObject.get("accumulatedReward").toString());
596                 }
597                 if (myObject.has("currency")) {
598                     updateState(LIVE_CURRENCY, new StringType(myObject.get("currency").toString()));
599                 }
600                 if (myObject.has("minPower")) {
601                     updateChannel(LIVE_MINPOWER, myObject.get("minPower").toString());
602                 }
603                 if (myObject.has("averagePower")) {
604                     updateChannel(LIVE_AVERAGEPOWER, myObject.get("averagePower").toString());
605                 }
606                 if (myObject.has("maxPower")) {
607                     updateChannel(LIVE_MAXPOWER, myObject.get("maxPower").toString());
608                 }
609                 if (myObject.has("voltagePhase1")) {
610                     updateChannel(LIVE_VOLTAGE1, myObject.get("voltagePhase1").toString());
611                 }
612                 if (myObject.has("voltagePhase2")) {
613                     updateChannel(LIVE_VOLTAGE2, myObject.get("voltagePhase2").toString());
614                 }
615                 if (myObject.has("voltagePhase3")) {
616                     updateChannel(LIVE_VOLTAGE3, myObject.get("voltagePhase3").toString());
617                 }
618                 if (myObject.has("currentL1")) {
619                     updateChannel(LIVE_CURRENT1, myObject.get("currentL1").toString());
620                 }
621                 if (myObject.has("currentL2")) {
622                     updateChannel(LIVE_CURRENT2, myObject.get("currentL2").toString());
623                 }
624                 if (myObject.has("currentL3")) {
625                     updateChannel(LIVE_CURRENT3, myObject.get("currentL3").toString());
626                 }
627                 if (myObject.has("powerProduction")) {
628                     updateChannel(LIVE_POWERPRODUCTION, myObject.get("powerProduction").toString());
629                 }
630                 if (myObject.has("accumulatedProduction")) {
631                     updateChannel(LIVE_ACCUMULATEDPRODUCTION, myObject.get("accumulatedProduction").toString());
632                 }
633                 if (myObject.has("accumulatedProductionLastHour")) {
634                     updateChannel(LIVE_ACCUMULATEDPRODUCTION_THIS_HOUR,
635                             myObject.get("accumulatedProductionLastHour").toString());
636                 }
637                 if (myObject.has("minPowerProduction")) {
638                     updateChannel(LIVE_MINPOWERPRODUCTION, myObject.get("minPowerProduction").toString());
639                 }
640                 if (myObject.has("maxPowerProduction")) {
641                     updateChannel(LIVE_MAXPOWERPRODUCTION, myObject.get("maxPowerProduction").toString());
642                 }
643             } else {
644                 logger.debug("Unknown live response from Tibber");
645             }
646         }
647
648         private void sendMessage(String message) throws IOException {
649             logger.debug("Send message: {}", message);
650             Session session = TibberHandler.this.session;
651             if (session != null) {
652                 session.getRemote().sendString(message);
653             }
654         }
655
656         public void startSubscription() {
657             String query = "{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":null,\"query\":\"subscription {\\n liveMeasurement(homeId:\\\""
658                     + tibberConfig.getHomeid()
659                     + "\\\") {\\n timestamp\\n power\\n lastMeterConsumption\\n lastMeterProduction\\n accumulatedConsumption\\n accumulatedConsumptionLastHour\\n accumulatedCost\\n accumulatedReward\\n currency\\n minPower\\n averagePower\\n maxPower\\n"
660                     + "voltagePhase1\\n voltagePhase2\\n voltagePhase3\\n currentL1\\n currentL2\\n currentL3\\n powerProduction\\n accumulatedProduction\\n accumulatedProductionLastHour\\n minPowerProduction\\n maxPowerProduction\\n }\\n }\\n\"}}";
661             try {
662                 TibberWebSocketListener socket = TibberHandler.this.socket;
663                 if (socket != null) {
664                     socket.sendMessage(query);
665                 } else {
666                     logger.debug("Socket unable to send subscription message: Socket is null");
667                 }
668             } catch (IOException e) {
669                 logger.warn("Send Message Exception: {}", e.getMessage());
670             }
671         }
672     }
673 }