]> git.basschouten.com Git - openhab-addons.git/blob
3bedd73d03c4625007aff9ef12f0e45b553d6825
[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.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.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                 Arrays.asList(handlers.values()));
114         for (Map.Entry<AmbientWeatherStationHandler, String> device : handlers.entrySet()) {
115             AmbientWeatherStationHandler handler = device.getKey();
116             String mac = device.getValue();
117             if (mac.equalsIgnoreCase(macAddress)) {
118                 logger.debug("Listener: Found handler for {} with MAC {}", handler.getThing().getUID(), macAddress);
119                 return handler;
120             }
121         }
122         logger.debug("Listener: No handler available for event for station with MAC {}", macAddress);
123         return null;
124     }
125
126     /*
127      * Start the event listener for the Ambient Weather real-time API
128      */
129     public void start(String applicationKey, String apiKey, Gson gson) {
130         logger.debug("Listener: Event listener starting");
131         this.applicationKey = applicationKey;
132         this.apiKey = apiKey;
133         this.gson = gson;
134         connectToService();
135     }
136
137     /*
138      * Stop the event listener for the Ambient Weather real-time API.
139      */
140     public void stop() {
141         logger.debug("Listener: Event listener stopping");
142         sendUnsubscribe();
143         disconnectFromService();
144         handlers.clear();
145     }
146
147     /*
148      * Initiate the connection to the Ambient Weather real-time API
149      */
150     private synchronized void connectToService() {
151         final String url = REALTIME_URL.replace("%APPKEY%", applicationKey);
152         try {
153             IO.Options options = new IO.Options();
154             options.forceNew = true;
155             options.transports = new String[] { "websocket" };
156             socket = IO.socket(url, options);
157         } catch (URISyntaxException e) {
158             logger.info("Listener: URISyntaxException getting IO socket: {}", e.getMessage());
159             return;
160         }
161         socket.on(Socket.EVENT_CONNECT, onEventConnect);
162         socket.on(Socket.EVENT_CONNECT_ERROR, onEventConnectError);
163         socket.on(Socket.EVENT_CONNECT_TIMEOUT, onEventConnectTimeout);
164         socket.on(Socket.EVENT_DISCONNECT, onEventDisconnect);
165         socket.on(Socket.EVENT_RECONNECT, onEventReconnect);
166         socket.on("data", onData);
167         socket.on("subscribed", onSubscribed);
168
169         logger.debug("Listener: Opening connection to ambient weather service with socket {}", socket.toString());
170         socket.connect();
171     }
172
173     /*
174      * Initiate a disconnect from the Ambient Weather real-time API
175      */
176     private void disconnectFromService() {
177         if (socket != null) {
178             logger.debug("Listener: Disconnecting socket and removing event listeners for {}", socket.toString());
179             socket.disconnect();
180             socket.off();
181             socket = null;
182         }
183     }
184
185     /*
186      * Attempt to reconnect to the Ambient Weather real-time API
187      */
188     private void reconnectToService() {
189         logger.debug("Listener: Attempting to reconnect to service");
190         disconnectFromService();
191         connectToService();
192     }
193
194     /*
195      * Socket.io event callbacks
196      */
197     private Emitter.Listener onEventConnect = new Emitter.Listener() {
198         @Override
199         public void call(final Object... args) {
200             logger.debug("Listener: Connected! Subscribe to weather data events");
201             isConnected = true;
202             bridgeHandler.markBridgeOnline();
203             sendSubscribe();
204         }
205     };
206
207     private Emitter.Listener onEventDisconnect = new Emitter.Listener() {
208         @Override
209         public void call(final Object... args) {
210             logger.debug("Listener: Disconnected from the ambient weather service)");
211             handleError(Socket.EVENT_DISCONNECT, args);
212             isConnected = false;
213         }
214     };
215
216     private Emitter.Listener onEventConnectError = new Emitter.Listener() {
217         @Override
218         public void call(final Object... args) {
219             handleError(Socket.EVENT_CONNECT_ERROR, args);
220         }
221     };
222
223     private Emitter.Listener onEventConnectTimeout = new Emitter.Listener() {
224         @Override
225         public void call(final Object... args) {
226             handleError(Socket.EVENT_CONNECT_TIMEOUT, args);
227         }
228     };
229
230     private Emitter.Listener onEventReconnect = new Emitter.Listener() {
231         @Override
232         public void call(final Object... args) {
233             logger.debug("Listener: Received reconnect event from service");
234             reconnectToService();
235         }
236     };
237
238     private Emitter.Listener onSubscribed = new Emitter.Listener() {
239         @Override
240         public void call(final Object... args) {
241             logger.debug("Listener: Received SUBSCRIBED event");
242             // Got a response to a subscribe or unsubscribe command
243             if (args.length > 0) {
244                 handleSubscribed(((JSONObject) args[0]).toString());
245             }
246         }
247     };
248
249     private Emitter.Listener onData = new Emitter.Listener() {
250         @Override
251         public void call(final Object... args) {
252             logger.debug("Listener: Received DATA event");
253             // Got a weather data event from ambientweather.net
254             if (args.length > 0) {
255                 handleData(((JSONObject) args[0]).toString());
256             }
257         }
258     };
259
260     /*
261      * Handlers for events
262      */
263     private void handleError(String event, Object... args) {
264         String reason = "Unknown";
265         if (args.length > 0) {
266             if (args[0] instanceof String) {
267                 reason = (String) args[0];
268             } else if (args[0] instanceof Exception) {
269                 reason = String.format("Exception=%s Message=%s", args[0].getClass(),
270                         ((Exception) args[0]).getMessage());
271             }
272         }
273         logger.debug("Listener: Received socket event: {}, Reason: {}", event, reason);
274         bridgeHandler.markBridgeOffline(reason);
275     }
276
277     /*
278      * Parse the subscribed event, then tell the handler to update the station info channels.
279      */
280     private void handleSubscribed(String jsonData) {
281         logger.debug("Listener: subscribed={}", jsonData);
282         // Extract the station names and locations and give to handlers
283         try {
284             EventSubscribedJson subscribed = gson.fromJson(jsonData, EventSubscribedJson.class);
285             if (subscribed.invalidApiKeys != null) {
286                 logger.info("Listener: Invalid keys!! invalidApiKeys={}", subscribed.invalidApiKeys);
287                 bridgeHandler.markBridgeOffline("Invalid API keys");
288                 return;
289             }
290
291             if (subscribed.devices != null && subscribed.devices instanceof ArrayList) {
292                 // Convert the ArrayList back to JSON, then parse it
293                 String innerJson = gson.toJson(subscribed.devices);
294                 DeviceJson[] stations = gson.fromJson(innerJson, DeviceJson[].class);
295
296                 // Inform handlers of their name and location
297                 for (DeviceJson station : stations) {
298                     logger.debug("Listener: Subscribed event has station: name = {}, location = {}, MAC = {}",
299                             station.info.name, station.info.location, station.macAddress);
300                     sendStationInfoToHandler(station.macAddress, station.info.name, station.info.location);
301                 }
302             }
303             if (subscribed.isMethodSubscribe()) {
304                 logger.debug("Listener: Subscribed to data events. Waiting for data...");
305             } else if (subscribed.isMethodUnsubscribe()) {
306                 logger.debug("Listener: Unsubscribed from data events");
307             }
308         } catch (JsonSyntaxException e) {
309             logger.debug("Listener: Exception parsing subscribed.devices: {}", e.getMessage());
310         }
311     }
312
313     /*
314      * Parse the weather data event, then send to handler to update the channels
315      */
316     private synchronized void handleData(String jsonData) {
317         logger.debug("Listener: Data: {}", jsonData);
318         try {
319             EventDataGenericJson data = gson.fromJson(jsonData, EventDataGenericJson.class);
320             String macAddress = data == null ? null : data.macAddress;
321             if (macAddress != null && !macAddress.isEmpty()) {
322                 sendWeatherDataToHandler(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 }