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