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