]> git.basschouten.com Git - openhab-addons.git/blob
1b84e68922a6e626956361d12569e7f798da702b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.russound.internal.rio.system;
14
15 import java.io.IOException;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
18 import java.util.concurrent.atomic.AtomicReference;
19 import java.util.concurrent.locks.ReentrantLock;
20
21 import org.openhab.binding.russound.internal.discovery.RioSystemDeviceDiscoveryService;
22 import org.openhab.binding.russound.internal.net.SocketChannelSession;
23 import org.openhab.binding.russound.internal.net.SocketSession;
24 import org.openhab.binding.russound.internal.rio.AbstractBridgeHandler;
25 import org.openhab.binding.russound.internal.rio.AbstractRioHandlerCallback;
26 import org.openhab.binding.russound.internal.rio.RioConstants;
27 import org.openhab.binding.russound.internal.rio.RioHandlerCallback;
28 import org.openhab.binding.russound.internal.rio.RioHandlerCallbackListener;
29 import org.openhab.binding.russound.internal.rio.RioPresetsProtocol;
30 import org.openhab.binding.russound.internal.rio.RioSystemFavoritesProtocol;
31 import org.openhab.binding.russound.internal.rio.StatefulHandlerCallback;
32 import org.openhab.binding.russound.internal.rio.controller.RioControllerHandler;
33 import org.openhab.binding.russound.internal.rio.models.GsonUtilities;
34 import org.openhab.binding.russound.internal.rio.source.RioSourceHandler;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import com.google.gson.Gson;
50
51 /**
52  * The bridge handler for a Russound System. This is the entry point into the whole russound system and is generally
53  * points to the main controller. This implementation must be attached to a {@link RioSystemHandler} bridge.
54  *
55  * @author Tim Roberts - Initial contribution
56  */
57 public class RioSystemHandler extends AbstractBridgeHandler<RioSystemProtocol> {
58     // Logger
59     private final Logger logger = LoggerFactory.getLogger(RioSystemHandler.class);
60
61     /**
62      * The configuration for the system - will be recreated when the configuration changes and will be null when not
63      * online
64      */
65     private RioSystemConfig config;
66
67     /**
68      * The lock used to control access to {@link #config}
69      */
70     private final ReentrantLock configLock = new ReentrantLock();
71
72     /**
73      * The {@link SocketSession} telnet session to the switch. Will be null if not connected.
74      */
75     private SocketSession session;
76
77     /**
78      * The lock used to control access to {@link #session}
79      */
80     private final ReentrantLock sessionLock = new ReentrantLock();
81
82     /**
83      * The retry connection event - will only be created when we are retrying the connection attempt
84      */
85     private ScheduledFuture<?> retryConnection;
86
87     /**
88      * The lock used to control access to {@link #retryConnection}
89      */
90     private final ReentrantLock retryConnectionLock = new ReentrantLock();
91
92     /**
93      * The ping event - will be non-null when online (null otherwise)
94      */
95     private ScheduledFuture<?> ping;
96
97     /**
98      * The lock used to control access to {@link #ping}
99      */
100     private final ReentrantLock pingLock = new ReentrantLock();
101
102     /**
103      * {@link Gson} used for JSON serialization/deserialization
104      */
105     private final Gson gson = GsonUtilities.createGson();
106
107     /**
108      * Callback listener to use when source name changes - will call {@link #refreshNamedHandler(Gson, Class, String)}
109      * to
110      * refresh the {@link RioConstants#CHANNEL_SYSSOURCES} channel
111      */
112     private final RioHandlerCallbackListener handlerCallbackListener = new RioHandlerCallbackListener() {
113         @Override
114         public void stateUpdate(String channelId, State state) {
115             refreshNamedHandler(gson, RioSourceHandler.class, RioConstants.CHANNEL_SYSSOURCES);
116         }
117     };
118
119     /**
120      * The protocol for favorites handling
121      */
122     private final AtomicReference<RioSystemFavoritesProtocol> favoritesProtocol = new AtomicReference<>(null);
123
124     /**
125      * The protocol for presets handling
126      */
127     private final AtomicReference<RioPresetsProtocol> presetsProtocol = new AtomicReference<>(null);
128
129     /**
130      * The discovery service to discover the zones/sources, etc
131      * Will be null if not active.
132      */
133     private final AtomicReference<RioSystemDeviceDiscoveryService> discoveryService = new AtomicReference<>(null);
134
135     /**
136      * Constructs the handler from the {@link Bridge}
137      *
138      * @param bridge a non-null {@link Bridge} the handler is for
139      */
140     public RioSystemHandler(Bridge bridge) {
141         super(bridge);
142     }
143
144     /**
145      * Overrides the base method since we are the source of the {@link SocketSession}.
146      *
147      * @return the {@link SocketSession} once initialized. Null if not initialized or disposed of
148      */
149     @Override
150     public SocketSession getSocketSession() {
151         sessionLock.lock();
152         try {
153             return session;
154         } finally {
155             sessionLock.unlock();
156         }
157     }
158
159     /**
160      * {@inheritDoc}
161      *
162      * Handles commands to specific channels. This implementation will offload much of its work to the
163      * {@link RioSystemProtocol}. Basically we validate the type of command for the channel then call the
164      * {@link RioSystemProtocol} to handle the actual protocol. Special use case is the {@link RefreshType}
165      * where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
166      * {@link RioSystemProtocol} to handle the actual refresh
167      */
168     @Override
169     public void handleCommand(ChannelUID channelUID, Command command) {
170         if (command instanceof RefreshType) {
171             handleRefresh(channelUID.getId());
172             return;
173         }
174
175         String id = channelUID.getId();
176
177         if (id == null) {
178             logger.debug("Called with a null channel id - ignoring");
179             return;
180         }
181
182         if (id.equals(RioConstants.CHANNEL_SYSLANG)) {
183             if (command instanceof StringType stringCommand) {
184                 getProtocolHandler().setSystemLanguage(stringCommand.toString());
185             } else {
186                 logger.debug("Received a SYSTEM LANGUAGE channel command with a non StringType: {}", command);
187             }
188         } else if (id.equals(RioConstants.CHANNEL_SYSALLON)) {
189             if (command instanceof OnOffType) {
190                 getProtocolHandler().setSystemAllOn(command == OnOffType.ON);
191             } else {
192                 logger.debug("Received a SYSTEM ALL ON channel command with a non OnOffType: {}", command);
193             }
194         } else {
195             logger.debug("Unknown/Unsupported Channel id: {}", id);
196         }
197     }
198
199     /**
200      * Method that handles the {@link RefreshType} command specifically. Calls the {@link RioSystemProtocol} to
201      * handle the actual refresh based on the channel id.
202      *
203      * @param id a non-null, possibly empty channel id to refresh
204      */
205     private void handleRefresh(String id) {
206         if (getThing().getStatus() != ThingStatus.ONLINE) {
207             return;
208         }
209
210         if (getProtocolHandler() == null) {
211             return;
212         }
213
214         // Remove the cache'd value to force a refreshed value
215         ((StatefulHandlerCallback) getProtocolHandler().getCallback()).removeState(id);
216
217         if (id.equals(RioConstants.CHANNEL_SYSLANG)) {
218             getProtocolHandler().refreshSystemLanguage();
219
220         } else if (id.equals(RioConstants.CHANNEL_SYSSTATUS)) {
221             getProtocolHandler().refreshSystemStatus();
222
223         } else if (id.equals(RioConstants.CHANNEL_SYSALLON)) {
224             getProtocolHandler().refreshSystemAllOn();
225
226         } else if (id.equals(RioConstants.CHANNEL_SYSCONTROLLERS)) {
227             refreshNamedHandler(gson, RioControllerHandler.class, RioConstants.CHANNEL_SYSCONTROLLERS);
228         } else if (id.equals(RioConstants.CHANNEL_SYSSOURCES)) {
229             refreshNamedHandler(gson, RioSourceHandler.class, RioConstants.CHANNEL_SYSSOURCES);
230
231         }
232         // Can't refresh any others...
233     }
234
235     /**
236      * {@inheritDoc}
237      *
238      * Initializes the handler. This initialization will read/validate the configuration, then will create the
239      * {@link SocketSession} and will attempt to connect via {@link #connect()}.
240      */
241     @Override
242     public void initialize() {
243         final RioSystemConfig rioConfig = getRioConfig();
244
245         if (rioConfig == null) {
246             return;
247         }
248
249         if (rioConfig.getIpAddress() == null || rioConfig.getIpAddress().trim().length() == 0) {
250             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
251                     "IP Address of Russound is missing from configuration");
252             return;
253         }
254
255         sessionLock.lock();
256         try {
257             session = new SocketChannelSession(rioConfig.getIpAddress(), RioConstants.RIO_PORT);
258         } finally {
259             sessionLock.unlock();
260         }
261
262         // Try initial connection in a scheduled task
263         this.scheduler.schedule(this::connect, 1, TimeUnit.SECONDS);
264     }
265
266     /**
267      * Attempts to connect to the system. If successfully connect, the {@link RioSystemProtocol#login()} will be
268      * called to log into the system (if needed). Once completed, a ping job will be created to keep the connection
269      * alive. If a connection cannot be established (or login failed), the connection attempt will be retried later (via
270      * {@link #retryConnect()})
271      */
272     private void connect() {
273         String response = "Server is offline - will try to reconnect later";
274
275         sessionLock.lock();
276         pingLock.lock();
277         try {
278             session.connect();
279
280             final StatefulHandlerCallback callback = new StatefulHandlerCallback(new AbstractRioHandlerCallback() {
281                 @Override
282                 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
283                     updateStatus(status, detail, msg);
284                     if (status != ThingStatus.ONLINE) {
285                         disconnect();
286                         reconnect();
287                     }
288                 }
289
290                 @Override
291                 public void stateChanged(String channelId, State state) {
292                     updateState(channelId, state);
293                     fireStateUpdated(channelId, state);
294                 }
295
296                 @Override
297                 public void setProperty(String propertyName, String propertyValue) {
298                     getThing().setProperty(propertyName, propertyValue);
299                 }
300             });
301
302             setProtocolHandler(new RioSystemProtocol(session, callback));
303             favoritesProtocol.set(new RioSystemFavoritesProtocol(session, callback));
304             presetsProtocol.set(new RioPresetsProtocol(session, callback));
305
306             response = getProtocolHandler().login();
307             if (response == null) {
308                 final RioSystemConfig rioConfig = getRioConfig();
309                 if (rioConfig != null) {
310                     ping = this.scheduler.scheduleWithFixedDelay(() -> {
311                         try {
312                             final ThingStatus status = getThing().getStatus();
313                             if (status == ThingStatus.ONLINE) {
314                                 if (session.isConnected()) {
315                                     getProtocolHandler().ping();
316                                 }
317                             }
318                         } catch (Exception e) {
319                             logger.error("Exception while pinging: {}", e.getMessage(), e);
320                         }
321                     }, rioConfig.getPing(), rioConfig.getPing(), TimeUnit.SECONDS);
322
323                     logger.debug("Going online!");
324                     updateStatus(ThingStatus.ONLINE);
325                     startScan(rioConfig);
326                     refreshNamedHandler(gson, RioSourceHandler.class, RioConstants.CHANNEL_SYSSOURCES);
327                     refreshNamedHandler(gson, RioControllerHandler.class, RioConstants.CHANNEL_SYSCONTROLLERS);
328
329                     return;
330                 } else {
331                     logger.debug("getRioConfig returned a null!");
332                 }
333             } else {
334                 logger.warn("Login return {}", response);
335             }
336
337         } catch (Exception e) {
338             logger.error("Error connecting: {}", e.getMessage(), e);
339             // do nothing
340         } finally {
341             pingLock.unlock();
342             sessionLock.unlock();
343         }
344
345         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response);
346         reconnect();
347     }
348
349     /**
350      * Retries the connection attempt - schedules a job in {@link RioSystemConfig#getRetryPolling()} seconds to
351      * call the {@link #connect()} method. If a retry attempt is pending, the request is ignored.
352      */
353     @Override
354     protected void reconnect() {
355         retryConnectionLock.lock();
356         try {
357             if (retryConnection == null) {
358                 final RioSystemConfig rioConfig = getRioConfig();
359                 if (rioConfig != null) {
360                     logger.info("Will try to reconnect in {} seconds", rioConfig.getRetryPolling());
361                     retryConnection = this.scheduler.schedule(() -> {
362                         retryConnection = null;
363                         try {
364                             if (getThing().getStatus() != ThingStatus.ONLINE) {
365                                 connect();
366                             }
367                         } catch (Exception e) {
368                             logger.error("Exception connecting: {}", e.getMessage(), e);
369                         }
370                     }, rioConfig.getRetryPolling(), TimeUnit.SECONDS);
371                 }
372             } else {
373                 logger.debug("RetryConnection called when a retry connection is pending - ignoring request");
374             }
375         } finally {
376             retryConnectionLock.unlock();
377         }
378     }
379
380     /**
381      * {@inheritDoc}
382      *
383      * Attempts to disconnect from the session. The protocol handler will be set to null, the {@link #ping} will be
384      * cancelled/set to null and the {@link #session} will be disconnected
385      */
386     @Override
387     protected void disconnect() {
388         // Cancel ping
389         pingLock.lock();
390         try {
391             if (ping != null) {
392                 ping.cancel(true);
393                 ping = null;
394             }
395         } finally {
396             pingLock.unlock();
397         }
398
399         if (getProtocolHandler() != null) {
400             getProtocolHandler().watchSystem(false);
401             setProtocolHandler(null);
402         }
403
404         sessionLock.lock();
405         try {
406             session.disconnect();
407         } catch (IOException e) {
408             // ignore - we don't care
409         } finally {
410             sessionLock.unlock();
411         }
412     }
413
414     /**
415      * Simple gets the {@link RioSystemConfig} from the {@link Thing} and will set the status to offline if not
416      * found.
417      *
418      * @return a possible null {@link RioSystemConfig}
419      */
420     public RioSystemConfig getRioConfig() {
421         configLock.lock();
422         try {
423             final RioSystemConfig sysConfig = getThing().getConfiguration().as(RioSystemConfig.class);
424
425             if (sysConfig == null) {
426                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
427             } else {
428                 config = sysConfig;
429             }
430             return config;
431         } finally {
432             configLock.unlock();
433         }
434     }
435
436     /**
437      * Registers the {@link RioSystemDeviceDiscoveryService} with this handler. The discovery service will be called in
438      * {@link #startScan(RioSystemConfig)} when a device should be scanned and 'things' discovered from it
439      *
440      * @param service a possibly null {@link RioSystemDeviceDiscoveryService}
441      */
442     public void registerDiscoveryService(RioSystemDeviceDiscoveryService service) {
443         discoveryService.set(service);
444     }
445
446     /**
447      * Helper method to possibly start a scan. A scan will ONLY be started if the {@link RioSystemConfig#isScanDevice()}
448      * is true and a discovery service has been set ({@link #registerDiscoveryService(RioSystemDeviceDiscoveryService)})
449      *
450      * @param sysConfig a non-null {@link RioSystemConfig}
451      */
452     private void startScan(RioSystemConfig sysConfig) {
453         final RioSystemDeviceDiscoveryService service = discoveryService.get();
454         if (service != null) {
455             if (sysConfig != null && sysConfig.isScanDevice()) {
456                 this.scheduler.execute(() -> {
457                     logger.info("Starting device discovery");
458                     service.scanDevice();
459                 });
460             }
461         }
462     }
463
464     /**
465      * Overrides the base to call {@link #childChanged(ThingHandler)} to recreate the sources/controllers names
466      */
467     @Override
468     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
469         childChanged(childHandler, true);
470     }
471
472     /**
473      * Overrides the base to call {@link #childChanged(ThingHandler)} to recreate the sources/controllers names
474      */
475     @Override
476     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
477         childChanged(childHandler, false);
478     }
479
480     /**
481      * Helper method to recreate the {@link RioConstants#CHANNEL_SYSSOURCES} &&
482      * {@link RioConstants#CHANNEL_SYSCONTROLLERS} channels
483      *
484      * @param childHandler a non-null child handler that changed
485      * @param added true if added, false otherwise
486      * @throw IllegalArgumentException if childHandler is null
487      */
488     private void childChanged(ThingHandler childHandler, boolean added) {
489         if (childHandler == null) {
490             throw new IllegalArgumentException("childHandler cannot be null");
491         }
492         if (childHandler instanceof RioSourceHandler sourceHandler) {
493             final RioHandlerCallback callback = sourceHandler.getRioHandlerCallback();
494             if (callback != null) {
495                 if (added) {
496                     callback.addListener(RioConstants.CHANNEL_SOURCENAME, handlerCallbackListener);
497                 } else {
498                     callback.removeListener(RioConstants.CHANNEL_SOURCENAME, handlerCallbackListener);
499                 }
500             }
501             refreshNamedHandler(gson, RioSourceHandler.class, RioConstants.CHANNEL_SYSSOURCES);
502         } else if (childHandler instanceof RioControllerHandler) {
503             refreshNamedHandler(gson, RioControllerHandler.class, RioConstants.CHANNEL_SYSCONTROLLERS);
504         }
505     }
506
507     /**
508      * Returns the {@link RioSystemFavoritesProtocol} for the system
509      *
510      * @return a possibly null {@link RioSystemFavoritesProtocol}
511      */
512     @Override
513     public RioSystemFavoritesProtocol getSystemFavoritesHandler() {
514         return favoritesProtocol.get();
515     }
516
517     /**
518      * Returns the {@link RioPresetsProtocol} for the system
519      *
520      * @return a possibly null {@link RioPresetsProtocol}
521      */
522     @Override
523     public RioPresetsProtocol getPresetsProtocol() {
524         return presetsProtocol.get();
525     }
526 }