]> git.basschouten.com Git - openhab-addons.git/blob
7b32d87f9b27ad47647fb7e305aa13b7e1717b5e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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(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         try {
106             httpHeader.put("cache-control", "no-cache");
107             httpHeader.put("content-type", JSON_CONTENT_TYPE);
108             httpHeader.put("Authorization", "Bearer " + tibberConfig.getToken());
109
110             TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
111             InputStream connectionStream = tibberQuery.connectionInputStream(tibberConfig.getHomeid());
112             String response = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, connectionStream, null,
113                     REQUEST_TIMEOUT);
114
115             if (!response.contains("error") && !response.contains("<html>")) {
116                 updateStatus(ThingStatus.ONLINE);
117
118                 getURLInput(BASE_URL);
119
120                 InputStream inputStream = tibberQuery.getRealtimeInputStream(tibberConfig.getHomeid());
121                 String jsonResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, inputStream, null,
122                         REQUEST_TIMEOUT);
123
124                 JsonObject object = (JsonObject) new JsonParser().parse(jsonResponse);
125                 rtEnabled = object.getAsJsonObject("data").getAsJsonObject("viewer").getAsJsonObject("home")
126                         .getAsJsonObject("features").get("realTimeConsumptionEnabled").toString();
127
128                 if ("true".equals(rtEnabled)) {
129                     logger.debug("Pulse associated with HomeId: Live stream will be started");
130                     open();
131                 } else {
132                     logger.debug("No Pulse associated with HomeId: No live stream will be started");
133                 }
134             } else {
135                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
136                         "Problems connecting/communicating with server: " + response);
137             }
138         } catch (IOException | JsonSyntaxException e) {
139             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
140         }
141     }
142
143     public void getURLInput(String url) throws IOException {
144         TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler();
145
146         InputStream inputStream = tibberQuery.getInputStream(tibberConfig.getHomeid());
147         String 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) new JsonParser().parse(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
167                 } catch (JsonSyntaxException e) {
168                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
169                             "Error communicating with Tibber API: " + e.getMessage());
170                 }
171             }
172             if (jsonResponse.contains("daily")) {
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")) {
192                 try {
193                     JsonObject myObject = (JsonObject) object.getAsJsonObject("data").getAsJsonObject("viewer")
194                             .getAsJsonObject("home").getAsJsonObject("hourly").getAsJsonArray("nodes").get(0);
195
196                     String timestampHourlyFrom = myObject.get("from").toString().substring(1, 20);
197                     updateState(HOURLY_FROM, new DateTimeType(timestampHourlyFrom));
198
199                     String timestampHourlyTo = myObject.get("to").toString().substring(1, 20);
200                     updateState(HOURLY_TO, new DateTimeType(timestampHourlyTo));
201
202                     updateChannel(HOURLY_COST, myObject.get("cost").toString());
203                     updateChannel(HOURLY_CONSUMPTION, myObject.get("consumption").toString());
204
205                 } catch (JsonSyntaxException e) {
206                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
207                             "Error communicating with Tibber API: " + e.getMessage());
208                 }
209             }
210         } else if (jsonResponse.contains("error")) {
211             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
212                     "Error in response from Tibber API: " + jsonResponse);
213             try {
214                 Thread.sleep(300 * 1000);
215                 return;
216             } catch (InterruptedException e) {
217                 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
218             }
219         } else {
220             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
221                     "Unexpected response from Tibber: " + jsonResponse);
222             try {
223                 Thread.sleep(300 * 1000);
224                 return;
225             } catch (InterruptedException e) {
226                 logger.debug("Tibber OFFLINE, attempting thread sleep: {}", e.getMessage());
227             }
228         }
229     }
230
231     public void startRefresh(int refresh) {
232         if (pollingJob == null) {
233             pollingJob = scheduler.scheduleWithFixedDelay(() -> {
234                 try {
235                     updateRequest();
236                 } catch (IOException e) {
237                     logger.warn("IO Exception: {}", e.getMessage());
238                 }
239             }, 1, refresh, TimeUnit.MINUTES);
240         }
241     }
242
243     public void updateRequest() throws IOException {
244         getURLInput(BASE_URL);
245         if ("true".equals(rtEnabled) && !isConnected()) {
246             logger.debug("Attempting to reopen Websocket connection");
247             open();
248         }
249     }
250
251     public void updateChannel(String channelID, String channelValue) {
252         if (!channelValue.contains("null")) {
253             if (channelID.contains("consumption") || channelID.contains("Consumption")
254                     || channelID.contains("accumulatedProduction")) {
255                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.KILOWATT_HOUR));
256             } else if (channelID.contains("power") || channelID.contains("Power")) {
257                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.WATT));
258             } else if (channelID.contains("voltage")) {
259                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.VOLT));
260             } else if (channelID.contains("live_current")) {
261                 updateState(channelID, new QuantityType<>(new BigDecimal(channelValue), Units.AMPERE));
262             } else {
263                 updateState(channelID, new DecimalType(channelValue));
264             }
265         }
266     }
267
268     public void thingStatusChanged(ThingStatusInfo thingStatusInfo) {
269         logger.debug("Thing Status updated to {} for device: {}", thingStatusInfo.getStatus(), getThing().getUID());
270         if (thingStatusInfo.getStatus() != ThingStatus.ONLINE) {
271             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
272                     "Unable to communicate with Tibber API");
273         }
274     }
275
276     @Override
277     public void dispose() {
278         ScheduledFuture<?> pollingJob = this.pollingJob;
279         if (pollingJob != null) {
280             pollingJob.cancel(true);
281             this.pollingJob = null;
282         }
283         if (isConnected()) {
284             close();
285             WebSocketClient client = this.client;
286             if (client != null) {
287                 try {
288                     logger.debug("Stopping and Terminating Websocket connection");
289                     client.stop();
290                     client.destroy();
291                 } catch (Exception e) {
292                     logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
293                 }
294                 this.client = null;
295             }
296         }
297         super.dispose();
298     }
299
300     public void open() {
301         if (isConnected()) {
302             logger.debug("Open: connection is already open");
303         } else {
304             sslContextFactory.setTrustAll(true);
305             sslContextFactory.setEndpointIdentificationAlgorithm(null);
306
307             WebSocketClient client = this.client;
308             if (client == null) {
309                 client = new WebSocketClient(sslContextFactory, websocketExecutor);
310                 this.client = client;
311             }
312
313             TibberWebSocketListener socket = this.socket;
314             if (socket == null) {
315                 socket = new TibberWebSocketListener();
316                 this.socket = socket;
317             }
318
319             ClientUpgradeRequest newRequest = new ClientUpgradeRequest();
320             newRequest.setHeader("Authorization", "Bearer " + tibberConfig.getToken());
321             newRequest.setSubProtocols("graphql-subscriptions");
322
323             try {
324                 logger.debug("Starting Websocket connection");
325                 client.start();
326             } catch (Exception e) {
327                 logger.warn("Websocket Start Exception: {}", e.getMessage());
328             }
329             try {
330                 logger.debug("Connecting Websocket connection");
331                 sessionFuture = client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest);
332             } catch (IOException e) {
333                 logger.warn("Websocket Connect Exception: {}", e.getMessage());
334             } catch (URISyntaxException e) {
335                 logger.warn("Websocket URI Exception: {}", e.getMessage());
336             }
337         }
338     }
339
340     public void close() {
341         Session session = this.session;
342         if (session != null) {
343             String disconnect = "{\"type\":\"connection_terminate\",\"payload\":null}";
344             try {
345                 TibberWebSocketListener socket = this.socket;
346                 if (socket != null) {
347                     logger.debug("Sending websocket disconnect message");
348                     socket.sendMessage(disconnect);
349                 } else {
350                     logger.debug("Socket unable to send disconnect message: Socket is null");
351                 }
352             } catch (IOException e) {
353                 logger.warn("Websocket Close Exception: {}", e.getMessage());
354             }
355             session.close(0, "Tibber websocket disposed");
356             this.session = null;
357             this.socket = null;
358         }
359         Future<?> sessionFuture = this.sessionFuture;
360         if (sessionFuture != null && !sessionFuture.isDone()) {
361             sessionFuture.cancel(true);
362         }
363         WebSocketClient client = this.client;
364         if (client != null) {
365             try {
366                 client.stop();
367             } catch (Exception e) {
368                 logger.warn("Failed to stop websocket client: {}", e.getMessage());
369             }
370         }
371     }
372
373     public boolean isConnected() {
374         Session session = this.session;
375         return session != null && session.isOpen();
376     }
377
378     @WebSocket
379     @NonNullByDefault
380     public class TibberWebSocketListener {
381
382         @OnWebSocketConnect
383         public void onConnect(Session wssession) {
384             TibberHandler.this.session = wssession;
385             TibberWebSocketListener socket = TibberHandler.this.socket;
386             String connection = "{\"type\":\"connection_init\", \"payload\":\"token=" + tibberConfig.getToken() + "\"}";
387             try {
388                 if (socket != null) {
389                     logger.debug("Sending websocket connect message");
390                     socket.sendMessage(connection);
391                 } else {
392                     logger.debug("Socket unable to send connect message: Socket is null");
393                 }
394             } catch (IOException e) {
395                 logger.warn("Send Message Exception: {}", e.getMessage());
396             }
397         }
398
399         @OnWebSocketClose
400         public void onClose(int statusCode, String reason) {
401             logger.debug("Closing a WebSocket due to {}", reason);
402             WebSocketClient client = TibberHandler.this.client;
403             if (client != null && client.isRunning()) {
404                 try {
405                     logger.debug("Stopping and Terminating Websocket connection");
406                     client.stop();
407                     client.destroy();
408                 } catch (Exception e) {
409                     logger.warn("Websocket Client Stop Exception: {}", e.getMessage());
410                 }
411             }
412             TibberHandler.this.session = null;
413             TibberHandler.this.client = null;
414             TibberHandler.this.socket = null;
415         }
416
417         @OnWebSocketError
418         public void onWebSocketError(Throwable e) {
419             String message = e.getMessage();
420             logger.debug("Error during websocket communication: {}", message);
421             onClose(0, message != null ? message : "null");
422         }
423
424         @OnWebSocketMessage
425         public void onMessage(String message) {
426             if (message.contains("connection_ack")) {
427                 logger.debug("Connected to Server");
428                 startSubscription();
429             } else if (message.contains("error") || message.contains("terminate")) {
430                 logger.debug("Error/terminate received from server: {}", message);
431                 close();
432             } else if (message.contains("liveMeasurement")) {
433                 JsonObject object = (JsonObject) new JsonParser().parse(message);
434                 JsonObject myObject = object.getAsJsonObject("payload").getAsJsonObject("data")
435                         .getAsJsonObject("liveMeasurement");
436                 if (myObject.has("timestamp")) {
437                     String liveTimestamp = myObject.get("timestamp").toString().substring(1, 20);
438                     updateState(LIVE_TIMESTAMP, new DateTimeType(liveTimestamp));
439                 }
440                 if (myObject.has("power")) {
441                     updateChannel(LIVE_POWER, myObject.get("power").toString());
442                 }
443                 if (myObject.has("lastMeterConsumption")) {
444                     updateChannel(LIVE_LASTMETERCONSUMPTION, myObject.get("lastMeterConsumption").toString());
445                 }
446                 if (myObject.has("accumulatedConsumption")) {
447                     updateChannel(LIVE_ACCUMULATEDCONSUMPTION, myObject.get("accumulatedConsumption").toString());
448                 }
449                 if (myObject.has("accumulatedCost")) {
450                     updateChannel(LIVE_ACCUMULATEDCOST, myObject.get("accumulatedCost").toString());
451                 }
452                 if (myObject.has("currency")) {
453                     updateState(LIVE_CURRENCY, new StringType(myObject.get("currency").toString()));
454                 }
455                 if (myObject.has("minPower")) {
456                     updateChannel(LIVE_MINPOWER, myObject.get("minPower").toString());
457                 }
458                 if (myObject.has("averagePower")) {
459                     updateChannel(LIVE_AVERAGEPOWER, myObject.get("averagePower").toString());
460                 }
461                 if (myObject.has("maxPower")) {
462                     updateChannel(LIVE_MAXPOWER, myObject.get("maxPower").toString());
463                 }
464                 if (myObject.has("voltagePhase1")) {
465                     updateChannel(LIVE_VOLTAGE1, myObject.get("voltagePhase1").toString());
466                 }
467                 if (myObject.has("voltagePhase2")) {
468                     updateChannel(LIVE_VOLTAGE2, myObject.get("voltagePhase2").toString());
469                 }
470                 if (myObject.has("voltagePhase3")) {
471                     updateChannel(LIVE_VOLTAGE3, myObject.get("voltagePhase3").toString());
472                 }
473                 if (myObject.has("currentPhase1")) {
474                     updateChannel(LIVE_CURRENT1, myObject.get("currentPhase1").toString());
475                 }
476                 if (myObject.has("currentPhase2")) {
477                     updateChannel(LIVE_CURRENT2, myObject.get("currentPhase2").toString());
478                 }
479                 if (myObject.has("currentPhase3")) {
480                     updateChannel(LIVE_CURRENT3, myObject.get("currentPhase3").toString());
481                 }
482                 if (myObject.has("powerProduction")) {
483                     updateChannel(LIVE_POWERPRODUCTION, myObject.get("powerProduction").toString());
484                 }
485                 if (myObject.has("accumulatedProduction")) {
486                     updateChannel(LIVE_ACCUMULATEDPRODUCTION, myObject.get("accumulatedProduction").toString());
487                 }
488                 if (myObject.has("minPowerProduction")) {
489                     updateChannel(LIVE_MINPOWERPRODUCTION, myObject.get("minPowerProduction").toString());
490                 }
491                 if (myObject.has("maxPowerProduction")) {
492                     updateChannel(LIVE_MAXPOWERPRODUCTION, myObject.get("maxPowerProduction").toString());
493                 }
494             } else {
495                 logger.debug("Unknown live response from Tibber");
496             }
497         }
498
499         private void sendMessage(String message) throws IOException {
500             logger.debug("Send message: {}", message);
501             Session session = TibberHandler.this.session;
502             if (session != null) {
503                 session.getRemote().sendString(message);
504             }
505         }
506
507         public void startSubscription() {
508             String query = "{\"id\":\"1\",\"type\":\"start\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":null,\"query\":\"subscription {\\n liveMeasurement(homeId:\\\""
509                     + tibberConfig.getHomeid()
510                     + "\\\") {\\n timestamp\\n power\\n lastMeterConsumption\\n accumulatedConsumption\\n accumulatedCost\\n currency\\n minPower\\n averagePower\\n maxPower\\n"
511                     + "voltagePhase1\\n voltagePhase2\\n voltagePhase3\\n currentPhase1\\n currentPhase2\\n currentPhase3\\n powerProduction\\n accumulatedProduction\\n minPowerProduction\\n maxPowerProduction\\n }\\n }\\n\"}}";
512             try {
513                 TibberWebSocketListener socket = TibberHandler.this.socket;
514                 if (socket != null) {
515                     socket.sendMessage(query);
516                 } else {
517                     logger.debug("Socket unable to send subscription message: Socket is null");
518                 }
519             } catch (IOException e) {
520                 logger.warn("Send Message Exception: {}", e.getMessage());
521             }
522         }
523     }
524 }