]> git.basschouten.com Git - openhab-addons.git/blob
9bfda0ca74e4c138669d307d633bc68f36e6baba
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.util.Properties;
23 import java.util.concurrent.Future;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
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;
55
56 import com.google.gson.JsonArray;
57 import com.google.gson.JsonObject;
58 import com.google.gson.JsonParser;
59 import com.google.gson.JsonSyntaxException;
60
61 /**
62  * The {@link TibberHandler} is responsible for handling queries to/from Tibber API.
63  *
64  * @author Stian Kjoglum - Initial contribution
65  */
66 @NonNullByDefault
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";
79
80     public TibberHandler(Thing thing) {
81         super(thing);
82     }
83
84     @Override
85     public void initialize() {
86         updateStatus(ThingStatus.UNKNOWN);
87         tibberConfig = getConfigAs(TibberConfiguration.class);
88
89         getTibberParameters();
90         startRefresh(tibberConfig.getRefresh());
91     }
92
93     @Override
94     public void handleCommand(ChannelUID channelUID, Command command) {
95         if (command instanceof RefreshType) {
96             startRefresh(tibberConfig.getRefresh());
97         } else {
98             logger.debug("Tibber API is read-only and does not handle commands");
99         }
100     }
101
102     public void getTibberParameters() {
103         String response = "";
104         try {
105             httpHeader.put("cache-control", "no-cache");
106             httpHeader.put("content-type", JSON_CONTENT_TYPE);
107             httpHeader.put("Authorization", "Bearer " + tibberConfig.getToken());
108
109             TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
110             InputStream connectionStream = tibberQuery.connectionInputStream(tibberConfig.getHomeid());
111             response = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, connectionStream, null, REQUEST_TIMEOUT);
112
113             if (!response.contains("error") && !response.contains("<html>")) {
114                 updateStatus(ThingStatus.ONLINE);
115                 getURLInput(BASE_URL);
116
117                 InputStream inputStream = tibberQuery.getRealtimeInputStream(tibberConfig.getHomeid());
118                 String jsonResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, inputStream, null,
119                         REQUEST_TIMEOUT);
120
121                 JsonObject object = (JsonObject) JsonParser.parseString(jsonResponse);
122                 rtEnabled = object.getAsJsonObject("data").getAsJsonObject("viewer").getAsJsonObject("home")
123                         .getAsJsonObject("features").get("realTimeConsumptionEnabled").toString();
124
125                 if ("true".equals(rtEnabled)) {
126                     logger.debug("Pulse associated with HomeId: Live stream will be started");
127                     open();
128                 } else {
129                     logger.debug("No Pulse associated with HomeId: No live stream will be started");
130                 }
131             } else {
132                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
133                         "Problems connecting/communicating with server: " + response);
134             }
135         } catch (IOException | JsonSyntaxException e) {
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
137         }
138     }
139
140     public void getURLInput(String url) throws IOException {
141         String jsonResponse = "";
142         TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
143
144         InputStream inputStream = tibberQuery.getInputStream(tibberConfig.getHomeid());
145         jsonResponse = HttpUtil.executeUrl("POST", url, httpHeader, inputStream, null, REQUEST_TIMEOUT);
146         logger.debug("API response: {}", jsonResponse);
147
148         if (!jsonResponse.contains("error") && !jsonResponse.contains("<html>")) {
149             if (getThing().getStatus() == ThingStatus.OFFLINE || getThing().getStatus() == ThingStatus.INITIALIZING) {
150                 updateStatus(ThingStatus.ONLINE);
151             }
152
153             JsonObject rootJsonObject = (JsonObject) JsonParser.parseString(jsonResponse);
154
155             if (jsonResponse.contains("total")) {
156                 try {
157                     JsonObject current = rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
158                             .getAsJsonObject("home").getAsJsonObject("currentSubscription").getAsJsonObject("priceInfo")
159                             .getAsJsonObject("current");
160
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("^\"|\"$", "")));
166
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());
174                 }
175             }
176             if (jsonResponse.contains("daily") && !jsonResponse.contains("\"daily\":{\"nodes\":[]")
177                     && !jsonResponse.contains("\"daily\":null")) {
178                 try {
179                     JsonObject myObject = (JsonObject) rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
180                             .getAsJsonObject("home").getAsJsonObject("daily").getAsJsonArray("nodes").get(0);
181
182                     String timestampDailyFrom = myObject.get("from").toString().substring(1, 20);
183                     updateState(DAILY_FROM, new DateTimeType(timestampDailyFrom));
184
185                     String timestampDailyTo = myObject.get("to").toString().substring(1, 20);
186                     updateState(DAILY_TO, new DateTimeType(timestampDailyTo));
187
188                     updateChannel(DAILY_COST, myObject.get("cost").toString());
189                     updateChannel(DAILY_CONSUMPTION, myObject.get("consumption").toString());
190
191                 } catch (JsonSyntaxException e) {
192                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193                             "Error communicating with Tibber API: " + e.getMessage());
194                 }
195             }
196             if (jsonResponse.contains("hourly") && !jsonResponse.contains("\"hourly\":{\"nodes\":[]")
197                     && !jsonResponse.contains("\"hourly\":null")) {
198                 try {
199                     JsonObject myObject = (JsonObject) rootJsonObject.getAsJsonObject("data").getAsJsonObject("viewer")
200                             .getAsJsonObject("home").getAsJsonObject("hourly").getAsJsonArray("nodes").get(0);
201
202                     String timestampHourlyFrom = myObject.get("from").toString().substring(1, 20);
203                     updateState(HOURLY_FROM, new DateTimeType(timestampHourlyFrom));
204
205                     String timestampHourlyTo = myObject.get("to").toString().substring(1, 20);
206                     updateState(HOURLY_TO, new DateTimeType(timestampHourlyTo));
207
208                     updateChannel(HOURLY_COST, myObject.get("cost").toString());
209                     updateChannel(HOURLY_CONSUMPTION, myObject.get("consumption").toString());
210
211                 } catch (JsonSyntaxException e) {
212                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
213                             "Error communicating with Tibber API: " + e.getMessage());
214                 }
215             }
216         } else if (jsonResponse.contains("error")) {
217             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
218                     "Error in response from Tibber API: " + jsonResponse);
219             try {
220                 Thread.sleep(300 * 1000);
221                 return;
222             } catch (InterruptedException e) {
223                 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
224             }
225         } else {
226             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
227                     "Unexpected response from Tibber: " + jsonResponse);
228             try {
229                 Thread.sleep(300 * 1000);
230                 return;
231             } catch (InterruptedException e) {
232                 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
233             }
234         }
235     }
236
237     public void startRefresh(int refresh) {
238         if (pollingJob == null) {
239             pollingJob = scheduler.scheduleWithFixedDelay(() -> {
240                 try {
241                     updateRequest();
242                 } catch (IOException e) {
243                     logger.warn("IO Exception: {}", e.getMessage());
244                 }
245             }, 1, refresh, TimeUnit.MINUTES);
246         }
247     }
248
249     public void updateRequest() throws IOException {
250         getURLInput(BASE_URL);
251         if ("true".equals(rtEnabled) && !isConnected()) {
252             logger.debug("Attempting to reopen Websocket connection");
253             open();
254         }
255     }
256
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));
268             } else {
269                 updateState(channelID, new DecimalType(channelValue));
270             }
271         }
272     }
273
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");
279         }
280     }
281
282     @Override
283     public void dispose() {
284         ScheduledFuture<?> pollingJob = this.pollingJob;
285         if (pollingJob != null) {
286             pollingJob.cancel(true);
287             this.pollingJob = null;
288         }
289         if (isConnected()) {
290             close();
291             WebSocketClient client = this.client;
292             if (client != null) {
293                 try {
294                     logger.debug("DISPOSE - Stopping and Terminating Websocket connection");
295                     client.stop();
296                 } catch (Exception e) {
297                     logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
298                 }
299                 client.destroy();
300                 this.client = null;
301             }
302         }
303         super.dispose();
304     }
305
306     public void open() {
307         WebSocketClient client = this.client;
308         if (client == null || !client.isRunning() || !isConnected()) {
309             if (client != null) {
310                 try {
311                     client.stop();
312                 } catch (Exception e) {
313                     logger.warn("OPEN FRAME - Failed to stop websocket client: {}", e.getMessage());
314                 }
315                 client.destroy();
316             }
317             sslContextFactory = new SslContextFactory.Client(true);
318             sslContextFactory.setTrustAll(true);
319             sslContextFactory.setEndpointIdentificationAlgorithm(null);
320
321             client = new WebSocketClient(sslContextFactory);
322             client.setMaxIdleTimeout(30 * 1000);
323             this.client = client;
324
325             TibberWebSocketListener socket = this.socket;
326             if (socket == null) {
327                 logger.debug("New socket being created");
328                 socket = new TibberWebSocketListener();
329                 this.socket = socket;
330             }
331
332             ClientUpgradeRequest newRequest = new ClientUpgradeRequest();
333             newRequest.setHeader("Authorization", "Bearer " + tibberConfig.getToken());
334             newRequest.setSubProtocols("graphql-subscriptions");
335
336             try {
337                 logger.debug("Starting Websocket connection");
338                 client.start();
339             } catch (Exception e) {
340                 logger.warn("Websocket Start Exception: {}", e.getMessage());
341             }
342             try {
343                 logger.debug("Connecting Websocket connection");
344                 sessionFuture = client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest);
345                 try {
346                     Thread.sleep(10 * 1000);
347                 } catch (InterruptedException e) {
348                 }
349                 Session session = this.session;
350                 if (!session.isOpen()) {
351                     close();
352                     logger.warn("Unable to establish websocket session");
353                 } else {
354                     logger.debug("Websocket session established");
355                 }
356             } catch (IOException e) {
357                 logger.warn("Websocket Connect Exception: {}", e.getMessage());
358             } catch (URISyntaxException e) {
359                 logger.warn("Websocket URI Exception: {}", e.getMessage());
360             }
361         } else {
362             logger.warn("Open: Websocket client already running");
363         }
364     }
365
366     public void close() {
367         Session session = this.session;
368         if (session != null) {
369             String disconnect = "{\"type\":\"connection_terminate\",\"payload\":null}";
370             try {
371                 TibberWebSocketListener socket = this.socket;
372                 if (socket != null) {
373                     logger.debug("Sending websocket disconnect message");
374                     socket.sendMessage(disconnect);
375                 } else {
376                     logger.warn("Socket unable to send disconnect message: Socket is null");
377                 }
378             } catch (IOException e) {
379                 logger.warn("Websocket Close Exception: {}", e.getMessage());
380             }
381             try {
382                 session.close();
383             } catch (Exception e) {
384                 logger.warn("Unable to disconnect session");
385             }
386             this.session = null;
387             this.socket = null;
388         }
389         Future<?> sessionFuture = this.sessionFuture;
390         if (sessionFuture != null && !sessionFuture.isDone()) {
391             sessionFuture.cancel(true);
392         }
393         WebSocketClient client = this.client;
394         if (client != null) {
395             try {
396                 client.stop();
397             } catch (Exception e) {
398                 logger.warn("CLOSE FRAME - Failed to stop websocket client: {}", e.getMessage());
399             }
400             client.destroy();
401         }
402     }
403
404     public boolean isConnected() {
405         Session session = this.session;
406         return session != null && session.isOpen();
407     }
408
409     @WebSocket
410     @NonNullByDefault
411     public class TibberWebSocketListener {
412
413         @OnWebSocketConnect
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() + "\"}";
418             try {
419                 if (socket != null) {
420                     logger.debug("Sending websocket connect message");
421                     socket.sendMessage(connection);
422                 } else {
423                     logger.debug("Socket unable to send connect message: Socket is null");
424                 }
425             } catch (IOException e) {
426                 logger.warn("Send Message Exception: {}", e.getMessage());
427             }
428         }
429
430         @OnWebSocketClose
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()) {
435                 try {
436                     logger.debug("ONCLOSE - Stopping and Terminating Websocket connection");
437                     client.stop();
438                 } catch (Exception e) {
439                     logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
440                 }
441             }
442         }
443
444         @OnWebSocketError
445         public void onWebSocketError(Throwable e) {
446             String message = e.getMessage();
447             logger.debug("Error during websocket communication: {}", message);
448             close();
449         }
450
451         @OnWebSocketMessage
452         public void onMessage(String message) {
453             if (message.contains("connection_ack")) {
454                 logger.debug("Connected to Server");
455                 startSubscription();
456             } else if (message.contains("error") || message.contains("terminate")) {
457                 logger.debug("Error/terminate received from server: {}", message);
458                 close();
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));
466                 }
467                 if (myObject.has("power")) {
468                     updateChannel(LIVE_POWER, myObject.get("power").toString());
469                 }
470                 if (myObject.has("lastMeterConsumption")) {
471                     updateChannel(LIVE_LASTMETERCONSUMPTION, myObject.get("lastMeterConsumption").toString());
472                 }
473                 if (myObject.has("accumulatedConsumption")) {
474                     updateChannel(LIVE_ACCUMULATEDCONSUMPTION, myObject.get("accumulatedConsumption").toString());
475                 }
476                 if (myObject.has("accumulatedCost")) {
477                     updateChannel(LIVE_ACCUMULATEDCOST, myObject.get("accumulatedCost").toString());
478                 }
479                 if (myObject.has("currency")) {
480                     updateState(LIVE_CURRENCY, new StringType(myObject.get("currency").toString()));
481                 }
482                 if (myObject.has("minPower")) {
483                     updateChannel(LIVE_MINPOWER, myObject.get("minPower").toString());
484                 }
485                 if (myObject.has("averagePower")) {
486                     updateChannel(LIVE_AVERAGEPOWER, myObject.get("averagePower").toString());
487                 }
488                 if (myObject.has("maxPower")) {
489                     updateChannel(LIVE_MAXPOWER, myObject.get("maxPower").toString());
490                 }
491                 if (myObject.has("voltagePhase1")) {
492                     updateChannel(LIVE_VOLTAGE1, myObject.get("voltagePhase1").toString());
493                 }
494                 if (myObject.has("voltagePhase2")) {
495                     updateChannel(LIVE_VOLTAGE2, myObject.get("voltagePhase2").toString());
496                 }
497                 if (myObject.has("voltagePhase3")) {
498                     updateChannel(LIVE_VOLTAGE3, myObject.get("voltagePhase3").toString());
499                 }
500                 if (myObject.has("currentL1")) {
501                     updateChannel(LIVE_CURRENT1, myObject.get("currentL1").toString());
502                 }
503                 if (myObject.has("currentL2")) {
504                     updateChannel(LIVE_CURRENT2, myObject.get("currentL2").toString());
505                 }
506                 if (myObject.has("currentL3")) {
507                     updateChannel(LIVE_CURRENT3, myObject.get("currentL3").toString());
508                 }
509                 if (myObject.has("powerProduction")) {
510                     updateChannel(LIVE_POWERPRODUCTION, myObject.get("powerProduction").toString());
511                 }
512                 if (myObject.has("accumulatedProduction")) {
513                     updateChannel(LIVE_ACCUMULATEDPRODUCTION, myObject.get("accumulatedProduction").toString());
514                 }
515                 if (myObject.has("minPowerProduction")) {
516                     updateChannel(LIVE_MINPOWERPRODUCTION, myObject.get("minPowerProduction").toString());
517                 }
518                 if (myObject.has("maxPowerProduction")) {
519                     updateChannel(LIVE_MAXPOWERPRODUCTION, myObject.get("maxPowerProduction").toString());
520                 }
521             } else {
522                 logger.debug("Unknown live response from Tibber");
523             }
524         }
525
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);
531             }
532         }
533
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\"}}";
539             try {
540                 TibberWebSocketListener socket = TibberHandler.this.socket;
541                 if (socket != null) {
542                     socket.sendMessage(query);
543                 } else {
544                     logger.debug("Socket unable to send subscription message: Socket is null");
545                 }
546             } catch (IOException e) {
547                 logger.warn("Send Message Exception: {}", e.getMessage());
548             }
549         }
550     }
551 }