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