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.russound.internal.rio.system;
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;
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;
49 import com.google.gson.Gson;
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.
55 * @author Tim Roberts - Initial contribution
57 public class RioSystemHandler extends AbstractBridgeHandler<RioSystemProtocol> {
59 private final Logger logger = LoggerFactory.getLogger(RioSystemHandler.class);
62 * The configuration for the system - will be recreated when the configuration changes and will be null when not
65 private RioSystemConfig config;
68 * The lock used to control access to {@link #config}
70 private final ReentrantLock configLock = new ReentrantLock();
73 * The {@link SocketSession} telnet session to the switch. Will be null if not connected.
75 private SocketSession session;
78 * The lock used to control access to {@link #session}
80 private final ReentrantLock sessionLock = new ReentrantLock();
83 * The retry connection event - will only be created when we are retrying the connection attempt
85 private ScheduledFuture<?> retryConnection;
88 * The lock used to control access to {@link #retryConnection}
90 private final ReentrantLock retryConnectionLock = new ReentrantLock();
93 * The ping event - will be non-null when online (null otherwise)
95 private ScheduledFuture<?> ping;
98 * The lock used to control access to {@link #ping}
100 private final ReentrantLock pingLock = new ReentrantLock();
103 * {@link Gson} used for JSON serialization/deserialization
105 private final Gson gson = GsonUtilities.createGson();
108 * Callback listener to use when source name changes - will call {@link #refreshNamedHandler(Gson, Class, String)}
110 * refresh the {@link RioConstants#CHANNEL_SYSSOURCES} channel
112 private final RioHandlerCallbackListener handlerCallbackListener = new RioHandlerCallbackListener() {
114 public void stateUpdate(String channelId, State state) {
115 refreshNamedHandler(gson, RioSourceHandler.class, RioConstants.CHANNEL_SYSSOURCES);
120 * The protocol for favorites handling
122 private final AtomicReference<RioSystemFavoritesProtocol> favoritesProtocol = new AtomicReference<>(null);
125 * The protocol for presets handling
127 private final AtomicReference<RioPresetsProtocol> presetsProtocol = new AtomicReference<>(null);
130 * The discovery service to discover the zones/sources, etc
131 * Will be null if not active.
133 private final AtomicReference<RioSystemDeviceDiscoveryService> discoveryService = new AtomicReference<>(null);
136 * Constructs the handler from the {@link Bridge}
138 * @param bridge a non-null {@link Bridge} the handler is for
140 public RioSystemHandler(Bridge bridge) {
145 * Overrides the base method since we are the source of the {@link SocketSession}.
147 * @return the {@link SocketSession} once initialized. Null if not initialized or disposed of
150 public SocketSession getSocketSession() {
155 sessionLock.unlock();
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
169 public void handleCommand(ChannelUID channelUID, Command command) {
170 if (command instanceof RefreshType) {
171 handleRefresh(channelUID.getId());
175 String id = channelUID.getId();
178 logger.debug("Called with a null channel id - ignoring");
182 if (id.equals(RioConstants.CHANNEL_SYSLANG)) {
183 if (command instanceof StringType stringCommand) {
184 getProtocolHandler().setSystemLanguage(stringCommand.toString());
186 logger.debug("Received a SYSTEM LANGUAGE channel command with a non StringType: {}", command);
188 } else if (id.equals(RioConstants.CHANNEL_SYSALLON)) {
189 if (command instanceof OnOffType) {
190 getProtocolHandler().setSystemAllOn(command == OnOffType.ON);
192 logger.debug("Received a SYSTEM ALL ON channel command with a non OnOffType: {}", command);
195 logger.debug("Unknown/Unsupported Channel id: {}", id);
200 * Method that handles the {@link RefreshType} command specifically. Calls the {@link RioSystemProtocol} to
201 * handle the actual refresh based on the channel id.
203 * @param id a non-null, possibly empty channel id to refresh
205 private void handleRefresh(String id) {
206 if (getThing().getStatus() != ThingStatus.ONLINE) {
210 if (getProtocolHandler() == null) {
214 // Remove the cache'd value to force a refreshed value
215 ((StatefulHandlerCallback) getProtocolHandler().getCallback()).removeState(id);
217 if (id.equals(RioConstants.CHANNEL_SYSLANG)) {
218 getProtocolHandler().refreshSystemLanguage();
220 } else if (id.equals(RioConstants.CHANNEL_SYSSTATUS)) {
221 getProtocolHandler().refreshSystemStatus();
223 } else if (id.equals(RioConstants.CHANNEL_SYSALLON)) {
224 getProtocolHandler().refreshSystemAllOn();
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);
232 // Can't refresh any others...
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()}.
242 public void initialize() {
243 final RioSystemConfig rioConfig = getRioConfig();
245 if (rioConfig == null) {
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");
257 session = new SocketChannelSession(rioConfig.getIpAddress(), RioConstants.RIO_PORT);
259 sessionLock.unlock();
262 // Try initial connection in a scheduled task
263 this.scheduler.schedule(this::connect, 1, TimeUnit.SECONDS);
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()})
272 private void connect() {
273 String response = "Server is offline - will try to reconnect later";
280 final StatefulHandlerCallback callback = new StatefulHandlerCallback(new AbstractRioHandlerCallback() {
282 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
283 updateStatus(status, detail, msg);
284 if (status != ThingStatus.ONLINE) {
291 public void stateChanged(String channelId, State state) {
292 updateState(channelId, state);
293 fireStateUpdated(channelId, state);
297 public void setProperty(String propertyName, String propertyValue) {
298 getThing().setProperty(propertyName, propertyValue);
302 setProtocolHandler(new RioSystemProtocol(session, callback));
303 favoritesProtocol.set(new RioSystemFavoritesProtocol(session, callback));
304 presetsProtocol.set(new RioPresetsProtocol(session, callback));
306 response = getProtocolHandler().login();
307 if (response == null) {
308 final RioSystemConfig rioConfig = getRioConfig();
309 if (rioConfig != null) {
310 ping = this.scheduler.scheduleWithFixedDelay(() -> {
312 final ThingStatus status = getThing().getStatus();
313 if (status == ThingStatus.ONLINE) {
314 if (session.isConnected()) {
315 getProtocolHandler().ping();
318 } catch (Exception e) {
319 logger.error("Exception while pinging: {}", e.getMessage(), e);
321 }, rioConfig.getPing(), rioConfig.getPing(), TimeUnit.SECONDS);
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);
331 logger.debug("getRioConfig returned a null!");
334 logger.warn("Login return {}", response);
337 } catch (Exception e) {
338 logger.error("Error connecting: {}", e.getMessage(), e);
342 sessionLock.unlock();
345 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, response);
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.
354 protected void reconnect() {
355 retryConnectionLock.lock();
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;
364 if (getThing().getStatus() != ThingStatus.ONLINE) {
367 } catch (Exception e) {
368 logger.error("Exception connecting: {}", e.getMessage(), e);
370 }, rioConfig.getRetryPolling(), TimeUnit.SECONDS);
373 logger.debug("RetryConnection called when a retry connection is pending - ignoring request");
376 retryConnectionLock.unlock();
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
387 protected void disconnect() {
399 if (getProtocolHandler() != null) {
400 getProtocolHandler().watchSystem(false);
401 setProtocolHandler(null);
406 session.disconnect();
407 } catch (IOException e) {
408 // ignore - we don't care
410 sessionLock.unlock();
415 * Simple gets the {@link RioSystemConfig} from the {@link Thing} and will set the status to offline if not
418 * @return a possible null {@link RioSystemConfig}
420 public RioSystemConfig getRioConfig() {
423 final RioSystemConfig sysConfig = getThing().getConfiguration().as(RioSystemConfig.class);
425 if (sysConfig == null) {
426 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
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
440 * @param service a possibly null {@link RioSystemDeviceDiscoveryService}
442 public void registerDiscoveryService(RioSystemDeviceDiscoveryService service) {
443 discoveryService.set(service);
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)})
450 * @param sysConfig a non-null {@link RioSystemConfig}
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();
465 * Overrides the base to call {@link #childChanged(ThingHandler)} to recreate the sources/controllers names
468 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
469 childChanged(childHandler, true);
473 * Overrides the base to call {@link #childChanged(ThingHandler)} to recreate the sources/controllers names
476 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
477 childChanged(childHandler, false);
481 * Helper method to recreate the {@link RioConstants#CHANNEL_SYSSOURCES} &&
482 * {@link RioConstants#CHANNEL_SYSCONTROLLERS} channels
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
488 private void childChanged(ThingHandler childHandler, boolean added) {
489 if (childHandler == null) {
490 throw new IllegalArgumentException("childHandler cannot be null");
492 if (childHandler instanceof RioSourceHandler sourceHandler) {
493 final RioHandlerCallback callback = sourceHandler.getRioHandlerCallback();
494 if (callback != null) {
496 callback.addListener(RioConstants.CHANNEL_SOURCENAME, handlerCallbackListener);
498 callback.removeListener(RioConstants.CHANNEL_SOURCENAME, handlerCallbackListener);
501 refreshNamedHandler(gson, RioSourceHandler.class, RioConstants.CHANNEL_SYSSOURCES);
502 } else if (childHandler instanceof RioControllerHandler) {
503 refreshNamedHandler(gson, RioControllerHandler.class, RioConstants.CHANNEL_SYSCONTROLLERS);
508 * Returns the {@link RioSystemFavoritesProtocol} for the system
510 * @return a possibly null {@link RioSystemFavoritesProtocol}
513 public RioSystemFavoritesProtocol getSystemFavoritesHandler() {
514 return favoritesProtocol.get();
518 * Returns the {@link RioPresetsProtocol} for the system
520 * @return a possibly null {@link RioPresetsProtocol}
523 public RioPresetsProtocol getPresetsProtocol() {
524 return presetsProtocol.get();