2 * Copyright (c) 2010-2021 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;
17 import java.util.Arrays;
19 import java.util.concurrent.ConcurrentHashMap;
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;
29 import com.google.gson.Gson;
30 import com.google.gson.JsonSyntaxException;
32 import io.socket.client.IO;
33 import io.socket.client.Socket;
34 import io.socket.emitter.Emitter;
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.
42 * @author Mark Hilbush - Initial contribution
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%";
48 // JSON used to subscribe or unsubscribe from weather data events
49 private static final String SUB_UNSUB_JSON = "{ apiKeys: [ '%APIKEY%' ] }";
51 private final Logger logger = LoggerFactory.getLogger(AmbientWeatherEventListener.class);
53 // Maintain mapping of handler and weather station MAC address
54 private final Map<AmbientWeatherStationHandler, String> handlers = new ConcurrentHashMap<>();
56 private String apiKey;
58 private String applicationKey;
60 // Socket.io socket used to access Ambient Weather real-time API
61 private Socket socket;
63 // Identifies if connected to real-time API
64 private boolean isConnected;
68 private AmbientWeatherBridgeHandler bridgeHandler;
70 public AmbientWeatherEventListener(AmbientWeatherBridgeHandler bridgeHandler) {
71 this.bridgeHandler = bridgeHandler;
75 * Update the list of handlers to include 'handler' at MAC address 'macAddress'
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);
83 * Update the list of handlers to remove 'handler' at MAC address 'macAddress'
85 public void removeHandler(AmbientWeatherStationHandler handler, String macAddress) {
86 logger.debug("Listener: Remove station handler from list: {}", handler.getThing().getUID());
87 handlers.remove(handler);
91 * Send weather station information (station name and location) to the
92 * thing handler associated with the MAC address
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);
102 * Send an Ambient Weather data event to the station thing handler associated
103 * with the MAC address
105 private void sendWeatherDataToHandler(String macAddress, String jsonData) {
106 AmbientWeatherStationHandler handler = getHandler(macAddress);
107 if (handler != null) {
108 handler.handleWeatherDataEvent(jsonData);
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);
123 logger.debug("Listener: No handler available for event for station with MAC {}", macAddress);
128 * Start the event listener for the Ambient Weather real-time API
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;
139 * Stop the event listener for the Ambient Weather real-time API.
142 logger.debug("Listener: Event listener stopping");
144 disconnectFromService();
149 * Initiate the connection to the Ambient Weather real-time API
151 private synchronized void connectToService() {
152 final String url = REALTIME_URL.replace("%APPKEY%", applicationKey);
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());
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);
170 logger.debug("Listener: Opening connection to ambient weather service with socket {}", socket.toString());
175 * Initiate a disconnect from the Ambient Weather real-time API
177 private void disconnectFromService() {
178 if (socket != null) {
179 logger.debug("Listener: Disconnecting socket and removing event listeners for {}", socket.toString());
187 * Attempt to reconnect to the Ambient Weather real-time API
189 private void reconnectToService() {
190 logger.debug("Listener: Attempting to reconnect to service");
191 disconnectFromService();
196 * Socket.io event callbacks
198 private Emitter.Listener onEventConnect = new Emitter.Listener() {
200 public void call(final Object... args) {
201 logger.debug("Listener: Connected! Subscribe to weather data events");
203 bridgeHandler.markBridgeOnline();
208 private Emitter.Listener onEventDisconnect = new Emitter.Listener() {
210 public void call(final Object... args) {
211 logger.debug("Listener: Disconnected from the ambient weather service)");
212 handleError(Socket.EVENT_DISCONNECT, args);
217 private Emitter.Listener onEventConnectError = new Emitter.Listener() {
219 public void call(final Object... args) {
220 handleError(Socket.EVENT_CONNECT_ERROR, args);
224 private Emitter.Listener onEventConnectTimeout = new Emitter.Listener() {
226 public void call(final Object... args) {
227 handleError(Socket.EVENT_CONNECT_TIMEOUT, args);
231 private Emitter.Listener onEventReconnect = new Emitter.Listener() {
233 public void call(final Object... args) {
234 logger.debug("Listener: Received reconnect event from service");
235 reconnectToService();
239 private Emitter.Listener onSubscribed = new Emitter.Listener() {
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());
250 private Emitter.Listener onData = new Emitter.Listener() {
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());
262 * Handlers for events
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());
274 logger.debug("Listener: Received socket event: {}, Reason: {}", event, reason);
275 bridgeHandler.markBridgeOffline(reason);
279 * Parse the subscribed event, then tell the handler to update the station info channels.
281 private void handleSubscribed(String jsonData) {
282 logger.debug("Listener: subscribed={}", jsonData);
283 // Extract the station names and locations and give to handlers
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");
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);
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);
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");
309 } catch (JsonSyntaxException e) {
310 logger.debug("Listener: Exception parsing subscribed.devices: {}", e.getMessage());
315 * Parse the weather data event, then send to handler to update the channels
317 private synchronized void handleData(String jsonData) {
318 logger.debug("Listener: Data: {}", jsonData);
320 EventDataGenericJson data = gson.fromJson(jsonData, EventDataGenericJson.class);
321 if (StringUtils.isNotEmpty(data.macAddress)) {
322 sendWeatherDataToHandler(data.macAddress, jsonData);
324 } catch (JsonSyntaxException e) {
325 logger.info("Listener: Exception parsing subscribed event: {}", e.getMessage());
330 * Subscribe to weather data events for stations associated with the API key
332 private void sendSubscribe() {
333 if (apiKey == null) {
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));
344 * Unsubscribe from weather data events for stations associated with the API key
346 private void sendUnsubscribe() {
347 if (apiKey == null) {
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));
358 * Resubscribe when a handler is initialized
360 public synchronized void resubscribe() {