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.russound.internal.rio.zone;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicInteger;
17 import java.util.concurrent.atomic.AtomicReference;
19 import org.apache.commons.lang.StringUtils;
20 import org.openhab.binding.russound.internal.net.SocketSession;
21 import org.openhab.binding.russound.internal.rio.AbstractBridgeHandler;
22 import org.openhab.binding.russound.internal.rio.AbstractRioHandlerCallback;
23 import org.openhab.binding.russound.internal.rio.AbstractThingHandler;
24 import org.openhab.binding.russound.internal.rio.RioCallbackHandler;
25 import org.openhab.binding.russound.internal.rio.RioConstants;
26 import org.openhab.binding.russound.internal.rio.RioHandlerCallback;
27 import org.openhab.binding.russound.internal.rio.RioNamedHandler;
28 import org.openhab.binding.russound.internal.rio.RioPresetsProtocol;
29 import org.openhab.binding.russound.internal.rio.RioSystemFavoritesProtocol;
30 import org.openhab.binding.russound.internal.rio.StatefulHandlerCallback;
31 import org.openhab.binding.russound.internal.rio.controller.RioControllerHandler;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.IncreaseDecreaseType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
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;
50 * The bridge handler for a Russound Zone. A zone is the main receiving area for music. This implementation must be
51 * attached to a {@link RioControllerHandler} bridge.
53 * @author Tim Roberts - Initial contribution
55 public class RioZoneHandler extends AbstractThingHandler<RioZoneProtocol>
56 implements RioNamedHandler, RioCallbackHandler {
58 private final Logger logger = LoggerFactory.getLogger(RioZoneHandler.class);
61 * The controller identifier we are attached to
63 private final AtomicInteger controller = new AtomicInteger(0);
66 * The zone identifier for this instance
68 private final AtomicInteger zone = new AtomicInteger(0);
71 * The zone name for this instance
73 private final AtomicReference<String> zoneName = new AtomicReference<>(null);
76 * Constructs the handler from the {@link Thing}
78 * @param thing a non-null {@link Thing} the handler is for
80 public RioZoneHandler(Thing thing) {
85 * Returns the controller identifier
87 * @return the controller identifier
89 public int getController() {
90 return controller.get();
94 * Returns the zone identifier
96 * @return the zone identifier
104 * Returns the zone name
106 * @return the zone name
109 public String getName() {
110 final String name = zoneName.get();
111 return StringUtils.isEmpty(name) ? ("Zone " + getId()) : name;
117 * Handles commands to specific channels. This implementation will offload much of its work to the
118 * {@link RioZoneProtocol}. Basically we validate the type of command for the channel then call the
119 * {@link RioZoneProtocol} to handle the actual protocol. Special use case is the {@link RefreshType}
120 * where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
121 * {@link RioZoneProtocol} to handle the actual refresh
124 public void handleCommand(ChannelUID channelUID, Command command) {
125 if (command instanceof RefreshType) {
126 handleRefresh(channelUID.getId());
130 // if (getThing().getStatus() != ThingStatus.ONLINE) {
131 // // Ignore any command if not online
135 String id = channelUID.getId();
138 logger.debug("Called with a null channel id - ignoring");
142 if (id.equals(RioConstants.CHANNEL_ZONEBASS)) {
143 if (command instanceof DecimalType) {
144 getProtocolHandler().setZoneBass(((DecimalType) command).intValue());
146 logger.debug("Received a ZONE BASS channel command with a non DecimalType: {}", command);
149 } else if (id.equals(RioConstants.CHANNEL_ZONETREBLE)) {
150 if (command instanceof DecimalType) {
151 getProtocolHandler().setZoneTreble(((DecimalType) command).intValue());
153 logger.debug("Received a ZONE TREBLE channel command with a non DecimalType: {}", command);
156 } else if (id.equals(RioConstants.CHANNEL_ZONEBALANCE)) {
157 if (command instanceof DecimalType) {
158 getProtocolHandler().setZoneBalance(((DecimalType) command).intValue());
160 logger.debug("Received a ZONE BALANCE channel command with a non DecimalType: {}", command);
163 } else if (id.equals(RioConstants.CHANNEL_ZONETURNONVOLUME)) {
164 if (command instanceof PercentType) {
165 getProtocolHandler().setZoneTurnOnVolume(((PercentType) command).intValue() / 100d);
166 } else if (command instanceof DecimalType) {
167 getProtocolHandler().setZoneTurnOnVolume(((DecimalType) command).doubleValue());
169 logger.debug("Received a ZONE TURN ON VOLUME channel command with a non PercentType/DecimalType: {}",
173 } else if (id.equals(RioConstants.CHANNEL_ZONELOUDNESS)) {
174 if (command instanceof OnOffType) {
175 getProtocolHandler().setZoneLoudness(command == OnOffType.ON);
177 logger.debug("Received a ZONE TURN ON VOLUME channel command with a non OnOffType: {}", command);
180 } else if (id.equals(RioConstants.CHANNEL_ZONESLEEPTIMEREMAINING)) {
181 if (command instanceof DecimalType) {
182 getProtocolHandler().setZoneSleepTimeRemaining(((DecimalType) command).intValue());
184 logger.debug("Received a ZONE SLEEP TIME REMAINING channel command with a non DecimalType: {}",
187 } else if (id.equals(RioConstants.CHANNEL_ZONESOURCE)) {
188 if (command instanceof DecimalType) {
189 getProtocolHandler().setZoneSource(((DecimalType) command).intValue());
191 logger.debug("Received a ZONE SOURCE channel command with a non DecimalType: {}", command);
194 } else if (id.equals(RioConstants.CHANNEL_ZONESTATUS)) {
195 if (command instanceof OnOffType) {
196 getProtocolHandler().setZoneStatus(command == OnOffType.ON);
198 logger.debug("Received a ZONE STATUS channel command with a non OnOffType: {}", command);
200 } else if (id.equals(RioConstants.CHANNEL_ZONEPARTYMODE)) {
201 if (command instanceof StringType) {
202 getProtocolHandler().setZonePartyMode(((StringType) command).toString());
204 logger.debug("Received a ZONE PARTY MODE channel command with a non StringType: {}", command);
207 } else if (id.equals(RioConstants.CHANNEL_ZONEDONOTDISTURB)) {
208 if (command instanceof StringType) {
209 getProtocolHandler().setZoneDoNotDisturb(((StringType) command).toString());
211 logger.debug("Received a ZONE DO NOT DISTURB channel command with a non StringType: {}", command);
214 } else if (id.equals(RioConstants.CHANNEL_ZONEMUTE)) {
215 if (command instanceof OnOffType) {
216 getProtocolHandler().toggleZoneMute();
218 logger.debug("Received a ZONE MUTE channel command with a non OnOffType: {}", command);
221 } else if (id.equals(RioConstants.CHANNEL_ZONEREPEAT)) {
222 if (command instanceof OnOffType) {
223 getProtocolHandler().toggleZoneRepeat();
225 logger.debug("Received a ZONE REPEAT channel command with a non OnOffType: {}", command);
228 } else if (id.equals(RioConstants.CHANNEL_ZONESHUFFLE)) {
229 if (command instanceof OnOffType) {
230 getProtocolHandler().toggleZoneShuffle();
232 logger.debug("Received a ZONE SHUFFLE channel command with a non OnOffType: {}", command);
235 } else if (id.equals(RioConstants.CHANNEL_ZONEVOLUME)) {
236 if (command instanceof OnOffType) {
237 getProtocolHandler().setZoneStatus(command == OnOffType.ON);
238 } else if (command instanceof IncreaseDecreaseType) {
239 getProtocolHandler().setZoneVolume(command == IncreaseDecreaseType.INCREASE);
240 } else if (command instanceof PercentType) {
241 getProtocolHandler().setZoneVolume(((PercentType) command).intValue() / 100d);
242 } else if (command instanceof DecimalType) {
243 getProtocolHandler().setZoneVolume(((DecimalType) command).doubleValue());
246 "Received a ZONE VOLUME channel command with a non OnOffType/IncreaseDecreaseType/PercentType/DecimalTye: {}",
250 } else if (id.equals(RioConstants.CHANNEL_ZONERATING)) {
251 if (command instanceof OnOffType) {
252 getProtocolHandler().setZoneRating(command == OnOffType.ON);
254 logger.debug("Received a ZONE RATING channel command with a non OnOffType: {}", command);
257 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYPRESS)) {
258 if (command instanceof StringType) {
259 getProtocolHandler().sendKeyPress(((StringType) command).toString());
261 logger.debug("Received a ZONE KEYPRESS channel command with a non StringType: {}", command);
264 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYRELEASE)) {
265 if (command instanceof StringType) {
266 getProtocolHandler().sendKeyRelease(((StringType) command).toString());
268 logger.debug("Received a ZONE KEYRELEASE channel command with a non StringType: {}", command);
271 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYHOLD)) {
272 if (command instanceof StringType) {
273 getProtocolHandler().sendKeyHold(((StringType) command).toString());
275 logger.debug("Received a ZONE KEYHOLD channel command with a non StringType: {}", command);
278 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYCODE)) {
279 if (command instanceof StringType) {
280 getProtocolHandler().sendKeyCode(((StringType) command).toString());
282 logger.debug("Received a ZONE KEYCODE channel command with a non StringType: {}", command);
285 } else if (id.equals(RioConstants.CHANNEL_ZONEEVENT)) {
286 if (command instanceof StringType) {
287 getProtocolHandler().sendEvent(((StringType) command).toString());
289 logger.debug("Received a ZONE EVENT channel command with a non StringType: {}", command);
292 } else if (id.equals(RioConstants.CHANNEL_ZONEMMINIT)) {
293 getProtocolHandler().sendMMInit();
295 } else if (id.equals(RioConstants.CHANNEL_ZONEMMCONTEXTMENU)) {
296 getProtocolHandler().sendMMContextMenu();
298 } else if (id.equals(RioConstants.CHANNEL_ZONESYSFAVORITES)) {
299 if (command instanceof StringType) {
300 // Remove any state for this channel to ensure it's recreated/sent again
301 // (clears any bad or deleted favorites information from the channel)
302 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
303 .removeState(RioConstants.CHANNEL_ZONESYSFAVORITES);
305 getProtocolHandler().setSystemFavorites(command.toString());
307 logger.debug("Received a SYSTEM FAVORITES channel command with a non StringType: {}", command);
310 } else if (id.equals(RioConstants.CHANNEL_ZONEFAVORITES)) {
311 if (command instanceof StringType) {
312 // Remove any state for this channel to ensure it's recreated/sent again
313 // (clears any bad or deleted favorites information from the channel)
314 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
315 .removeState(RioConstants.CHANNEL_ZONEFAVORITES);
317 // schedule the returned callback in the future (to allow the channel to process and to allow russound
318 // to process (before re-retrieving information)
319 scheduler.schedule(getProtocolHandler().setZoneFavorites(command.toString()), 250,
320 TimeUnit.MILLISECONDS);
323 logger.debug("Received a ZONE FAVORITES channel command with a non StringType: {}", command);
325 } else if (id.equals(RioConstants.CHANNEL_ZONEPRESETS)) {
326 if (command instanceof StringType) {
327 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
328 .removeState(RioConstants.CHANNEL_ZONEPRESETS);
330 getProtocolHandler().setZonePresets(command.toString());
332 logger.debug("Received a ZONE FAVORITES channel command with a non StringType: {}", command);
335 logger.debug("Unknown/Unsupported Channel id: {}", id);
340 * Method that handles the {@link RefreshType} command specifically. Calls the {@link RioZoneProtocol} to
341 * handle the actual refresh based on the channel id.
343 * @param id a non-null, possibly empty channel id to refresh
345 private void handleRefresh(String id) {
346 if (getThing().getStatus() != ThingStatus.ONLINE) {
350 if (getProtocolHandler() == null) {
354 // Remove the cache'd value to force a refreshed value
355 ((StatefulHandlerCallback) getProtocolHandler().getCallback()).removeState(id);
357 if (id.equals(RioConstants.CHANNEL_ZONENAME)) {
358 getProtocolHandler().refreshZoneName();
359 } else if (id.startsWith(RioConstants.CHANNEL_ZONESOURCE)) {
360 getProtocolHandler().refreshZoneSource();
361 } else if (id.startsWith(RioConstants.CHANNEL_ZONEBASS)) {
362 getProtocolHandler().refreshZoneBass();
363 } else if (id.startsWith(RioConstants.CHANNEL_ZONETREBLE)) {
364 getProtocolHandler().refreshZoneTreble();
365 } else if (id.startsWith(RioConstants.CHANNEL_ZONEBALANCE)) {
366 getProtocolHandler().refreshZoneBalance();
367 } else if (id.startsWith(RioConstants.CHANNEL_ZONELOUDNESS)) {
368 getProtocolHandler().refreshZoneLoudness();
369 } else if (id.startsWith(RioConstants.CHANNEL_ZONETURNONVOLUME)) {
370 getProtocolHandler().refreshZoneTurnOnVolume();
371 } else if (id.startsWith(RioConstants.CHANNEL_ZONEDONOTDISTURB)) {
372 getProtocolHandler().refreshZoneDoNotDisturb();
373 } else if (id.startsWith(RioConstants.CHANNEL_ZONEPARTYMODE)) {
374 getProtocolHandler().refreshZonePartyMode();
375 } else if (id.startsWith(RioConstants.CHANNEL_ZONESTATUS)) {
376 getProtocolHandler().refreshZoneStatus();
377 } else if (id.startsWith(RioConstants.CHANNEL_ZONEVOLUME)) {
378 getProtocolHandler().refreshZoneVolume();
379 } else if (id.startsWith(RioConstants.CHANNEL_ZONEMUTE)) {
380 getProtocolHandler().refreshZoneMute();
381 } else if (id.startsWith(RioConstants.CHANNEL_ZONEPAGE)) {
382 getProtocolHandler().refreshZonePage();
383 } else if (id.startsWith(RioConstants.CHANNEL_ZONESHAREDSOURCE)) {
384 getProtocolHandler().refreshZoneSharedSource();
385 } else if (id.startsWith(RioConstants.CHANNEL_ZONESLEEPTIMEREMAINING)) {
386 getProtocolHandler().refreshZoneSleepTimeRemaining();
387 } else if (id.startsWith(RioConstants.CHANNEL_ZONELASTERROR)) {
388 getProtocolHandler().refreshZoneLastError();
389 } else if (id.equals(RioConstants.CHANNEL_ZONESYSFAVORITES)) {
390 getProtocolHandler().refreshSystemFavorites();
391 } else if (id.equals(RioConstants.CHANNEL_ZONEFAVORITES)) {
392 getProtocolHandler().refreshZoneFavorites();
393 } else if (id.equals(RioConstants.CHANNEL_ZONEPRESETS)) {
394 getProtocolHandler().refreshZonePresets();
396 // Can't refresh any others...
400 * Initializes the bridge. Confirms the configuration is valid and that our parent bridge is a
401 * {@link RioControllerHandler}. Once validated, a {@link RioZoneProtocol} is set via
402 * {@link #setProtocolHandler(RioZoneProtocol)} and the bridge comes online.
405 public void initialize() {
406 final Bridge bridge = getBridge();
407 if (bridge == null) {
408 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
409 "Cannot be initialized without a bridge");
412 if (bridge.getStatus() != ThingStatus.ONLINE) {
413 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
417 final ThingHandler handler = bridge.getHandler();
418 if (handler == null) {
419 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
420 "No handler specified (null) for the bridge!");
424 if (!(handler instanceof RioControllerHandler)) {
425 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
426 "Source must be attached to a controller bridge: " + handler.getClass());
430 final RioZoneConfig config = getThing().getConfiguration().as(RioZoneConfig.class);
431 if (config == null) {
432 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
436 final int configZone = config.getZone();
437 if (configZone < 1 || configZone > 8) {
438 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
439 "Source must be between 1 and 8: " + configZone);
442 zone.set(configZone);
444 final int handlerController = ((RioControllerHandler) handler).getId();
445 controller.set(handlerController);
447 // Get the socket session from the
448 final SocketSession socketSession = getSocketSession();
449 if (socketSession == null) {
450 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, "No socket session found");
454 setProtocolHandler(new RioZoneProtocol(configZone, handlerController, getSystemFavoritesHandler(),
455 getPresetsProtocol(), socketSession, new StatefulHandlerCallback(new AbstractRioHandlerCallback() {
457 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
458 updateStatus(status, detail, msg);
462 public void stateChanged(String channelId, State state) {
463 if (channelId.equals(RioConstants.CHANNEL_ZONENAME)) {
464 zoneName.set(state.toString());
466 updateState(channelId, state);
467 fireStateUpdated(channelId, state);
471 public void setProperty(String propertyName, String propertyValue) {
472 getThing().setProperty(propertyName, propertyValue);
476 updateStatus(ThingStatus.ONLINE);
477 getProtocolHandler().postOnline();
481 * Returns the {@link RioHandlerCallback} related to the zone
483 * @return a possibly null {@link RioHandlerCallback}
486 public RioHandlerCallback getRioHandlerCallback() {
487 final RioZoneProtocol protocolHandler = getProtocolHandler();
488 return protocolHandler == null ? null : protocolHandler.getCallback();
492 * Returns the {@link RioPresetsProtocol} related to the system. This simply queries the parent bridge for the
495 * @return a possibly null {@link RioPresetsProtocol}
497 @SuppressWarnings("rawtypes")
498 RioPresetsProtocol getPresetsProtocol() {
499 final Bridge bridge = getBridge();
500 if (bridge != null && bridge.getHandler() instanceof AbstractBridgeHandler) {
501 return ((AbstractBridgeHandler) bridge.getHandler()).getPresetsProtocol();
507 * Returns the {@link RioSystemFavoritesProtocol} related to the system. This simply queries the parent bridge for
510 * @return a possibly null {@link RioSystemFavoritesProtocol}
512 @SuppressWarnings("rawtypes")
513 RioSystemFavoritesProtocol getSystemFavoritesHandler() {
514 final Bridge bridge = getBridge();
515 if (bridge != null && bridge.getHandler() instanceof AbstractBridgeHandler) {
516 return ((AbstractBridgeHandler) bridge.getHandler()).getSystemFavoritesHandler();