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