2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ambientweather.internal.handler;
15 import java.net.URISyntaxException;
16 import java.util.ArrayList;
18 import java.util.concurrent.ConcurrentHashMap;
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;
28 import com.google.gson.Gson;
29 import com.google.gson.JsonSyntaxException;
31 import io.socket.client.IO;
32 import io.socket.client.Socket;
33 import io.socket.emitter.Emitter;
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.
41 * @author Mark Hilbush - Initial contribution
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%";
47 // JSON used to subscribe or unsubscribe from weather data events
48 private static final String SUB_UNSUB_JSON = "{ apiKeys: [ '%APIKEY%' ] }";
50 private final Logger logger = LoggerFactory.getLogger(AmbientWeatherEventListener.class);
52 // Maintain mapping of handler and weather station MAC address
53 private final Map<AmbientWeatherStationHandler, String> handlers = new ConcurrentHashMap<>();
55 private String apiKey;
57 private String applicationKey;
59 // Socket.io socket used to access Ambient Weather real-time API
60 private Socket socket;
62 // Identifies if connected to real-time API
63 private boolean isConnected;
67 private AmbientWeatherBridgeHandler bridgeHandler;
69 public AmbientWeatherEventListener(AmbientWeatherBridgeHandler bridgeHandler) {
70 this.bridgeHandler = bridgeHandler;
74 * Update the list of handlers to include 'handler' at MAC address 'macAddress'
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);
82 * Update the list of handlers to remove 'handler' at MAC address 'macAddress'
84 public void removeHandler(AmbientWeatherStationHandler handler, String macAddress) {
85 logger.debug("Listener: Remove station handler from list: {}", handler.getThing().getUID());
86 handlers.remove(handler);
90 * Send weather station information (station name and location) to the
91 * thing handler associated with the MAC address
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);
101 * Send an Ambient Weather data event to the station thing handler associated
102 * with the MAC address
104 private void sendWeatherDataToHandler(String macAddress, String jsonData) {
105 AmbientWeatherStationHandler handler = getHandler(macAddress);
106 if (handler != null) {
107 handler.handleWeatherDataEvent(jsonData);
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);
121 logger.debug("Listener: No handler available for event for station with MAC {}", macAddress);
126 * Start the event listener for the Ambient Weather real-time API
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;
137 * Stop the event listener for the Ambient Weather real-time API.
140 logger.debug("Listener: Event listener stopping");
142 disconnectFromService();
147 * Initiate the connection to the Ambient Weather real-time API
149 private synchronized void connectToService() {
150 final String url = REALTIME_URL.replace("%APPKEY%", applicationKey);
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());
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);
168 logger.debug("Listener: Opening connection to ambient weather service with socket {}", socket.toString());
173 * Initiate a disconnect from the Ambient Weather real-time API
175 private void disconnectFromService() {
176 if (socket != null) {
177 logger.debug("Listener: Disconnecting socket and removing event listeners for {}", socket.toString());
185 * Attempt to reconnect to the Ambient Weather real-time API
187 private void reconnectToService() {
188 logger.debug("Listener: Attempting to reconnect to service");
189 disconnectFromService();
194 * Socket.io event callbacks
196 private Emitter.Listener onEventConnect = new Emitter.Listener() {
198 public void call(final Object... args) {
199 logger.debug("Listener: Connected! Subscribe to weather data events");
201 bridgeHandler.markBridgeOnline();
206 private Emitter.Listener onEventDisconnect = new Emitter.Listener() {
208 public void call(final Object... args) {
209 logger.debug("Listener: Disconnected from the ambient weather service)");
210 handleError(Socket.EVENT_DISCONNECT, args);
215 private Emitter.Listener onEventConnectError = new Emitter.Listener() {
217 public void call(final Object... args) {
218 handleError(Socket.EVENT_CONNECT_ERROR, args);
222 private Emitter.Listener onEventConnectTimeout = new Emitter.Listener() {
224 public void call(final Object... args) {
225 handleError(Socket.EVENT_CONNECT_TIMEOUT, args);
229 private Emitter.Listener onEventReconnect = new Emitter.Listener() {
231 public void call(final Object... args) {
232 logger.debug("Listener: Received reconnect event from service");
233 reconnectToService();
237 private Emitter.Listener onSubscribed = new Emitter.Listener() {
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());
248 private Emitter.Listener onData = new Emitter.Listener() {
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());
260 * Handlers for events
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());
272 logger.debug("Listener: Received socket event: {}, Reason: {}", event, reason);
273 bridgeHandler.markBridgeOffline(reason);
277 * Parse the subscribed event, then tell the handler to update the station info channels.
279 private void handleSubscribed(String jsonData) {
280 logger.debug("Listener: subscribed={}", jsonData);
281 // Extract the station names and locations and give to handlers
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");
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);
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);
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");
307 } catch (JsonSyntaxException e) {
308 logger.debug("Listener: Exception parsing subscribed.devices: {}", e.getMessage());
313 * Parse the weather data event, then send to handler to update the channels
315 private synchronized void handleData(String jsonData) {
316 logger.debug("Listener: Data: {}", jsonData);
318 EventDataGenericJson data = gson.fromJson(jsonData, EventDataGenericJson.class);
319 if (StringUtils.isNotEmpty(data.macAddress)) {
320 sendWeatherDataToHandler(data.macAddress, jsonData);
322 } catch (JsonSyntaxException e) {
323 logger.info("Listener: Exception parsing subscribed event: {}", e.getMessage());
328 * Subscribe to weather data events for stations associated with the API key
330 private void sendSubscribe() {
331 if (apiKey == null) {
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));
342 * Unsubscribe from weather data events for stations associated with the API key
344 private void sendUnsubscribe() {
345 if (apiKey == null) {
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));
356 * Resubscribe when a handler is initialized
358 public synchronized void resubscribe() {