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