]> git.basschouten.com Git - openhab-addons.git/blob
2897aa4ad96d965ce2bb4cb81f8d8ae269c2d105
[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.zone;
14
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.atomic.AtomicInteger;
17 import java.util.concurrent.atomic.AtomicReference;
18
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;
47
48 /**
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.
51  *
52  * @author Tim Roberts - Initial contribution
53  */
54 public class RioZoneHandler extends AbstractThingHandler<RioZoneProtocol>
55         implements RioNamedHandler, RioCallbackHandler {
56     // Logger
57     private final Logger logger = LoggerFactory.getLogger(RioZoneHandler.class);
58
59     /**
60      * The controller identifier we are attached to
61      */
62     private final AtomicInteger controller = new AtomicInteger(0);
63
64     /**
65      * The zone identifier for this instance
66      */
67     private final AtomicInteger zone = new AtomicInteger(0);
68
69     /**
70      * The zone name for this instance
71      */
72     private final AtomicReference<String> zoneName = new AtomicReference<>(null);
73
74     /**
75      * Constructs the handler from the {@link Thing}
76      *
77      * @param thing a non-null {@link Thing} the handler is for
78      */
79     public RioZoneHandler(Thing thing) {
80         super(thing);
81     }
82
83     /**
84      * Returns the controller identifier
85      *
86      * @return the controller identifier
87      */
88     public int getController() {
89         return controller.get();
90     }
91
92     /**
93      * Returns the zone identifier
94      *
95      * @return the zone identifier
96      */
97     @Override
98     public int getId() {
99         return zone.get();
100     }
101
102     /**
103      * Returns the zone name
104      *
105      * @return the zone name
106      */
107     @Override
108     public String getName() {
109         final String name = zoneName.get();
110         return name == null || name.isEmpty() ? "Zone " + getId() : name;
111     }
112
113     /**
114      * {@inheritDoc}
115      *
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
121      */
122     @Override
123     public void handleCommand(ChannelUID channelUID, Command command) {
124         if (command instanceof RefreshType) {
125             handleRefresh(channelUID.getId());
126             return;
127         }
128
129         // if (getThing().getStatus() != ThingStatus.ONLINE) {
130         // // Ignore any command if not online
131         // return;
132         // }
133
134         String id = channelUID.getId();
135
136         if (id == null) {
137             logger.debug("Called with a null channel id - ignoring");
138             return;
139         }
140
141         if (id.equals(RioConstants.CHANNEL_ZONEBASS)) {
142             if (command instanceof DecimalType decimalCommand) {
143                 getProtocolHandler().setZoneBass(decimalCommand.intValue());
144             } else {
145                 logger.debug("Received a ZONE BASS channel command with a non DecimalType: {}", command);
146             }
147
148         } else if (id.equals(RioConstants.CHANNEL_ZONETREBLE)) {
149             if (command instanceof DecimalType decimalCommand) {
150                 getProtocolHandler().setZoneTreble(decimalCommand.intValue());
151             } else {
152                 logger.debug("Received a ZONE TREBLE channel command with a non DecimalType: {}", command);
153             }
154
155         } else if (id.equals(RioConstants.CHANNEL_ZONEBALANCE)) {
156             if (command instanceof DecimalType decimalCommand) {
157                 getProtocolHandler().setZoneBalance(decimalCommand.intValue());
158             } else {
159                 logger.debug("Received a ZONE BALANCE channel command with a non DecimalType: {}", command);
160             }
161
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());
167             } else {
168                 logger.debug("Received a ZONE TURN ON VOLUME channel command with a non PercentType/DecimalType: {}",
169                         command);
170             }
171
172         } else if (id.equals(RioConstants.CHANNEL_ZONELOUDNESS)) {
173             if (command instanceof OnOffType) {
174                 getProtocolHandler().setZoneLoudness(command == OnOffType.ON);
175             } else {
176                 logger.debug("Received a ZONE TURN ON VOLUME channel command with a non OnOffType: {}", command);
177             }
178
179         } else if (id.equals(RioConstants.CHANNEL_ZONESLEEPTIMEREMAINING)) {
180             if (command instanceof DecimalType decimalCommand) {
181                 getProtocolHandler().setZoneSleepTimeRemaining(decimalCommand.intValue());
182             } else {
183                 logger.debug("Received a ZONE SLEEP TIME REMAINING channel command with a non DecimalType: {}",
184                         command);
185             }
186         } else if (id.equals(RioConstants.CHANNEL_ZONESOURCE)) {
187             if (command instanceof DecimalType decimalCommand) {
188                 getProtocolHandler().setZoneSource(decimalCommand.intValue());
189             } else {
190                 logger.debug("Received a ZONE SOURCE channel command with a non DecimalType: {}", command);
191             }
192
193         } else if (id.equals(RioConstants.CHANNEL_ZONESTATUS)) {
194             if (command instanceof OnOffType) {
195                 getProtocolHandler().setZoneStatus(command == OnOffType.ON);
196             } else {
197                 logger.debug("Received a ZONE STATUS channel command with a non OnOffType: {}", command);
198             }
199         } else if (id.equals(RioConstants.CHANNEL_ZONEPARTYMODE)) {
200             if (command instanceof StringType stringCommand) {
201                 getProtocolHandler().setZonePartyMode(stringCommand.toString());
202             } else {
203                 logger.debug("Received a ZONE PARTY MODE channel command with a non StringType: {}", command);
204             }
205
206         } else if (id.equals(RioConstants.CHANNEL_ZONEDONOTDISTURB)) {
207             if (command instanceof StringType stringCommand) {
208                 getProtocolHandler().setZoneDoNotDisturb(stringCommand.toString());
209             } else {
210                 logger.debug("Received a ZONE DO NOT DISTURB channel command with a non StringType: {}", command);
211             }
212
213         } else if (id.equals(RioConstants.CHANNEL_ZONEMUTE)) {
214             if (command instanceof OnOffType) {
215                 getProtocolHandler().toggleZoneMute();
216             } else {
217                 logger.debug("Received a ZONE MUTE channel command with a non OnOffType: {}", command);
218             }
219
220         } else if (id.equals(RioConstants.CHANNEL_ZONEREPEAT)) {
221             if (command instanceof OnOffType) {
222                 getProtocolHandler().toggleZoneRepeat();
223             } else {
224                 logger.debug("Received a ZONE REPEAT channel command with a non OnOffType: {}", command);
225             }
226
227         } else if (id.equals(RioConstants.CHANNEL_ZONESHUFFLE)) {
228             if (command instanceof OnOffType) {
229                 getProtocolHandler().toggleZoneShuffle();
230             } else {
231                 logger.debug("Received a ZONE SHUFFLE channel command with a non OnOffType: {}", command);
232             }
233
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());
243             } else {
244                 logger.debug(
245                         "Received a ZONE VOLUME channel command with a non OnOffType/IncreaseDecreaseType/PercentType/DecimalTye: {}",
246                         command);
247             }
248
249         } else if (id.equals(RioConstants.CHANNEL_ZONERATING)) {
250             if (command instanceof OnOffType) {
251                 getProtocolHandler().setZoneRating(command == OnOffType.ON);
252             } else {
253                 logger.debug("Received a ZONE RATING channel command with a non OnOffType: {}", command);
254             }
255
256         } else if (id.equals(RioConstants.CHANNEL_ZONEKEYPRESS)) {
257             if (command instanceof StringType stringCommand) {
258                 getProtocolHandler().sendKeyPress(stringCommand.toString());
259             } else {
260                 logger.debug("Received a ZONE KEYPRESS channel command with a non StringType: {}", command);
261             }
262
263         } else if (id.equals(RioConstants.CHANNEL_ZONEKEYRELEASE)) {
264             if (command instanceof StringType stringCommand) {
265                 getProtocolHandler().sendKeyRelease(stringCommand.toString());
266             } else {
267                 logger.debug("Received a ZONE KEYRELEASE channel command with a non StringType: {}", command);
268             }
269
270         } else if (id.equals(RioConstants.CHANNEL_ZONEKEYHOLD)) {
271             if (command instanceof StringType stringCommand) {
272                 getProtocolHandler().sendKeyHold(stringCommand.toString());
273             } else {
274                 logger.debug("Received a ZONE KEYHOLD channel command with a non StringType: {}", command);
275             }
276
277         } else if (id.equals(RioConstants.CHANNEL_ZONEKEYCODE)) {
278             if (command instanceof StringType stringCommand) {
279                 getProtocolHandler().sendKeyCode(stringCommand.toString());
280             } else {
281                 logger.debug("Received a ZONE KEYCODE channel command with a non StringType: {}", command);
282             }
283
284         } else if (id.equals(RioConstants.CHANNEL_ZONEEVENT)) {
285             if (command instanceof StringType stringCommand) {
286                 getProtocolHandler().sendEvent(stringCommand.toString());
287             } else {
288                 logger.debug("Received a ZONE EVENT channel command with a non StringType: {}", command);
289             }
290
291         } else if (id.equals(RioConstants.CHANNEL_ZONEMMINIT)) {
292             getProtocolHandler().sendMMInit();
293
294         } else if (id.equals(RioConstants.CHANNEL_ZONEMMCONTEXTMENU)) {
295             getProtocolHandler().sendMMContextMenu();
296
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);
303
304                 getProtocolHandler().setSystemFavorites(command.toString());
305             } else {
306                 logger.debug("Received a SYSTEM FAVORITES channel command with a non StringType: {}", command);
307             }
308
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);
315
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);
320
321             } else {
322                 logger.debug("Received a ZONE FAVORITES channel command with a non StringType: {}", command);
323             }
324         } else if (id.equals(RioConstants.CHANNEL_ZONEPRESETS)) {
325             if (command instanceof StringType) {
326                 ((StatefulHandlerCallback) getProtocolHandler().getCallback())
327                         .removeState(RioConstants.CHANNEL_ZONEPRESETS);
328
329                 getProtocolHandler().setZonePresets(command.toString());
330             } else {
331                 logger.debug("Received a ZONE FAVORITES channel command with a non StringType: {}", command);
332             }
333         } else {
334             logger.debug("Unknown/Unsupported Channel id: {}", id);
335         }
336     }
337
338     /**
339      * Method that handles the {@link RefreshType} command specifically. Calls the {@link RioZoneProtocol} to
340      * handle the actual refresh based on the channel id.
341      *
342      * @param id a non-null, possibly empty channel id to refresh
343      */
344     private void handleRefresh(String id) {
345         if (getThing().getStatus() != ThingStatus.ONLINE) {
346             return;
347         }
348
349         if (getProtocolHandler() == null) {
350             return;
351         }
352
353         // Remove the cache'd value to force a refreshed value
354         ((StatefulHandlerCallback) getProtocolHandler().getCallback()).removeState(id);
355
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();
394         }
395         // Can't refresh any others...
396     }
397
398     /**
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.
402      */
403     @Override
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");
409             return;
410         }
411         if (bridge.getStatus() != ThingStatus.ONLINE) {
412             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
413             return;
414         }
415
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!");
420             return;
421         }
422
423         if (!(handler instanceof RioControllerHandler)) {
424             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
425                     "Source must be attached to a controller bridge: " + handler.getClass());
426             return;
427         }
428
429         final RioZoneConfig config = getThing().getConfiguration().as(RioZoneConfig.class);
430         if (config == null) {
431             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
432             return;
433         }
434
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);
439             return;
440         }
441         zone.set(configZone);
442
443         final int handlerController = ((RioControllerHandler) handler).getId();
444         controller.set(handlerController);
445
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");
450             return;
451         }
452
453         setProtocolHandler(new RioZoneProtocol(configZone, handlerController, getSystemFavoritesHandler(),
454                 getPresetsProtocol(), socketSession, new StatefulHandlerCallback(new AbstractRioHandlerCallback() {
455                     @Override
456                     public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
457                         updateStatus(status, detail, msg);
458                     }
459
460                     @Override
461                     public void stateChanged(String channelId, State state) {
462                         if (channelId.equals(RioConstants.CHANNEL_ZONENAME)) {
463                             zoneName.set(state.toString());
464                         }
465                         updateState(channelId, state);
466                         fireStateUpdated(channelId, state);
467                     }
468
469                     @Override
470                     public void setProperty(String propertyName, String propertyValue) {
471                         getThing().setProperty(propertyName, propertyValue);
472                     }
473                 })));
474
475         updateStatus(ThingStatus.ONLINE);
476         getProtocolHandler().postOnline();
477     }
478
479     /**
480      * Returns the {@link RioHandlerCallback} related to the zone
481      *
482      * @return a possibly null {@link RioHandlerCallback}
483      */
484     @Override
485     public RioHandlerCallback getRioHandlerCallback() {
486         final RioZoneProtocol protocolHandler = getProtocolHandler();
487         return protocolHandler == null ? null : protocolHandler.getCallback();
488     }
489
490     /**
491      * Returns the {@link RioPresetsProtocol} related to the system. This simply queries the parent bridge for the
492      * protocol
493      *
494      * @return a possibly null {@link RioPresetsProtocol}
495      */
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();
501         }
502         return null;
503     }
504
505     /**
506      * Returns the {@link RioSystemFavoritesProtocol} related to the system. This simply queries the parent bridge for
507      * the protocol
508      *
509      * @return a possibly null {@link RioSystemFavoritesProtocol}
510      */
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();
516         }
517         return null;
518     }
519 }