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.zone;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicInteger;
17 import java.util.concurrent.atomic.AtomicReference;
19 import org.openhab.binding.russound.internal.net.SocketSession;
20 import org.openhab.binding.russound.internal.rio.AbstractBridgeHandler;
21 import org.openhab.binding.russound.internal.rio.AbstractRioHandlerCallback;
22 import org.openhab.binding.russound.internal.rio.AbstractThingHandler;
23 import org.openhab.binding.russound.internal.rio.RioCallbackHandler;
24 import org.openhab.binding.russound.internal.rio.RioConstants;
25 import org.openhab.binding.russound.internal.rio.RioHandlerCallback;
26 import org.openhab.binding.russound.internal.rio.RioNamedHandler;
27 import org.openhab.binding.russound.internal.rio.RioPresetsProtocol;
28 import org.openhab.binding.russound.internal.rio.RioSystemFavoritesProtocol;
29 import org.openhab.binding.russound.internal.rio.StatefulHandlerCallback;
30 import org.openhab.binding.russound.internal.rio.controller.RioControllerHandler;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.IncreaseDecreaseType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.ThingHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The bridge handler for a Russound Zone. A zone is the main receiving area for music. This implementation must be
50 * attached to a {@link RioControllerHandler} bridge.
52 * @author Tim Roberts - Initial contribution
54 public class RioZoneHandler extends AbstractThingHandler<RioZoneProtocol>
55 implements RioNamedHandler, RioCallbackHandler {
57 private final Logger logger = LoggerFactory.getLogger(RioZoneHandler.class);
60 * The controller identifier we are attached to
62 private final AtomicInteger controller = new AtomicInteger(0);
65 * The zone identifier for this instance
67 private final AtomicInteger zone = new AtomicInteger(0);
70 * The zone name for this instance
72 private final AtomicReference<String> zoneName = new AtomicReference<>(null);
75 * Constructs the handler from the {@link Thing}
77 * @param thing a non-null {@link Thing} the handler is for
79 public RioZoneHandler(Thing thing) {
84 * Returns the controller identifier
86 * @return the controller identifier
88 public int getController() {
89 return controller.get();
93 * Returns the zone identifier
95 * @return the zone identifier
103 * Returns the zone name
105 * @return the zone name
108 public String getName() {
109 final String name = zoneName.get();
110 return name == null || name.isEmpty() ? "Zone " + getId() : name;
116 * Handles commands to specific channels. This implementation will offload much of its work to the
117 * {@link RioZoneProtocol}. Basically we validate the type of command for the channel then call the
118 * {@link RioZoneProtocol} to handle the actual protocol. Special use case is the {@link RefreshType}
119 * where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
120 * {@link RioZoneProtocol} to handle the actual refresh
123 public void handleCommand(ChannelUID channelUID, Command command) {
124 if (command instanceof RefreshType) {
125 handleRefresh(channelUID.getId());
129 // if (getThing().getStatus() != ThingStatus.ONLINE) {
130 // // Ignore any command if not online
134 String id = channelUID.getId();
137 logger.debug("Called with a null channel id - ignoring");
141 if (id.equals(RioConstants.CHANNEL_ZONEBASS)) {
142 if (command instanceof DecimalType decimalCommand) {
143 getProtocolHandler().setZoneBass(decimalCommand.intValue());
145 logger.debug("Received a ZONE BASS channel command with a non DecimalType: {}", command);
148 } else if (id.equals(RioConstants.CHANNEL_ZONETREBLE)) {
149 if (command instanceof DecimalType decimalCommand) {
150 getProtocolHandler().setZoneTreble(decimalCommand.intValue());
152 logger.debug("Received a ZONE TREBLE channel command with a non DecimalType: {}", command);
155 } else if (id.equals(RioConstants.CHANNEL_ZONEBALANCE)) {
156 if (command instanceof DecimalType decimalCommand) {
157 getProtocolHandler().setZoneBalance(decimalCommand.intValue());
159 logger.debug("Received a ZONE BALANCE channel command with a non DecimalType: {}", command);
162 } else if (id.equals(RioConstants.CHANNEL_ZONETURNONVOLUME)) {
163 if (command instanceof PercentType percentCommand) {
164 getProtocolHandler().setZoneTurnOnVolume(percentCommand.intValue() / 100d);
165 } else if (command instanceof DecimalType decimalCommand) {
166 getProtocolHandler().setZoneTurnOnVolume(decimalCommand.doubleValue());
168 logger.debug("Received a ZONE TURN ON VOLUME channel command with a non PercentType/DecimalType: {}",
172 } else if (id.equals(RioConstants.CHANNEL_ZONELOUDNESS)) {
173 if (command instanceof OnOffType) {
174 getProtocolHandler().setZoneLoudness(command == OnOffType.ON);
176 logger.debug("Received a ZONE TURN ON VOLUME channel command with a non OnOffType: {}", command);
179 } else if (id.equals(RioConstants.CHANNEL_ZONESLEEPTIMEREMAINING)) {
180 if (command instanceof DecimalType decimalCommand) {
181 getProtocolHandler().setZoneSleepTimeRemaining(decimalCommand.intValue());
183 logger.debug("Received a ZONE SLEEP TIME REMAINING channel command with a non DecimalType: {}",
186 } else if (id.equals(RioConstants.CHANNEL_ZONESOURCE)) {
187 if (command instanceof DecimalType decimalCommand) {
188 getProtocolHandler().setZoneSource(decimalCommand.intValue());
190 logger.debug("Received a ZONE SOURCE channel command with a non DecimalType: {}", command);
193 } else if (id.equals(RioConstants.CHANNEL_ZONESTATUS)) {
194 if (command instanceof OnOffType) {
195 getProtocolHandler().setZoneStatus(command == OnOffType.ON);
197 logger.debug("Received a ZONE STATUS channel command with a non OnOffType: {}", command);
199 } else if (id.equals(RioConstants.CHANNEL_ZONEPARTYMODE)) {
200 if (command instanceof StringType stringCommand) {
201 getProtocolHandler().setZonePartyMode(stringCommand.toString());
203 logger.debug("Received a ZONE PARTY MODE channel command with a non StringType: {}", command);
206 } else if (id.equals(RioConstants.CHANNEL_ZONEDONOTDISTURB)) {
207 if (command instanceof StringType stringCommand) {
208 getProtocolHandler().setZoneDoNotDisturb(stringCommand.toString());
210 logger.debug("Received a ZONE DO NOT DISTURB channel command with a non StringType: {}", command);
213 } else if (id.equals(RioConstants.CHANNEL_ZONEMUTE)) {
214 if (command instanceof OnOffType) {
215 getProtocolHandler().toggleZoneMute();
217 logger.debug("Received a ZONE MUTE channel command with a non OnOffType: {}", command);
220 } else if (id.equals(RioConstants.CHANNEL_ZONEREPEAT)) {
221 if (command instanceof OnOffType) {
222 getProtocolHandler().toggleZoneRepeat();
224 logger.debug("Received a ZONE REPEAT channel command with a non OnOffType: {}", command);
227 } else if (id.equals(RioConstants.CHANNEL_ZONESHUFFLE)) {
228 if (command instanceof OnOffType) {
229 getProtocolHandler().toggleZoneShuffle();
231 logger.debug("Received a ZONE SHUFFLE channel command with a non OnOffType: {}", command);
234 } else if (id.equals(RioConstants.CHANNEL_ZONEVOLUME)) {
235 if (command instanceof OnOffType) {
236 getProtocolHandler().setZoneStatus(command == OnOffType.ON);
237 } else if (command instanceof IncreaseDecreaseType) {
238 getProtocolHandler().setZoneVolume(command == IncreaseDecreaseType.INCREASE);
239 } else if (command instanceof PercentType percentCommand) {
240 getProtocolHandler().setZoneVolume(percentCommand.intValue() / 100d);
241 } else if (command instanceof DecimalType decimalCommand) {
242 getProtocolHandler().setZoneVolume(decimalCommand.doubleValue());
245 "Received a ZONE VOLUME channel command with a non OnOffType/IncreaseDecreaseType/PercentType/DecimalTye: {}",
249 } else if (id.equals(RioConstants.CHANNEL_ZONERATING)) {
250 if (command instanceof OnOffType) {
251 getProtocolHandler().setZoneRating(command == OnOffType.ON);
253 logger.debug("Received a ZONE RATING channel command with a non OnOffType: {}", command);
256 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYPRESS)) {
257 if (command instanceof StringType stringCommand) {
258 getProtocolHandler().sendKeyPress(stringCommand.toString());
260 logger.debug("Received a ZONE KEYPRESS channel command with a non StringType: {}", command);
263 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYRELEASE)) {
264 if (command instanceof StringType stringCommand) {
265 getProtocolHandler().sendKeyRelease(stringCommand.toString());
267 logger.debug("Received a ZONE KEYRELEASE channel command with a non StringType: {}", command);
270 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYHOLD)) {
271 if (command instanceof StringType stringCommand) {
272 getProtocolHandler().sendKeyHold(stringCommand.toString());
274 logger.debug("Received a ZONE KEYHOLD channel command with a non StringType: {}", command);
277 } else if (id.equals(RioConstants.CHANNEL_ZONEKEYCODE)) {
278 if (command instanceof StringType stringCommand) {
279 getProtocolHandler().sendKeyCode(stringCommand.toString());
281 logger.debug("Received a ZONE KEYCODE channel command with a non StringType: {}", command);
284 } else if (id.equals(RioConstants.CHANNEL_ZONEEVENT)) {
285 if (command instanceof StringType stringCommand) {
286 getProtocolHandler().sendEvent(stringCommand.toString());
288 logger.debug("Received a ZONE EVENT channel command with a non StringType: {}", command);
291 } else if (id.equals(RioConstants.CHANNEL_ZONEMMINIT)) {
292 getProtocolHandler().sendMMInit();
294 } else if (id.equals(RioConstants.CHANNEL_ZONEMMCONTEXTMENU)) {
295 getProtocolHandler().sendMMContextMenu();
297 } else if (id.equals(RioConstants.CHANNEL_ZONESYSFAVORITES)) {
298 if (command instanceof StringType) {
299 // Remove any state for this channel to ensure it's recreated/sent again
300 // (clears any bad or deleted favorites information from the channel)
301 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
302 .removeState(RioConstants.CHANNEL_ZONESYSFAVORITES);
304 getProtocolHandler().setSystemFavorites(command.toString());
306 logger.debug("Received a SYSTEM FAVORITES channel command with a non StringType: {}", command);
309 } else if (id.equals(RioConstants.CHANNEL_ZONEFAVORITES)) {
310 if (command instanceof StringType) {
311 // Remove any state for this channel to ensure it's recreated/sent again
312 // (clears any bad or deleted favorites information from the channel)
313 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
314 .removeState(RioConstants.CHANNEL_ZONEFAVORITES);
316 // schedule the returned callback in the future (to allow the channel to process and to allow russound
317 // to process (before re-retrieving information)
318 scheduler.schedule(getProtocolHandler().setZoneFavorites(command.toString()), 250,
319 TimeUnit.MILLISECONDS);
322 logger.debug("Received a ZONE FAVORITES channel command with a non StringType: {}", command);
324 } else if (id.equals(RioConstants.CHANNEL_ZONEPRESETS)) {
325 if (command instanceof StringType) {
326 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
327 .removeState(RioConstants.CHANNEL_ZONEPRESETS);
329 getProtocolHandler().setZonePresets(command.toString());
331 logger.debug("Received a ZONE FAVORITES channel command with a non StringType: {}", command);
334 logger.debug("Unknown/Unsupported Channel id: {}", id);
339 * Method that handles the {@link RefreshType} command specifically. Calls the {@link RioZoneProtocol} to
340 * handle the actual refresh based on the channel id.
342 * @param id a non-null, possibly empty channel id to refresh
344 private void handleRefresh(String id) {
345 if (getThing().getStatus() != ThingStatus.ONLINE) {
349 if (getProtocolHandler() == null) {
353 // Remove the cache'd value to force a refreshed value
354 ((StatefulHandlerCallback) getProtocolHandler().getCallback()).removeState(id);
356 if (id.equals(RioConstants.CHANNEL_ZONENAME)) {
357 getProtocolHandler().refreshZoneName();
358 } else if (id.startsWith(RioConstants.CHANNEL_ZONESOURCE)) {
359 getProtocolHandler().refreshZoneSource();
360 } else if (id.startsWith(RioConstants.CHANNEL_ZONEBASS)) {
361 getProtocolHandler().refreshZoneBass();
362 } else if (id.startsWith(RioConstants.CHANNEL_ZONETREBLE)) {
363 getProtocolHandler().refreshZoneTreble();
364 } else if (id.startsWith(RioConstants.CHANNEL_ZONEBALANCE)) {
365 getProtocolHandler().refreshZoneBalance();
366 } else if (id.startsWith(RioConstants.CHANNEL_ZONELOUDNESS)) {
367 getProtocolHandler().refreshZoneLoudness();
368 } else if (id.startsWith(RioConstants.CHANNEL_ZONETURNONVOLUME)) {
369 getProtocolHandler().refreshZoneTurnOnVolume();
370 } else if (id.startsWith(RioConstants.CHANNEL_ZONEDONOTDISTURB)) {
371 getProtocolHandler().refreshZoneDoNotDisturb();
372 } else if (id.startsWith(RioConstants.CHANNEL_ZONEPARTYMODE)) {
373 getProtocolHandler().refreshZonePartyMode();
374 } else if (id.startsWith(RioConstants.CHANNEL_ZONESTATUS)) {
375 getProtocolHandler().refreshZoneStatus();
376 } else if (id.startsWith(RioConstants.CHANNEL_ZONEVOLUME)) {
377 getProtocolHandler().refreshZoneVolume();
378 } else if (id.startsWith(RioConstants.CHANNEL_ZONEMUTE)) {
379 getProtocolHandler().refreshZoneMute();
380 } else if (id.startsWith(RioConstants.CHANNEL_ZONEPAGE)) {
381 getProtocolHandler().refreshZonePage();
382 } else if (id.startsWith(RioConstants.CHANNEL_ZONESHAREDSOURCE)) {
383 getProtocolHandler().refreshZoneSharedSource();
384 } else if (id.startsWith(RioConstants.CHANNEL_ZONESLEEPTIMEREMAINING)) {
385 getProtocolHandler().refreshZoneSleepTimeRemaining();
386 } else if (id.startsWith(RioConstants.CHANNEL_ZONELASTERROR)) {
387 getProtocolHandler().refreshZoneLastError();
388 } else if (id.equals(RioConstants.CHANNEL_ZONESYSFAVORITES)) {
389 getProtocolHandler().refreshSystemFavorites();
390 } else if (id.equals(RioConstants.CHANNEL_ZONEFAVORITES)) {
391 getProtocolHandler().refreshZoneFavorites();
392 } else if (id.equals(RioConstants.CHANNEL_ZONEPRESETS)) {
393 getProtocolHandler().refreshZonePresets();
395 // Can't refresh any others...
399 * Initializes the bridge. Confirms the configuration is valid and that our parent bridge is a
400 * {@link RioControllerHandler}. Once validated, a {@link RioZoneProtocol} is set via
401 * {@link #setProtocolHandler(AbstractRioProtocol)} and the bridge comes online.
404 public void initialize() {
405 final Bridge bridge = getBridge();
406 if (bridge == null) {
407 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
408 "Cannot be initialized without a bridge");
411 if (bridge.getStatus() != ThingStatus.ONLINE) {
412 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
416 final ThingHandler handler = bridge.getHandler();
417 if (handler == null) {
418 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
419 "No handler specified (null) for the bridge!");
423 if (!(handler instanceof RioControllerHandler)) {
424 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
425 "Source must be attached to a controller bridge: " + handler.getClass());
429 final RioZoneConfig config = getThing().getConfiguration().as(RioZoneConfig.class);
430 if (config == null) {
431 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
435 final int configZone = config.getZone();
436 if (configZone < 1 || configZone > 8) {
437 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
438 "Source must be between 1 and 8: " + configZone);
441 zone.set(configZone);
443 final int handlerController = ((RioControllerHandler) handler).getId();
444 controller.set(handlerController);
446 // Get the socket session from the
447 final SocketSession socketSession = getSocketSession();
448 if (socketSession == null) {
449 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, "No socket session found");
453 setProtocolHandler(new RioZoneProtocol(configZone, handlerController, getSystemFavoritesHandler(),
454 getPresetsProtocol(), socketSession, new StatefulHandlerCallback(new AbstractRioHandlerCallback() {
456 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
457 updateStatus(status, detail, msg);
461 public void stateChanged(String channelId, State state) {
462 if (channelId.equals(RioConstants.CHANNEL_ZONENAME)) {
463 zoneName.set(state.toString());
465 updateState(channelId, state);
466 fireStateUpdated(channelId, state);
470 public void setProperty(String propertyName, String propertyValue) {
471 getThing().setProperty(propertyName, propertyValue);
475 updateStatus(ThingStatus.ONLINE);
476 getProtocolHandler().postOnline();
480 * Returns the {@link RioHandlerCallback} related to the zone
482 * @return a possibly null {@link RioHandlerCallback}
485 public RioHandlerCallback getRioHandlerCallback() {
486 final RioZoneProtocol protocolHandler = getProtocolHandler();
487 return protocolHandler == null ? null : protocolHandler.getCallback();
491 * Returns the {@link RioPresetsProtocol} related to the system. This simply queries the parent bridge for the
494 * @return a possibly null {@link RioPresetsProtocol}
496 @SuppressWarnings("rawtypes")
497 RioPresetsProtocol getPresetsProtocol() {
498 final Bridge bridge = getBridge();
499 if (bridge != null && bridge.getHandler() instanceof AbstractBridgeHandler) {
500 return ((AbstractBridgeHandler) bridge.getHandler()).getPresetsProtocol();
506 * Returns the {@link RioSystemFavoritesProtocol} related to the system. This simply queries the parent bridge for
509 * @return a possibly null {@link RioSystemFavoritesProtocol}
511 @SuppressWarnings("rawtypes")
512 RioSystemFavoritesProtocol getSystemFavoritesHandler() {
513 final Bridge bridge = getBridge();
514 if (bridge != null && bridge.getHandler() instanceof AbstractBridgeHandler) {
515 return ((AbstractBridgeHandler) bridge.getHandler()).getSystemFavoritesHandler();