]> git.basschouten.com Git - openhab-addons.git/blob
2d3a0ca21161030d8c82ea171637b3df6f87bc95
[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.yamahareceiver.internal.handler;
14
15 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
16 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
17
18 import java.io.IOException;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Map;
23 import java.util.Map.Entry;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.openhab.binding.yamahareceiver.internal.ChannelsTypeProviderAvailableInputs;
29 import org.openhab.binding.yamahareceiver.internal.ChannelsTypeProviderPreset;
30 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
31 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
32 import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
33 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
34 import org.openhab.binding.yamahareceiver.internal.protocol.IStateUpdatable;
35 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithNavigationControl;
36 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPlayControl;
37 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
38 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithTunerBandControl;
39 import org.openhab.binding.yamahareceiver.internal.protocol.ProtocolFactory;
40 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
41 import org.openhab.binding.yamahareceiver.internal.protocol.ZoneAvailableInputs;
42 import org.openhab.binding.yamahareceiver.internal.protocol.ZoneControl;
43 import org.openhab.binding.yamahareceiver.internal.protocol.xml.InputWithNavigationControlXML;
44 import org.openhab.binding.yamahareceiver.internal.protocol.xml.InputWithPlayControlXML;
45 import org.openhab.binding.yamahareceiver.internal.protocol.xml.ZoneControlXML;
46 import org.openhab.binding.yamahareceiver.internal.state.AvailableInputState;
47 import org.openhab.binding.yamahareceiver.internal.state.AvailableInputStateListener;
48 import org.openhab.binding.yamahareceiver.internal.state.DabBandState;
49 import org.openhab.binding.yamahareceiver.internal.state.DabBandStateListener;
50 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
51 import org.openhab.binding.yamahareceiver.internal.state.NavigationControlState;
52 import org.openhab.binding.yamahareceiver.internal.state.NavigationControlStateListener;
53 import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
54 import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
55 import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
56 import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
57 import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
58 import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
59 import org.openhab.core.config.core.Configuration;
60 import org.openhab.core.library.types.DecimalType;
61 import org.openhab.core.library.types.IncreaseDecreaseType;
62 import org.openhab.core.library.types.NextPreviousType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.PlayPauseType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.library.types.UpDownType;
68 import org.openhab.core.thing.Bridge;
69 import org.openhab.core.thing.Channel;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.ThingStatusInfo;
75 import org.openhab.core.thing.binding.BaseThingHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.binding.builder.ChannelBuilder;
78 import org.openhab.core.types.Command;
79 import org.openhab.core.types.RefreshType;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 /**
84  * The {@link YamahaZoneThingHandler} is managing one zone of a Yamaha AVR.
85  * It has a state consisting of the zone, the current input ID, {@link ZoneControlState}
86  * and some more state objects and uses the zone control protocol
87  * class {@link ZoneControlXML}, {@link InputWithPlayControlXML} and {@link InputWithNavigationControlXML}
88  * for communication.
89  *
90  * @author David Graeff <david.graeff@web.de>
91  * @author Tomasz Maruszak - [yamaha] Tuner band selection and preset feature for dual band models (RX-S601D), added
92  *         config object
93  */
94 public class YamahaZoneThingHandler extends BaseThingHandler
95         implements ZoneControlStateListener, NavigationControlStateListener, PlayInfoStateListener,
96         AvailableInputStateListener, PresetInfoStateListener, DabBandStateListener {
97
98     private final Logger logger = LoggerFactory.getLogger(YamahaZoneThingHandler.class);
99
100     private YamahaZoneConfig zoneConfig;
101
102     /// ChannelType providers
103     public @NonNullByDefault({}) ChannelsTypeProviderPreset channelsTypeProviderPreset;
104     public @NonNullByDefault({}) ChannelsTypeProviderAvailableInputs channelsTypeProviderAvailableInputs;
105
106     /// State
107     protected ZoneControlState zoneState = new ZoneControlState();
108     protected PresetInfoState presetInfoState = new PresetInfoState();
109     protected DabBandState dabBandState = new DabBandState();
110     protected PlayInfoState playInfoState = new PlayInfoState();
111     protected NavigationControlState navigationInfoState = new NavigationControlState();
112
113     /// Control
114     protected ZoneControl zoneControl;
115     protected InputWithPlayControl inputWithPlayControl;
116     protected InputWithNavigationControl inputWithNavigationControl;
117     protected ZoneAvailableInputs zoneAvailableInputs;
118     protected InputWithPresetControl inputWithPresetControl;
119     protected InputWithTunerBandControl inputWithDabBandControl;
120
121     public YamahaZoneThingHandler(Thing thing) {
122         super(thing);
123     }
124
125     @Override
126     public Collection<Class<? extends ThingHandlerService>> getServices() {
127         return Collections
128                 .unmodifiableList(Stream.of(ChannelsTypeProviderAvailableInputs.class, ChannelsTypeProviderPreset.class)
129                         .collect(Collectors.toList()));
130     }
131
132     /**
133      * Sets the {@link DeviceInformationState} for the handler.
134      */
135     public DeviceInformationState getDeviceInformationState() {
136         return getBridgeHandler().getDeviceInformationState();
137     }
138
139     @Override
140     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
141         validateConfigurationParameters(configurationParameters);
142
143         Configuration configuration = editConfiguration();
144         for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
145             configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
146         }
147
148         updateConfiguration(configuration);
149
150         zoneConfig = configuration.as(YamahaZoneConfig.class);
151         logger.trace("Updating configuration of {} with zone '{}'", getThing().getLabel(), zoneConfig.getZoneValue());
152     }
153
154     /**
155      * We handle updates of this thing ourself.
156      */
157     @Override
158     public void thingUpdated(Thing thing) {
159         this.thing = thing;
160     }
161
162     /**
163      * Calls createCommunicationObject if the host name is configured correctly.
164      */
165     @Override
166     public void initialize() {
167         // Determine the zone of this thing
168
169         zoneConfig = getConfigAs(YamahaZoneConfig.class);
170         logger.trace("Initialize {} with zone '{}'", getThing().getLabel(), zoneConfig.getZoneValue());
171
172         Bridge bridge = getBridge();
173         initializeThing(bridge != null ? bridge.getStatus() : null);
174     }
175
176     protected YamahaBridgeHandler getBridgeHandler() {
177         Bridge bridge = getBridge();
178         if (bridge == null) {
179             return null;
180         }
181         return (YamahaBridgeHandler) bridge.getHandler();
182     }
183
184     protected ProtocolFactory getProtocolFactory() {
185         return getBridgeHandler().getProtocolFactory();
186     }
187
188     protected AbstractConnection getConnection() {
189         return getBridgeHandler().getConnection();
190     }
191
192     @Override
193     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
194         initializeThing(bridgeStatusInfo.getStatus());
195     }
196
197     private void initializeThing(ThingStatus bridgeStatus) {
198         YamahaBridgeHandler bridgeHandler = getBridgeHandler();
199         if (bridgeHandler != null && bridgeStatus != null) {
200             if (bridgeStatus == ThingStatus.ONLINE) {
201                 if (zoneConfig == null || zoneConfig.getZone() == null) {
202                     String msg = String.format(
203                             "Zone not set or invalid zone name used: '%s'. It needs to be on of: '%s'",
204                             zoneConfig.getZoneValue(), Arrays.toString(Zone.values()));
205                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
206                     logger.info("{}", msg);
207                 } else {
208                     if (zoneControl == null) {
209                         YamahaBridgeHandler brHandler = getBridgeHandler();
210
211                         zoneControl = getProtocolFactory().ZoneControl(getConnection(), zoneConfig, this,
212                                 brHandler::getInputConverter, getDeviceInformationState());
213                         zoneAvailableInputs = getProtocolFactory().ZoneAvailableInputs(getConnection(), zoneConfig,
214                                 this, brHandler::getInputConverter, getDeviceInformationState());
215
216                         updateZoneInformation();
217                     }
218
219                     updateStatus(ThingStatus.ONLINE);
220                 }
221             } else {
222                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
223                 zoneControl = null;
224                 zoneAvailableInputs = null;
225             }
226         } else {
227             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
228         }
229     }
230
231     /**
232      * Return true if the zone is set, and zoneControl and zoneAvailableInputs objects have been created.
233      */
234     boolean isCorrectlyInitialized() {
235         return zoneConfig != null && zoneConfig.getZone() != null && zoneAvailableInputs != null && zoneControl != null;
236     }
237
238     /**
239      * Request new zone and available input information
240      */
241     void updateZoneInformation() {
242         updateAsyncMakeOfflineIfFail(zoneAvailableInputs);
243         updateAsyncMakeOfflineIfFail(zoneControl);
244
245         if (inputWithPlayControl != null) {
246             updateAsyncMakeOfflineIfFail(inputWithPlayControl);
247         }
248
249         if (inputWithNavigationControl != null) {
250             updateAsyncMakeOfflineIfFail(inputWithNavigationControl);
251         }
252
253         if (inputWithPresetControl != null) {
254             updateAsyncMakeOfflineIfFail(inputWithPresetControl);
255         }
256
257         if (inputWithDabBandControl != null) {
258             updateAsyncMakeOfflineIfFail(inputWithDabBandControl);
259         }
260     }
261
262     @Override
263     public void handleCommand(ChannelUID channelUID, Command command) {
264         if (zoneControl == null) {
265             return;
266         }
267
268         String id = channelUID.getIdWithoutGroup();
269
270         try {
271             if (command instanceof RefreshType) {
272                 refreshFromState(channelUID);
273                 return;
274             }
275
276             switch (id) {
277                 case CHANNEL_POWER:
278                     zoneControl.setPower(((OnOffType) command) == OnOffType.ON);
279                     break;
280                 case CHANNEL_INPUT:
281                     zoneControl.setInput(((StringType) command).toString());
282                     break;
283                 case CHANNEL_SURROUND:
284                     zoneControl.setSurroundProgram(((StringType) command).toString());
285                     break;
286                 case CHANNEL_VOLUME_DB:
287                     zoneControl.setVolumeDB(((DecimalType) command).floatValue());
288                     break;
289                 case CHANNEL_VOLUME:
290                     if (command instanceof DecimalType) {
291                         zoneControl.setVolume(((DecimalType) command).floatValue());
292                     } else if (command instanceof IncreaseDecreaseType) {
293                         zoneControl.setVolumeRelative(zoneState,
294                                 (((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE ? 1 : -1)
295                                         * zoneConfig.getVolumeRelativeChangeFactor());
296                     }
297                     break;
298                 case CHANNEL_MUTE:
299                     zoneControl.setMute(((OnOffType) command) == OnOffType.ON);
300                     break;
301                 case CHANNEL_SCENE:
302                     zoneControl.setScene(((StringType) command).toString());
303                     break;
304                 case CHANNEL_DIALOGUE_LEVEL:
305                     zoneControl.setDialogueLevel(((DecimalType) command).intValue());
306                     break;
307
308                 case CHANNEL_HDMI1OUT:
309                     zoneControl.setHDMI1Out(((OnOffType) command) == OnOffType.ON);
310                     break;
311
312                 case CHANNEL_HDMI2OUT:
313                     zoneControl.setHDMI2Out(((OnOffType) command) == OnOffType.ON);
314                     break;
315
316                 case CHANNEL_NAVIGATION_MENU:
317                     if (inputWithNavigationControl == null) {
318                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
319                         return;
320                     }
321
322                     String path = ((StringType) command).toFullString();
323                     inputWithNavigationControl.selectItemFullPath(path);
324                     break;
325
326                 case CHANNEL_NAVIGATION_UPDOWN:
327                     if (inputWithNavigationControl == null) {
328                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
329                         return;
330                     }
331                     if (((UpDownType) command) == UpDownType.UP) {
332                         inputWithNavigationControl.goUp();
333                     } else {
334                         inputWithNavigationControl.goDown();
335                     }
336                     break;
337
338                 case CHANNEL_NAVIGATION_LEFTRIGHT:
339                     if (inputWithNavigationControl == null) {
340                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
341                         return;
342                     }
343                     if (((UpDownType) command) == UpDownType.UP) {
344                         inputWithNavigationControl.goLeft();
345                     } else {
346                         inputWithNavigationControl.goRight();
347                     }
348                     break;
349
350                 case CHANNEL_NAVIGATION_SELECT:
351                     if (inputWithNavigationControl == null) {
352                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
353                         return;
354                     }
355                     inputWithNavigationControl.selectCurrentItem();
356                     break;
357
358                 case CHANNEL_NAVIGATION_BACK:
359                     if (inputWithNavigationControl == null) {
360                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
361                         return;
362                     }
363                     inputWithNavigationControl.goBack();
364                     break;
365
366                 case CHANNEL_NAVIGATION_BACKTOROOT:
367                     if (inputWithNavigationControl == null) {
368                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
369                         return;
370                     }
371                     inputWithNavigationControl.goToRoot();
372                     break;
373
374                 case CHANNEL_PLAYBACK_PRESET:
375                     if (inputWithPresetControl == null) {
376                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
377                         return;
378                     }
379
380                     if (command instanceof DecimalType) {
381                         inputWithPresetControl.selectItemByPresetNumber(((DecimalType) command).intValue());
382                     } else if (command instanceof StringType) {
383                         try {
384                             int v = Integer.valueOf(((StringType) command).toString());
385                             inputWithPresetControl.selectItemByPresetNumber(v);
386                         } catch (NumberFormatException e) {
387                             logger.warn("Provide a number for {}", id);
388                         }
389                     }
390                     break;
391
392                 case CHANNEL_TUNER_BAND:
393                     if (inputWithDabBandControl == null) {
394                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
395                         return;
396                     }
397
398                     if (command instanceof StringType) {
399                         inputWithDabBandControl.selectBandByName(command.toString());
400                     } else {
401                         logger.warn("Provide a string for {}", id);
402                     }
403                     break;
404
405                 case CHANNEL_PLAYBACK:
406                     if (inputWithPlayControl == null) {
407                         logger.warn("Channel {} not working with {} input!", id, zoneState.inputID);
408                         return;
409                     }
410
411                     if (command instanceof PlayPauseType) {
412                         PlayPauseType t = ((PlayPauseType) command);
413                         switch (t) {
414                             case PAUSE:
415                                 inputWithPlayControl.pause();
416                                 break;
417                             case PLAY:
418                                 inputWithPlayControl.play();
419                                 break;
420                         }
421                     } else if (command instanceof NextPreviousType) {
422                         NextPreviousType t = ((NextPreviousType) command);
423                         switch (t) {
424                             case NEXT:
425                                 inputWithPlayControl.nextTrack();
426                                 break;
427                             case PREVIOUS:
428                                 inputWithPlayControl.previousTrack();
429                                 break;
430                         }
431                     } else if (command instanceof DecimalType) {
432                         int v = ((DecimalType) command).intValue();
433                         if (v < 0) {
434                             inputWithPlayControl.skipREV();
435                         } else if (v > 0) {
436                             inputWithPlayControl.skipFF();
437                         }
438                     } else if (command instanceof StringType) {
439                         String v = ((StringType) command).toFullString();
440                         switch (v) {
441                             case "Play":
442                                 inputWithPlayControl.play();
443                                 break;
444                             case "Pause":
445                                 inputWithPlayControl.pause();
446                                 break;
447                             case "Stop":
448                                 inputWithPlayControl.stop();
449                                 break;
450                             case "Rewind":
451                                 inputWithPlayControl.skipREV();
452                                 break;
453                             case "FastForward":
454                                 inputWithPlayControl.skipFF();
455                                 break;
456                             case "Next":
457                                 inputWithPlayControl.nextTrack();
458                                 break;
459                             case "Previous":
460                                 inputWithPlayControl.previousTrack();
461                                 break;
462                         }
463                     }
464                     break;
465                 default:
466                     logger.warn("Channel {} not supported!", id);
467             }
468         } catch (IOException e) {
469             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
470         } catch (ReceivedMessageParseException e) {
471             // Some AVRs send unexpected responses. We log parser exceptions therefore.
472             logger.debug("Parse error!", e);
473         }
474     }
475
476     /**
477      * Called by handleCommand() if a RefreshType command was received. It will update
478      * the given channel with the correct state.
479      *
480      * @param channelUID The channel
481      */
482     private void refreshFromState(ChannelUID channelUID) {
483         String id = channelUID.getId();
484
485         if (id.equals(grpZone(CHANNEL_POWER))) {
486             updateState(channelUID, zoneState.power ? OnOffType.ON : OnOffType.OFF);
487
488         } else if (id.equals(grpZone(CHANNEL_VOLUME_DB))) {
489             updateState(channelUID, new DecimalType(zoneState.volumeDB));
490         } else if (id.equals(grpZone(CHANNEL_VOLUME))) {
491             updateState(channelUID, new PercentType((int) zoneConfig.getVolumePercentage(zoneState.volumeDB)));
492         } else if (id.equals(grpZone(CHANNEL_MUTE))) {
493             updateState(channelUID, zoneState.mute ? OnOffType.ON : OnOffType.OFF);
494         } else if (id.equals(grpZone(CHANNEL_INPUT))) {
495             updateState(channelUID, new StringType(zoneState.inputID));
496         } else if (id.equals(grpZone(CHANNEL_SURROUND))) {
497             updateState(channelUID, new StringType(zoneState.surroundProgram));
498         } else if (id.equals(grpZone(CHANNEL_SCENE))) {
499             // no state updates available
500         } else if (id.equals(grpZone(CHANNEL_DIALOGUE_LEVEL))) {
501             updateState(channelUID, new DecimalType(zoneState.dialogueLevel));
502         } else if (id.equals(grpZone(CHANNEL_HDMI1OUT))) {
503             updateState(channelUID, zoneState.hdmi1Out ? OnOffType.ON : OnOffType.OFF);
504         } else if (id.equals(grpZone(CHANNEL_HDMI2OUT))) {
505             updateState(channelUID, zoneState.hdmi2Out ? OnOffType.ON : OnOffType.OFF);
506
507         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK))) {
508             updateState(channelUID, new StringType(playInfoState.playbackMode));
509         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_STATION))) {
510             updateState(channelUID, new StringType(playInfoState.station));
511         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_ARTIST))) {
512             updateState(channelUID, new StringType(playInfoState.artist));
513         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_ALBUM))) {
514             updateState(channelUID, new StringType(playInfoState.album));
515         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_SONG))) {
516             updateState(channelUID, new StringType(playInfoState.song));
517         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_SONG_IMAGE_URL))) {
518             updateState(channelUID, new StringType(playInfoState.songImageUrl));
519         } else if (id.equals(grpPlayback(CHANNEL_PLAYBACK_PRESET))) {
520             updateState(channelUID, new DecimalType(presetInfoState.presetChannel));
521         } else if (id.equals(grpPlayback(CHANNEL_TUNER_BAND))) {
522             updateState(channelUID, new StringType(dabBandState.band));
523
524         } else if (id.equals(grpNav(CHANNEL_NAVIGATION_MENU))) {
525             updateState(channelUID, new StringType(navigationInfoState.getCurrentItemName()));
526         } else if (id.equals(grpNav(CHANNEL_NAVIGATION_LEVEL))) {
527             updateState(channelUID, new DecimalType(navigationInfoState.menuLayer));
528         } else if (id.equals(grpNav(CHANNEL_NAVIGATION_CURRENT_ITEM))) {
529             updateState(channelUID, new DecimalType(navigationInfoState.currentLine));
530         } else if (id.equals(grpNav(CHANNEL_NAVIGATION_TOTAL_ITEMS))) {
531             updateState(channelUID, new DecimalType(navigationInfoState.maxLine));
532         } else {
533             logger.warn("Channel {} not implemented!", id);
534         }
535     }
536
537     @Override
538     public void zoneStateChanged(ZoneControlState msg) {
539         boolean inputChanged = !msg.inputID.equals(zoneState.inputID);
540         zoneState = msg;
541
542         updateStatus(ThingStatus.ONLINE);
543
544         updateState(grpZone(CHANNEL_POWER), zoneState.power ? OnOffType.ON : OnOffType.OFF);
545         updateState(grpZone(CHANNEL_INPUT), new StringType(zoneState.inputID));
546         updateState(grpZone(CHANNEL_SURROUND), new StringType(zoneState.surroundProgram));
547         updateState(grpZone(CHANNEL_VOLUME_DB), new DecimalType(zoneState.volumeDB));
548         updateState(grpZone(CHANNEL_VOLUME), new PercentType((int) zoneConfig.getVolumePercentage(zoneState.volumeDB)));
549         updateState(grpZone(CHANNEL_MUTE), zoneState.mute ? OnOffType.ON : OnOffType.OFF);
550         updateState(grpZone(CHANNEL_DIALOGUE_LEVEL), new DecimalType(zoneState.dialogueLevel));
551         updateState(grpZone(CHANNEL_HDMI1OUT), zoneState.hdmi1Out ? OnOffType.ON : OnOffType.OFF);
552         updateState(grpZone(CHANNEL_HDMI2OUT), zoneState.hdmi2Out ? OnOffType.ON : OnOffType.OFF);
553
554         // If the input changed
555         if (inputChanged) {
556             inputChanged();
557         }
558     }
559
560     /**
561      * Called by {@link #zoneStateChanged(ZoneControlState)} if the input has changed.
562      * Will request updates from {@see InputWithNavigationControl} and {@see InputWithPlayControl}.
563      */
564     private void inputChanged() {
565         logger.debug("Input changed to {}", zoneState.inputID);
566
567         if (!isInputSupported(zoneState.inputID)) {
568             // for now just emit a warning in logs
569             logger.warn("Input {} is not supported on your AVR model", zoneState.inputID);
570         }
571
572         inputChangedCheckForNavigationControl();
573         // Note: the DAB band needs to be initialized before preset and playback
574         inputChangedCheckForDabBand();
575         inputChangedCheckForPlaybackControl();
576         inputChangedCheckForPresetControl();
577     }
578
579     /**
580      * Checks if the specified input is supported given the detected device feature information.
581      *
582      * @param inputID - the input name
583      * @return true when input is supported
584      */
585     private boolean isInputSupported(String inputID) {
586         switch (inputID) {
587             case INPUT_SPOTIFY:
588                 return getDeviceInformationState().features.contains(Feature.SPOTIFY);
589
590             case INPUT_TUNER:
591                 return getDeviceInformationState().features.contains(Feature.TUNER)
592                         || getDeviceInformationState().features.contains(Feature.DAB);
593
594             // Note: add more inputs here in the future
595         }
596         return true;
597     }
598
599     private void inputChangedCheckForNavigationControl() {
600         boolean includeInputWithNavigationControl = false;
601
602         for (String channelName : CHANNELS_NAVIGATION) {
603             if (isLinked(grpNav(channelName))) {
604                 includeInputWithNavigationControl = true;
605                 break;
606             }
607         }
608
609         if (includeInputWithNavigationControl) {
610             includeInputWithNavigationControl = InputWithNavigationControl.SUPPORTED_INPUTS.contains(zoneState.inputID);
611             if (!includeInputWithNavigationControl) {
612                 logger.debug("Navigation control not supported by {}", zoneState.inputID);
613             }
614         }
615
616         logger.trace("Navigation control requested by channel");
617
618         if (!includeInputWithNavigationControl) {
619             inputWithNavigationControl = null;
620             navigationInfoState.invalidate();
621             navigationUpdated(navigationInfoState);
622             return;
623         }
624
625         inputWithNavigationControl = getProtocolFactory().InputWithNavigationControl(getConnection(),
626                 navigationInfoState, zoneState.inputID, this, getDeviceInformationState());
627
628         updateAsyncMakeOfflineIfFail(inputWithNavigationControl);
629     }
630
631     private void inputChangedCheckForPlaybackControl() {
632         boolean includeInputWithPlaybackControl = false;
633
634         for (String channelName : CHANNELS_PLAYBACK) {
635             if (isLinked(grpPlayback(channelName))) {
636                 includeInputWithPlaybackControl = true;
637                 break;
638             }
639         }
640
641         logger.trace("Playback control requested by channel");
642
643         if (includeInputWithPlaybackControl) {
644             includeInputWithPlaybackControl = InputWithPlayControl.SUPPORTED_INPUTS.contains(zoneState.inputID);
645             if (!includeInputWithPlaybackControl) {
646                 logger.debug("Playback control not supported by {}", zoneState.inputID);
647             }
648         }
649
650         if (!includeInputWithPlaybackControl) {
651             inputWithPlayControl = null;
652             playInfoState.invalidate();
653             playInfoUpdated(playInfoState);
654             return;
655         }
656
657         /**
658          * The {@link inputChangedCheckForDabBand} needs to be called first before this method, in case the AVR Supports
659          * DAB
660          */
661         if (inputWithDabBandControl != null) {
662             // When input is Tuner DAB there is no playback control
663             inputWithPlayControl = null;
664         } else {
665             inputWithPlayControl = getProtocolFactory().InputWithPlayControl(getConnection(), zoneState.inputID, this,
666                     getBridgeHandler().getConfiguration(), getDeviceInformationState());
667
668             updateAsyncMakeOfflineIfFail(inputWithPlayControl);
669         }
670     }
671
672     private void inputChangedCheckForPresetControl() {
673         boolean includeInput = isLinked(grpPlayback(CHANNEL_PLAYBACK_PRESET));
674
675         logger.trace("Preset control requested by channel");
676
677         if (includeInput) {
678             includeInput = InputWithPresetControl.SUPPORTED_INPUTS.contains(zoneState.inputID);
679             if (!includeInput) {
680                 logger.debug("Preset control not supported by {}", zoneState.inputID);
681             }
682         }
683
684         if (!includeInput) {
685             inputWithPresetControl = null;
686             presetInfoState.invalidate();
687             presetInfoUpdated(presetInfoState);
688             return;
689         }
690
691         /**
692          * The {@link inputChangedCheckForDabBand} needs to be called first before this method, in case the AVR Supports
693          * DAB
694          */
695         if (inputWithDabBandControl != null) {
696             // When the input is Tuner DAB the control also provides preset functionality
697             inputWithPresetControl = (InputWithPresetControl) inputWithDabBandControl;
698             // Note: No need to update state - it will be already called for DabBand control (see
699             // inputChangedCheckForDabBand)
700         } else {
701             inputWithPresetControl = getProtocolFactory().InputWithPresetControl(getConnection(), zoneState.inputID,
702                     this, getDeviceInformationState());
703
704             updateAsyncMakeOfflineIfFail(inputWithPresetControl);
705         }
706     }
707
708     private void inputChangedCheckForDabBand() {
709         boolean includeInput = isLinked(grpPlayback(CHANNEL_TUNER_BAND));
710
711         logger.trace("Band control requested by channel");
712
713         if (includeInput) {
714             // Check if TUNER input is DAB - dual bands radio tuner
715             includeInput = InputWithTunerBandControl.SUPPORTED_INPUTS.contains(zoneState.inputID)
716                     && getDeviceInformationState().features.contains(Feature.DAB);
717             if (!includeInput) {
718                 logger.debug("Band control not supported by {}", zoneState.inputID);
719             }
720         }
721
722         if (!includeInput) {
723             inputWithDabBandControl = null;
724             dabBandState.invalidate();
725             dabBandUpdated(dabBandState);
726             return;
727         }
728
729         logger.debug("InputWithTunerBandControl created for {}", zoneState.inputID);
730         inputWithDabBandControl = getProtocolFactory().InputWithDabBandControl(zoneState.inputID, getConnection(), this,
731                 this, this, getDeviceInformationState());
732
733         updateAsyncMakeOfflineIfFail(inputWithDabBandControl);
734     }
735
736     protected void updateAsyncMakeOfflineIfFail(IStateUpdatable stateUpdatable) {
737         scheduler.submit(() -> {
738             try {
739                 stateUpdatable.update();
740             } catch (IOException e) {
741                 logger.debug("State update error. Changing thing to offline", e);
742                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
743             } catch (ReceivedMessageParseException e) {
744                 String message = e.getMessage();
745                 updateProperty(PROPERTY_LAST_PARSE_ERROR, message != null ? message : "");
746                 // Some AVRs send unexpected responses. We log parser exceptions therefore.
747                 logger.debug("Parse error!", e);
748             }
749         });
750     }
751
752     /**
753      * Once this thing is set up and the AVR is connected, the available inputs for this zone are requested.
754      * The thing is updated with a new CHANNEL_AVAILABLE_INPUT which lists the available inputs for the current zone..
755      */
756     @Override
757     public void availableInputsChanged(AvailableInputState msg) {
758         // Update channel type provider with a list of available inputs
759         channelsTypeProviderAvailableInputs.changeAvailableInputs(msg.availableInputs);
760
761         // Remove the old channel and add the new channel. The channel will be requested from the
762         // yamahaChannelTypeProvider.
763         ChannelUID inputChannelUID = new ChannelUID(thing.getUID(), CHANNEL_GROUP_ZONE, CHANNEL_INPUT);
764         Channel channel = ChannelBuilder.create(inputChannelUID, "String")
765                 .withType(channelsTypeProviderAvailableInputs.getChannelTypeUID()).build();
766         updateThing(editThing().withoutChannel(inputChannelUID).withChannel(channel).build());
767     }
768
769     private String grpPlayback(String channelIDWithoutGroup) {
770         return new ChannelUID(thing.getUID(), CHANNEL_GROUP_PLAYBACK, channelIDWithoutGroup).getId();
771     }
772
773     private String grpNav(String channelIDWithoutGroup) {
774         return new ChannelUID(thing.getUID(), CHANNEL_GROUP_NAVIGATION, channelIDWithoutGroup).getId();
775     }
776
777     private String grpZone(String channelIDWithoutGroup) {
778         return new ChannelUID(thing.getUID(), CHANNEL_GROUP_ZONE, channelIDWithoutGroup).getId();
779     }
780
781     @Override
782     public void playInfoUpdated(PlayInfoState msg) {
783         playInfoState = msg;
784
785         updateState(grpPlayback(CHANNEL_PLAYBACK), new StringType(msg.playbackMode));
786         updateState(grpPlayback(CHANNEL_PLAYBACK_STATION), new StringType(msg.station));
787         updateState(grpPlayback(CHANNEL_PLAYBACK_ARTIST), new StringType(msg.artist));
788         updateState(grpPlayback(CHANNEL_PLAYBACK_ALBUM), new StringType(msg.album));
789         updateState(grpPlayback(CHANNEL_PLAYBACK_SONG), new StringType(msg.song));
790         updateState(grpPlayback(CHANNEL_PLAYBACK_SONG_IMAGE_URL), new StringType(msg.songImageUrl));
791     }
792
793     @Override
794     public void presetInfoUpdated(PresetInfoState msg) {
795         presetInfoState = msg;
796
797         if (msg.presetChannelNamesChanged) {
798             msg.presetChannelNamesChanged = false;
799
800             channelsTypeProviderPreset.changePresetNames(msg.presetChannelNames);
801
802             // Remove the old channel and add the new channel. The channel will be requested from the
803             // channelsTypeProviderPreset.
804             ChannelUID inputChannelUID = new ChannelUID(thing.getUID(), CHANNEL_GROUP_PLAYBACK,
805                     CHANNEL_PLAYBACK_PRESET);
806             Channel channel = ChannelBuilder.create(inputChannelUID, "Number")
807                     .withType(channelsTypeProviderPreset.getChannelTypeUID()).build();
808             updateThing(editThing().withoutChannel(inputChannelUID).withChannel(channel).build());
809         }
810
811         updateState(grpPlayback(CHANNEL_PLAYBACK_PRESET), new DecimalType(msg.presetChannel));
812     }
813
814     @Override
815     public void dabBandUpdated(DabBandState msg) {
816         dabBandState = msg;
817         updateState(grpPlayback(CHANNEL_TUNER_BAND), new StringType(msg.band));
818     }
819
820     @Override
821     public void navigationUpdated(NavigationControlState msg) {
822         navigationInfoState = msg;
823         updateState(grpNav(CHANNEL_NAVIGATION_MENU), new StringType(msg.menuName));
824         updateState(grpNav(CHANNEL_NAVIGATION_LEVEL), new DecimalType(msg.menuLayer));
825         updateState(grpNav(CHANNEL_NAVIGATION_CURRENT_ITEM), new DecimalType(msg.currentLine));
826         updateState(grpNav(CHANNEL_NAVIGATION_TOTAL_ITEMS), new DecimalType(msg.maxLine));
827     }
828
829     @Override
830     public void navigationError(String msg) {
831         updateProperty(PROPERTY_MENU_ERROR, msg);
832         logger.warn("Navigation error: {}", msg);
833     }
834 }