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