]> git.basschouten.com Git - openhab-addons.git/blob
031675e6f8461ae979c37a19d94dc2634cb5f540
[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.ambientweather.internal.handler;
14
15 import java.net.URISyntaxException;
16 import java.util.ArrayList;
17 import java.util.Map;
18 import java.util.concurrent.ConcurrentHashMap;
19
20 import org.apache.commons.lang.StringUtils;
21 import org.json.JSONObject;
22 import org.openhab.binding.ambientweather.internal.model.DeviceJson;
23 import org.openhab.binding.ambientweather.internal.model.EventDataGenericJson;
24 import org.openhab.binding.ambientweather.internal.model.EventSubscribedJson;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 import com.google.gson.Gson;
29 import com.google.gson.JsonSyntaxException;
30
31 import io.socket.client.IO;
32 import io.socket.client.Socket;
33 import io.socket.emitter.Emitter;
34
35 /**
36  * The {@link AmbientWeatherEventListener} is responsible for establishing
37  * a socket.io connection with ambientweather.net, subscribing/unsubscribing
38  * to data events, receiving data events through the real-time socket.io API,
39  * and for routing the data events to a weather station thing handler for processing.
40  *
41  * @author Mark Hilbush - Initial contribution
42  */
43 public class AmbientWeatherEventListener {
44     // URL used to get the realtime event stream
45     private static final String REALTIME_URL = "https://api.ambientweather.net/?api=1&applicationKey=%APPKEY%";
46
47     // JSON used to subscribe or unsubscribe from weather data events
48     private static final String SUB_UNSUB_JSON = "{ apiKeys: [ '%APIKEY%' ] }";
49
50     private final Logger logger = LoggerFactory.getLogger(AmbientWeatherEventListener.class);
51
52     // Maintain mapping of handler and weather station MAC address
53     private final Map<AmbientWeatherStationHandler, String> handlers = new ConcurrentHashMap<>();
54
55     private String apiKey;
56
57     private String applicationKey;
58
59     // Socket.io socket used to access Ambient Weather real-time API
60     private Socket socket;
61
62     // Identifies if connected to real-time API
63     private boolean isConnected;
64
65     private Gson gson;
66
67     private AmbientWeatherBridgeHandler bridgeHandler;
68
69     public AmbientWeatherEventListener(AmbientWeatherBridgeHandler bridgeHandler) {
70         this.bridgeHandler = bridgeHandler;
71     }
72
73     /*
74      * Update the list of handlers to include 'handler' at MAC address 'macAddress'
75      */
76     public void addHandler(AmbientWeatherStationHandler handler, String macAddress) {
77         logger.debug("Listener: Add station handler to list: {}", handler.getThing().getUID());
78         handlers.put(handler, macAddress);
79     }
80
81     /*
82      * Update the list of handlers to remove 'handler' at MAC address 'macAddress'
83      */
84     public void removeHandler(AmbientWeatherStationHandler handler, String macAddress) {
85         logger.debug("Listener: Remove station handler from list: {}", handler.getThing().getUID());
86         handlers.remove(handler);
87     }
88
89     /*
90      * Send weather station information (station name and location) to the
91      * thing handler associated with the MAC address
92      */
93     private void sendStationInfoToHandler(String macAddress, String name, String location) {
94         AmbientWeatherStationHandler handler = getHandler(macAddress);
95         if (handler != null) {
96             handler.handleInfoEvent(macAddress, name, location);
97         }
98     }
99
100     /*
101      * Send an Ambient Weather data event to the station thing handler associated
102      * with the MAC address
103      */
104     private void sendWeatherDataToHandler(String macAddress, String jsonData) {
105         AmbientWeatherStationHandler handler = getHandler(macAddress);
106         if (handler != null) {
107             handler.handleWeatherDataEvent(jsonData);
108         }
109     }
110
111     private AmbientWeatherStationHandler getHandler(String macAddress) {
112         logger.debug("Listener: Search for MAC {} in handlers list with {} entries", macAddress, handlers.size());
113         for (Map.Entry<AmbientWeatherStationHandler, String> device : handlers.entrySet()) {
114             AmbientWeatherStationHandler handler = device.getKey();
115             String mac = device.getValue();
116             if (mac.equals(macAddress)) {
117                 logger.debug("Listener: Found handler for {} with MAC {}", handler.getThing().getUID(), macAddress);
118                 return handler;
119             }
120         }
121         logger.debug("Listener: No handler available for event for station with MAC {}", macAddress);
122         return null;
123     }
124
125     /*
126      * Start the event listener for the Ambient Weather real-time API
127      */
128     public void start(String applicationKey, String apiKey, Gson gson) {
129         logger.debug("Listener: Event listener starting");
130         this.applicationKey = applicationKey;
131         this.apiKey = apiKey;
132         this.gson = gson;
133         connectToService();
134     }
135
136     /*
137      * Stop the event listener for the Ambient Weather real-time API.
138      */
139     public void stop() {
140         logger.debug("Listener: Event listener stopping");
141         sendUnsubscribe();
142         disconnectFromService();
143         handlers.clear();
144     }
145
146     /*
147      * Initiate the connection to the Ambient Weather real-time API
148      */
149     private synchronized void connectToService() {
150         final String url = REALTIME_URL.replace("%APPKEY%", applicationKey);
151         try {
152             IO.Options options = new IO.Options();
153             options.forceNew = true;
154             options.transports = new String[] { "websocket" };
155             socket = IO.socket(url, options);
156         } catch (URISyntaxException e) {
157             logger.info("Listener: URISyntaxException getting IO socket: {}", e.getMessage());
158             return;
159         }
160         socket.on(Socket.EVENT_CONNECT, onEventConnect);
161         socket.on(Socket.EVENT_CONNECT_ERROR, onEventConnectError);
162         socket.on(Socket.EVENT_CONNECT_TIMEOUT, onEventConnectTimeout);
163         socket.on(Socket.EVENT_DISCONNECT, onEventDisconnect);
164         socket.on(Socket.EVENT_RECONNECT, onEventReconnect);
165         socket.on("data", onData);
166         socket.on("subscribed", onSubscribed);
167
168         logger.debug("Listener: Opening connection to ambient weather service with socket {}", socket.toString());
169         socket.connect();
170     }
171
172     /*
173      * Initiate a disconnect from the Ambient Weather real-time API
174      */
175     private void disconnectFromService() {
176         if (socket != null) {
177             logger.debug("Listener: Disconnecting socket and removing event listeners for {}", socket.toString());
178             socket.disconnect();
179             socket.off();
180             socket = null;
181         }
182     }
183
184     /*
185      * Attempt to reconnect to the Ambient Weather real-time API
186      */
187     private void reconnectToService() {
188         logger.debug("Listener: Attempting to reconnect to service");
189         disconnectFromService();
190         connectToService();
191     }
192
193     /*
194      * Socket.io event callbacks
195      */
196     private Emitter.Listener onEventConnect = new Emitter.Listener() {
197         @Override
198         public void call(final Object... args) {
199             logger.debug("Listener: Connected! Subscribe to weather data events");
200             isConnected = true;
201             bridgeHandler.markBridgeOnline();
202             sendSubscribe();
203         }
204     };
205
206     private Emitter.Listener onEventDisconnect = new Emitter.Listener() {
207         @Override
208         public void call(final Object... args) {
209             logger.debug("Listener: Disconnected from the ambient weather service)");
210             handleError(Socket.EVENT_DISCONNECT, args);
211             isConnected = false;
212         }
213     };
214
215     private Emitter.Listener onEventConnectError = new Emitter.Listener() {
216         @Override
217         public void call(final Object... args) {
218             handleError(Socket.EVENT_CONNECT_ERROR, args);
219         }
220     };
221
222     private Emitter.Listener onEventConnectTimeout = new Emitter.Listener() {
223         @Override
224         public void call(final Object... args) {
225             handleError(Socket.EVENT_CONNECT_TIMEOUT, args);
226         }
227     };
228
229     private Emitter.Listener onEventReconnect = new Emitter.Listener() {
230         @Override
231         public void call(final Object... args) {
232             logger.debug("Listener: Received reconnect event from service");
233             reconnectToService();
234         }
235     };
236
237     private Emitter.Listener onSubscribed = new Emitter.Listener() {
238         @Override
239         public void call(final Object... args) {
240             logger.debug("Listener: Received SUBSCRIBED event");
241             // Got a response to a subscribe or unsubscribe command
242             if (args.length > 0) {
243                 handleSubscribed(((JSONObject) args[0]).toString());
244             }
245         }
246     };
247
248     private Emitter.Listener onData = new Emitter.Listener() {
249         @Override
250         public void call(final Object... args) {
251             logger.debug("Listener: Received DATA event");
252             // Got a weather data event from ambientweather.net
253             if (args.length > 0) {
254                 handleData(((JSONObject) args[0]).toString());
255             }
256         }
257     };
258
259     /*
260      * Handlers for events
261      */
262     private void handleError(String event, Object... args) {
263         String reason = "Unknown";
264         if (args.length > 0) {
265             if (args[0] instanceof String) {
266                 reason = (String) args[0];
267             } else if (args[0] instanceof Exception) {
268                 reason = String.format("Exception=%s Message=%s", args[0].getClass(),
269                         ((Exception) args[0]).getMessage());
270             }
271         }
272         logger.debug("Listener: Received socket event: {}, Reason: {}", event, reason);
273         bridgeHandler.markBridgeOffline(reason);
274     }
275
276     /*
277      * Parse the subscribed event, then tell the handler to update the station info channels.
278      */
279     private void handleSubscribed(String jsonData) {
280         logger.debug("Listener: subscribed={}", jsonData);
281         // Extract the station names and locations and give to handlers
282         try {
283             EventSubscribedJson subscribed = gson.fromJson(jsonData, EventSubscribedJson.class);
284             if (subscribed.invalidApiKeys != null) {
285                 logger.info("Listener: Invalid keys!! invalidApiKeys={}", subscribed.invalidApiKeys);
286                 bridgeHandler.markBridgeOffline("Invalid API keys");
287                 return;
288             }
289
290             if (subscribed.devices != null && subscribed.devices instanceof ArrayList) {
291                 // Convert the ArrayList back to JSON, then parse it
292                 String innerJson = gson.toJson(subscribed.devices);
293                 DeviceJson[] stations = gson.fromJson(innerJson, DeviceJson[].class);
294
295                 // Inform handlers of their name and location
296                 for (DeviceJson station : stations) {
297                     logger.debug("Listener: Subscribed event has station: name = {}, location = {}, MAC = {}",
298                             station.info.name, station.info.location, station.macAddress);
299                     sendStationInfoToHandler(station.macAddress, station.info.name, station.info.location);
300                 }
301             }
302             if (subscribed.isMethodSubscribe()) {
303                 logger.debug("Listener: Subscribed to data events. Waiting for data...");
304             } else if (subscribed.isMethodUnsubscribe()) {
305                 logger.debug("Listener: Unsubscribed from data events");
306             }
307         } catch (JsonSyntaxException e) {
308             logger.debug("Listener: Exception parsing subscribed.devices: {}", e.getMessage());
309         }
310     }
311
312     /*
313      * Parse the weather data event, then send to handler to update the channels
314      */
315     private synchronized void handleData(String jsonData) {
316         logger.debug("Listener: Data: {}", jsonData);
317         try {
318             EventDataGenericJson data = gson.fromJson(jsonData, EventDataGenericJson.class);
319             if (StringUtils.isNotEmpty(data.macAddress)) {
320                 sendWeatherDataToHandler(data.macAddress, jsonData);
321             }
322         } catch (JsonSyntaxException e) {
323             logger.info("Listener: Exception parsing subscribed event: {}", e.getMessage());
324         }
325     }
326
327     /*
328      * Subscribe to weather data events for stations associated with the API key
329      */
330     private void sendSubscribe() {
331         if (apiKey == null) {
332             return;
333         }
334         final String sub = SUB_UNSUB_JSON.replace("%APIKEY%", apiKey);
335         if (isConnected && socket != null) {
336             logger.debug("Listener: Sending subscribe request");
337             socket.emit("subscribe", new JSONObject(sub));
338         }
339     }
340
341     /*
342      * Unsubscribe from weather data events for stations associated with the API key
343      */
344     private void sendUnsubscribe() {
345         if (apiKey == null) {
346             return;
347         }
348         final String unsub = SUB_UNSUB_JSON.replace("%APIKEY%", apiKey);
349         if (isConnected && socket != null) {
350             logger.debug("Listener: Sending unsubscribe request");
351             socket.emit("unsubscribe", new JSONObject(unsub));
352         }
353     }
354
355     /*
356      * Resubscribe when a handler is initialized
357      */
358     public synchronized void resubscribe() {
359         sendUnsubscribe();
360         sendSubscribe();
361     }
362 }