2 * Copyright (c) 2010-2023 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.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 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);
122 logger.debug("Listener: No handler available for event for station with MAC {}", macAddress);
127 * Start the event listener for the Ambient Weather real-time API
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;
138 * Stop the event listener for the Ambient Weather real-time API.
141 logger.debug("Listener: Event listener stopping");
143 disconnectFromService();
148 * Initiate the connection to the Ambient Weather real-time API
150 private synchronized void connectToService() {
151 final String url = REALTIME_URL.replace("%APPKEY%", applicationKey);
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());
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);
169 logger.debug("Listener: Opening connection to ambient weather service with socket {}", socket.toString());
174 * Initiate a disconnect from the Ambient Weather real-time API
176 private void disconnectFromService() {
177 if (socket != null) {
178 logger.debug("Listener: Disconnecting socket and removing event listeners for {}", socket.toString());
186 * Attempt to reconnect to the Ambient Weather real-time API
188 private void reconnectToService() {
189 logger.debug("Listener: Attempting to reconnect to service");
190 disconnectFromService();
195 * Socket.io event callbacks
197 private Emitter.Listener onEventConnect = new Emitter.Listener() {
199 public void call(final Object... args) {
200 logger.debug("Listener: Connected! Subscribe to weather data events");
202 bridgeHandler.markBridgeOnline();
207 private Emitter.Listener onEventDisconnect = new Emitter.Listener() {
209 public void call(final Object... args) {
210 logger.debug("Listener: Disconnected from the ambient weather service)");
211 handleError(Socket.EVENT_DISCONNECT, args);
216 private Emitter.Listener onEventConnectError = new Emitter.Listener() {
218 public void call(final Object... args) {
219 handleError(Socket.EVENT_CONNECT_ERROR, args);
223 private Emitter.Listener onEventConnectTimeout = new Emitter.Listener() {
225 public void call(final Object... args) {
226 handleError(Socket.EVENT_CONNECT_TIMEOUT, args);
230 private Emitter.Listener onEventReconnect = new Emitter.Listener() {
232 public void call(final Object... args) {
233 logger.debug("Listener: Received reconnect event from service");
234 reconnectToService();
238 private Emitter.Listener onSubscribed = new Emitter.Listener() {
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());
249 private Emitter.Listener onData = new Emitter.Listener() {
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());
261 * Handlers for events
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());
273 logger.debug("Listener: Received socket event: {}, Reason: {}", event, reason);
274 bridgeHandler.markBridgeOffline(reason);
278 * Parse the subscribed event, then tell the handler to update the station info channels.
280 private void handleSubscribed(String jsonData) {
281 logger.debug("Listener: subscribed={}", jsonData);
282 // Extract the station names and locations and give to handlers
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");
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);
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);
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");
308 } catch (JsonSyntaxException e) {
309 logger.debug("Listener: Exception parsing subscribed.devices: {}", e.getMessage());
314 * Parse the weather data event, then send to handler to update the channels
316 private synchronized void handleData(String jsonData) {
317 logger.debug("Listener: Data: {}", jsonData);
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);
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() {