]> git.basschouten.com Git - openhab-addons.git/blob
653ef3342bc957b8824afad21b29a0b251a1e903
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.bosesoundtouch.internal;
14
15 import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.*;
16 import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
17 import static org.openhab.core.thing.Thing.PROPERTY_HARDWARE_VERSION;
18 import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
19
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Stack;
24
25 import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
26 import org.openhab.core.io.net.http.HttpUtil;
27 import org.openhab.core.library.types.DecimalType;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.PercentType;
30 import org.openhab.core.library.types.PlayPauseType;
31 import org.openhab.core.library.types.RawType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.types.State;
34 import org.openhab.core.types.UnDefType;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.xml.sax.Attributes;
38 import org.xml.sax.SAXException;
39 import org.xml.sax.helpers.DefaultHandler;
40
41 /**
42  * The {@link XMLResponseHandler} class handles the XML communication with the Soundtouch
43  *
44  * @author Christian Niessner - Initial contribution
45  * @author Thomas Traunbauer - Initial contribution
46  * @author Kai Kreuzer - code clean up
47  */
48 public class XMLResponseHandler extends DefaultHandler {
49
50     private final Logger logger = LoggerFactory.getLogger(XMLResponseHandler.class);
51
52     private BoseSoundTouchHandler handler;
53     private CommandExecutor commandExecutor;
54
55     private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap;
56
57     private Stack<XMLHandlerState> states;
58     private XMLHandlerState state;
59     private boolean msgHeaderWasValid;
60
61     private ContentItem contentItem;
62     private boolean volumeMuteEnabled;
63     private OnOffType rateEnabled;
64     private OnOffType skipEnabled;
65     private OnOffType skipPreviousEnabled;
66
67     private State nowPlayingSource;
68
69     private BoseSoundTouchConfiguration masterDeviceId;
70     String deviceId;
71
72     private Map<Integer, ContentItem> playerPresets;
73
74     /**
75      * Creates a new instance of this class
76      *
77      * @param handler
78      * @param stateSwitchingMap the stateSwitchingMap is the XMLState Map, that says which Flags are computed
79      */
80     public XMLResponseHandler(BoseSoundTouchHandler handler,
81             Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap) {
82         this.handler = handler;
83         this.commandExecutor = handler.getCommandExecutor();
84         this.stateSwitchingMap = stateSwitchingMap;
85         init();
86     }
87
88     @Override
89     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
90         super.startElement(uri, localName, qName, attributes);
91         logger.trace("{}: startElement('{}'; state: {})", handler.getDeviceName(), localName, state);
92         states.push(state);
93         XMLHandlerState curState = state; // save for switch statement
94         Map<String, XMLHandlerState> stateMap = stateSwitchingMap.get(state);
95         state = XMLHandlerState.Unprocessed; // set default value; we avoid default in select to have the compiler
96                                              // showing a
97         // warning for unhandled states
98         switch (curState) {
99             case INIT:
100                 if ("updates".equals(localName)) {
101                     // it just seems to be a ping - havn't seen any data on it..
102                     if (checkDeviceId(localName, attributes, false)) {
103                         state = XMLHandlerState.Updates;
104                     } else {
105                         state = XMLHandlerState.Unprocessed;
106                     }
107                 } else {
108                     state = stateMap.get(localName);
109                     if (state == null) {
110                         if (logger.isDebugEnabled()) {
111                             logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
112                                     localName);
113                         }
114                         state = XMLHandlerState.Unprocessed;
115                     }
116                 }
117                 break;
118             case Msg:
119                 if ("header".equals(localName)) {
120                     // message
121                     if (checkDeviceId(localName, attributes, false)) {
122                         state = XMLHandlerState.MsgHeader;
123                         msgHeaderWasValid = true;
124                     } else {
125                         state = XMLHandlerState.Unprocessed;
126                     }
127                 } else if ("body".equals(localName)) {
128                     if (msgHeaderWasValid) {
129                         state = XMLHandlerState.MsgBody;
130                     } else {
131                         state = XMLHandlerState.Unprocessed;
132                     }
133                 } else {
134                     if (logger.isDebugEnabled()) {
135                         logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
136                                 localName);
137                     }
138                     state = XMLHandlerState.Unprocessed;
139                 }
140                 break;
141             case MsgHeader:
142                 if ("request".equals(localName)) {
143                     state = XMLHandlerState.Unprocessed; // TODO implement request id / response tracking...
144                 } else {
145                     if (logger.isDebugEnabled()) {
146                         logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
147                                 localName);
148                     }
149                     state = XMLHandlerState.Unprocessed;
150                 }
151                 break;
152             case MsgBody:
153                 if ("nowPlaying".equals(localName)) {
154                     /*
155                      * if (!checkDeviceId(localName, attributes, true)) {
156                      * state = XMLHandlerState.Unprocessed;
157                      * break;
158                      * }
159                      */
160                     rateEnabled = OnOffType.OFF;
161                     skipEnabled = OnOffType.OFF;
162                     skipPreviousEnabled = OnOffType.OFF;
163                     state = XMLHandlerState.NowPlaying;
164                     String source = attributes.getValue("source");
165                     if (nowPlayingSource == null || !nowPlayingSource.toString().equals(source)) {
166                         // source changed
167                         nowPlayingSource = new StringType(source);
168                         // reset enabled states
169                         updateRateEnabled(OnOffType.OFF);
170                         updateSkipEnabled(OnOffType.OFF);
171                         updateSkipPreviousEnabled(OnOffType.OFF);
172
173                         // clear all "nowPlaying" details on source change...
174                         updateNowPlayingAlbum(UnDefType.NULL);
175                         updateNowPlayingArtwork(UnDefType.NULL);
176                         updateNowPlayingArtist(UnDefType.NULL);
177                         updateNowPlayingDescription(UnDefType.NULL);
178                         updateNowPlayingGenre(UnDefType.NULL);
179                         updateNowPlayingItemName(UnDefType.NULL);
180                         updateNowPlayingStationLocation(UnDefType.NULL);
181                         updateNowPlayingStationName(UnDefType.NULL);
182                         updateNowPlayingTrack(UnDefType.NULL);
183                     }
184                 } else if ("zone".equals(localName)) {
185                     state = XMLHandlerState.Zone;
186                 } else if ("presets".equals(localName)) {
187                     // reset the current playerPrests
188                     playerPresets = new HashMap<>();
189                     for (int i = 1; i <= 6; i++) {
190                         playerPresets.put(i, null);
191                     }
192                     state = XMLHandlerState.Presets;
193                 } else if ("group".equals(localName)) {
194                     this.masterDeviceId = new BoseSoundTouchConfiguration();
195                     state = stateMap.get(localName);
196                 } else {
197                     state = stateMap.get(localName);
198                     if (state == null) {
199                         if (logger.isDebugEnabled()) {
200                             logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
201                                     localName);
202                         }
203                         state = XMLHandlerState.Unprocessed;
204                     } else if (state != XMLHandlerState.Volume && state != XMLHandlerState.Presets
205                             && state != XMLHandlerState.Group && state != XMLHandlerState.Unprocessed) {
206                         if (!checkDeviceId(localName, attributes, false)) {
207                             state = XMLHandlerState.Unprocessed;
208                             break;
209                         }
210                     }
211                 }
212                 break;
213             case Presets:
214                 if ("preset".equals(localName)) {
215                     state = XMLHandlerState.Preset;
216                     String id = attributes.getValue("id");
217                     if (contentItem == null) {
218                         contentItem = new ContentItem();
219                     }
220                     contentItem.setPresetID(Integer.parseInt(id));
221                 } else {
222                     if (logger.isDebugEnabled()) {
223                         logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
224                                 localName);
225                     }
226                     state = XMLHandlerState.Unprocessed;
227                 }
228                 break;
229             case Sources:
230                 if ("sourceItem".equals(localName)) {
231                     state = XMLHandlerState.Unprocessed;
232                     String source = attributes.getValue("source");
233                     String sourceAccount = attributes.getValue("sourceAccount");
234                     String status = attributes.getValue("status");
235                     if (status.equals("READY")) {
236                         if (source.equals("AUX")) {
237                             if (sourceAccount.equals("AUX")) {
238                                 commandExecutor.setAUXAvailable(true);
239                             }
240                             if (sourceAccount.equals("AUX1")) {
241                                 commandExecutor.setAUX1Available(true);
242                             }
243                             if (sourceAccount.equals("AUX2")) {
244                                 commandExecutor.setAUX2Available(true);
245                             }
246                             if (sourceAccount.equals("AUX3")) {
247                                 commandExecutor.setAUX3Available(true);
248                             }
249                         }
250                         if (source.equals("STORED_MUSIC")) {
251                             commandExecutor.setStoredMusicAvailable(true);
252                         }
253                         if (source.equals("INTERNET_RADIO")) {
254                             commandExecutor.setInternetRadioAvailable(true);
255                         }
256                         if (source.equals("BLUETOOTH")) {
257                             commandExecutor.setBluetoothAvailable(true);
258                         }
259                         if (source.equals("PRODUCT")) {
260                             if (sourceAccount.equals("TV")) {
261                                 commandExecutor.setTVAvailable(true);
262                             }
263                             if (sourceAccount.equals("HDMI_1")) {
264                                 commandExecutor.setHDMI1Available(true);
265                             }
266                         }
267                     }
268                 } else {
269                     if (logger.isDebugEnabled()) {
270                         logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
271                                 localName);
272                     }
273                     state = XMLHandlerState.Unprocessed;
274                 }
275                 break;
276             // auto go trough the state map
277             case Group:
278             case Zone:
279             case Bass:
280             case ContentItem:
281             case MasterDeviceId:
282             case GroupName:
283             case DeviceId:
284             case DeviceIp:
285             case Info:
286             case NowPlaying:
287             case Preset:
288             case Updates:
289             case Volume:
290             case Components:
291             case Component:
292                 state = nextState(stateMap, curState, localName);
293                 break;
294             case BassCapabilities:
295                 state = nextState(stateMap, curState, localName);
296                 break;
297             // all entities without any children expected..
298             case BassTarget:
299             case BassActual:
300             case BassUpdated:
301             case BassMin:
302             case BassMax:
303             case BassDefault:
304             case ContentItemItemName:
305             case ContentItemContainerArt:
306             case InfoName:
307             case InfoType:
308             case InfoFirmwareVersion:
309             case InfoModuleType:
310             case NowPlayingAlbum:
311             case NowPlayingArt:
312             case NowPlayingArtist:
313             case NowPlayingGenre:
314             case NowPlayingDescription:
315             case NowPlayingPlayStatus:
316             case NowPlayingRateEnabled:
317             case NowPlayingSkipEnabled:
318             case NowPlayingSkipPreviousEnabled:
319             case NowPlayingStationLocation:
320             case NowPlayingStationName:
321             case NowPlayingTrack:
322             case VolumeTarget:
323             case VolumeActual:
324             case VolumeUpdated:
325             case VolumeMuteEnabled:
326             case ZoneMember:
327             case ZoneUpdated: // currently this dosn't provide any zone details..
328                 if (logger.isDebugEnabled()) {
329                     logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
330                             localName);
331                 }
332                 state = XMLHandlerState.Unprocessed;
333                 break;
334             case BassAvailable:
335                 if (logger.isDebugEnabled()) {
336                     logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
337                             localName);
338                 }
339                 state = XMLHandlerState.Unprocessed;
340                 break;
341             case Unprocessed:
342                 // all further things are also unprocessed
343                 state = XMLHandlerState.Unprocessed;
344                 break;
345             case UnprocessedNoTextExpected:
346                 state = XMLHandlerState.UnprocessedNoTextExpected;
347                 break;
348         }
349         if (state == XMLHandlerState.ContentItem) {
350             if (contentItem == null) {
351                 contentItem = new ContentItem();
352             }
353             contentItem.setSource(attributes.getValue("source"));
354             contentItem.setSourceAccount(attributes.getValue("sourceAccount"));
355             contentItem.setLocation(attributes.getValue("location"));
356             contentItem.setPresetable(Boolean.parseBoolean(attributes.getValue("isPresetable")));
357             for (int attrId = 0; attrId < attributes.getLength(); attrId++) {
358                 String attrName = attributes.getLocalName(attrId);
359                 if ("source".equalsIgnoreCase(attrName)) {
360                     continue;
361                 }
362                 if ("location".equalsIgnoreCase(attrName)) {
363                     continue;
364                 }
365                 if ("sourceAccount".equalsIgnoreCase(attrName)) {
366                     continue;
367                 }
368                 if ("isPresetable".equalsIgnoreCase(attrName)) {
369                     continue;
370                 }
371                 contentItem.setAdditionalAttribute(attrName, attributes.getValue(attrId));
372             }
373         }
374     }
375
376     @Override
377     public void endElement(String uri, String localName, String qName) throws SAXException {
378         super.endElement(uri, localName, qName);
379         logger.trace("{}: endElement('{}')", handler.getDeviceName(), localName);
380         final XMLHandlerState prevState = state;
381         state = states.pop();
382         switch (prevState) {
383             case Info:
384                 commandExecutor.getInformations(APIRequest.VOLUME);
385                 commandExecutor.getInformations(APIRequest.PRESETS);
386                 commandExecutor.getInformations(APIRequest.NOW_PLAYING);
387                 commandExecutor.getInformations(APIRequest.GET_ZONE);
388                 commandExecutor.getInformations(APIRequest.BASS);
389                 commandExecutor.getInformations(APIRequest.SOURCES);
390                 commandExecutor.getInformations(APIRequest.BASSCAPABILITIES);
391                 commandExecutor.getInformations(APIRequest.GET_GROUP);
392                 break;
393             case ContentItem:
394                 if (state == XMLHandlerState.NowPlaying) {
395                     // update now playing name...
396                     updateNowPlayingItemName(new StringType(contentItem.getItemName()));
397                     commandExecutor.setCurrentContentItem(contentItem);
398                 }
399                 break;
400             case Preset:
401                 if (state == XMLHandlerState.Presets) {
402                     playerPresets.put(contentItem.getPresetID(), contentItem);
403                     contentItem = null;
404                 }
405                 break;
406             case NowPlaying:
407                 if (state == XMLHandlerState.MsgBody) {
408                     updateRateEnabled(rateEnabled);
409                     updateSkipEnabled(skipEnabled);
410                     updateSkipPreviousEnabled(skipPreviousEnabled);
411                 }
412                 break;
413             // handle special tags..
414             case BassUpdated:
415                 // request current bass level
416                 commandExecutor.getInformations(APIRequest.BASS);
417                 break;
418             case VolumeUpdated:
419                 commandExecutor.getInformations(APIRequest.VOLUME);
420                 break;
421             case NowPlayingRateEnabled:
422                 rateEnabled = OnOffType.ON;
423                 break;
424             case NowPlayingSkipEnabled:
425                 skipEnabled = OnOffType.ON;
426                 break;
427             case NowPlayingSkipPreviousEnabled:
428                 skipPreviousEnabled = OnOffType.ON;
429                 break;
430             case Volume:
431                 OnOffType muted = volumeMuteEnabled ? OnOffType.ON : OnOffType.OFF;
432                 commandExecutor.setCurrentMuted(volumeMuteEnabled);
433                 commandExecutor.postVolumeMuted(muted);
434                 break;
435             case ZoneUpdated:
436                 commandExecutor.getInformations(APIRequest.GET_ZONE);
437                 break;
438             case Presets:
439                 commandExecutor.updatePresetContainerFromPlayer(playerPresets);
440                 playerPresets = null;
441                 break;
442             case Group:
443                 handler.handleGroupUpdated(masterDeviceId);
444                 break;
445             default:
446                 // no actions...
447                 break;
448         }
449     }
450
451     @Override
452     public void characters(char[] ch, int start, int length) throws SAXException {
453         logger.trace("{}: Text data during {}: '{}'", handler.getDeviceName(), state, new String(ch, start, length));
454         super.characters(ch, start, length);
455         switch (state) {
456             case INIT:
457             case Msg:
458             case MsgHeader:
459             case MsgBody:
460             case Bass:
461             case BassUpdated:
462             case Updates:
463             case Volume:
464             case VolumeUpdated:
465             case Info:
466             case Preset:
467             case Presets:
468             case NowPlaying:
469             case NowPlayingRateEnabled:
470             case NowPlayingSkipEnabled:
471             case NowPlayingSkipPreviousEnabled:
472             case ContentItem:
473             case UnprocessedNoTextExpected:
474             case Zone:
475             case ZoneUpdated:
476             case Sources:
477                 logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
478                         new String(ch, start, length));
479                 break;
480             case BassMin: // @TODO - find out how to dynamically change "channel-type" bass configuration
481             case BassMax: // based on these values...
482             case BassDefault:
483             case BassTarget:
484             case VolumeTarget:
485                 // this are currently unprocessed values.
486                 break;
487             case BassCapabilities:
488                 logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
489                         new String(ch, start, length));
490                 break;
491             case Unprocessed:
492                 // drop quietly..
493                 break;
494             case BassActual:
495                 commandExecutor.updateBassLevelGUIState(new DecimalType(new String(ch, start, length)));
496                 break;
497             case InfoName:
498                 setConfigOption(DEVICE_INFO_NAME, new String(ch, start, length));
499                 break;
500             case InfoType:
501                 setConfigOption(DEVICE_INFO_TYPE, new String(ch, start, length));
502                 setConfigOption(PROPERTY_MODEL_ID, new String(ch, start, length));
503                 break;
504             case InfoModuleType:
505                 setConfigOption(PROPERTY_HARDWARE_VERSION, new String(ch, start, length));
506                 break;
507             case InfoFirmwareVersion:
508                 String[] fwVersion = new String(ch, start, length).split(" ");
509                 setConfigOption(PROPERTY_FIRMWARE_VERSION, fwVersion[0]);
510                 break;
511             case BassAvailable:
512                 boolean bassAvailable = Boolean.parseBoolean(new String(ch, start, length));
513                 commandExecutor.setBassAvailable(bassAvailable);
514                 break;
515             case NowPlayingAlbum:
516                 updateNowPlayingAlbum(new StringType(new String(ch, start, length)));
517                 break;
518             case NowPlayingArt:
519                 String url = new String(ch, start, length);
520                 if (url.startsWith("http")) {
521                     // We download the cover art in a different thread to not delay the other operations
522                     handler.getScheduler().submit(() -> {
523                         RawType image = HttpUtil.downloadImage(url, true, 500000);
524                         if (image != null) {
525                             updateNowPlayingArtwork(image);
526                         } else {
527                             updateNowPlayingArtwork(UnDefType.UNDEF);
528                         }
529                     });
530                 } else {
531                     updateNowPlayingArtwork(UnDefType.UNDEF);
532                 }
533                 break;
534             case NowPlayingArtist:
535                 updateNowPlayingArtist(new StringType(new String(ch, start, length)));
536                 break;
537             case ContentItemItemName:
538                 contentItem.setItemName(new String(ch, start, length));
539                 break;
540             case ContentItemContainerArt:
541                 contentItem.setContainerArt(new String(ch, start, length));
542                 break;
543             case NowPlayingDescription:
544                 updateNowPlayingDescription(new StringType(new String(ch, start, length)));
545                 break;
546             case NowPlayingGenre:
547                 updateNowPlayingGenre(new StringType(new String(ch, start, length)));
548                 break;
549             case NowPlayingPlayStatus:
550                 String playPauseState = new String(ch, start, length);
551                 if ("PLAY_STATE".equals(playPauseState) || "BUFFERING_STATE".equals(playPauseState)) {
552                     commandExecutor.updatePlayerControlGUIState(PlayPauseType.PLAY);
553                 } else if ("STOP_STATE".equals(playPauseState) || "PAUSE_STATE".equals(playPauseState)) {
554                     commandExecutor.updatePlayerControlGUIState(PlayPauseType.PAUSE);
555                 }
556                 break;
557             case NowPlayingStationLocation:
558                 updateNowPlayingStationLocation(new StringType(new String(ch, start, length)));
559                 break;
560             case NowPlayingStationName:
561                 updateNowPlayingStationName(new StringType(new String(ch, start, length)));
562                 break;
563             case NowPlayingTrack:
564                 updateNowPlayingTrack(new StringType(new String(ch, start, length)));
565                 break;
566             case VolumeActual:
567                 commandExecutor.updateVolumeGUIState(new PercentType(Integer.parseInt(new String(ch, start, length))));
568                 break;
569             case VolumeMuteEnabled:
570                 volumeMuteEnabled = Boolean.parseBoolean(new String(ch, start, length));
571                 commandExecutor.setCurrentMuted(volumeMuteEnabled);
572                 break;
573             case MasterDeviceId:
574                 if (masterDeviceId != null) {
575                     masterDeviceId.macAddress = new String(ch, start, length);
576                 }
577                 break;
578             case GroupName:
579                 if (masterDeviceId != null) {
580                     masterDeviceId.groupName = new String(ch, start, length);
581                 }
582                 break;
583             case DeviceId:
584                 deviceId = new String(ch, start, length);
585                 break;
586             case DeviceIp:
587                 if (masterDeviceId != null && Objects.equals(masterDeviceId.macAddress, deviceId)) {
588                     masterDeviceId.host = new String(ch, start, length);
589                 }
590                 break;
591             default:
592                 // do nothing
593                 break;
594         }
595     }
596
597     @Override
598     public void skippedEntity(String name) throws SAXException {
599         super.skippedEntity(name);
600     }
601
602     private boolean checkDeviceId(String localName, Attributes attributes, boolean allowFromMaster) {
603         String deviceID = attributes.getValue("deviceID");
604         if (deviceID == null) {
605             logger.warn("{}: No device-ID in entity {}", handler.getDeviceName(), localName);
606             return false;
607         }
608         if (deviceID.equals(handler.getMacAddress())) {
609             return true;
610         }
611         logger.warn("{}: Wrong device-ID in entity '{}': Got: '{}', expected: '{}'", handler.getDeviceName(), localName,
612                 deviceID, handler.getMacAddress());
613         return false;
614     }
615
616     private void init() {
617         states = new Stack<>();
618         state = XMLHandlerState.INIT;
619         nowPlayingSource = null;
620     }
621
622     private XMLHandlerState nextState(Map<String, XMLHandlerState> stateMap, XMLHandlerState curState,
623             String localName) {
624         XMLHandlerState state = stateMap.get(localName);
625         if (state == null) {
626             if (logger.isDebugEnabled()) {
627                 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState, localName);
628             }
629             state = XMLHandlerState.Unprocessed;
630         }
631         return state;
632     }
633
634     private void setConfigOption(String option, String value) {
635         Map<String, String> prop = handler.getThing().getProperties();
636         String cur = prop.get(option);
637         if (cur == null || !cur.equals(value)) {
638             logger.debug("{}: Option '{}' updated: From '{}' to '{}'", handler.getDeviceName(), option, cur, value);
639             handler.getThing().setProperty(option, value);
640         }
641     }
642
643     private void updateNowPlayingAlbum(State state) {
644         handler.updateState(CHANNEL_NOWPLAYING_ALBUM, state);
645     }
646
647     private void updateNowPlayingArtwork(State state) {
648         handler.updateState(CHANNEL_NOWPLAYING_ARTWORK, state);
649     }
650
651     private void updateNowPlayingArtist(State state) {
652         handler.updateState(CHANNEL_NOWPLAYING_ARTIST, state);
653     }
654
655     private void updateNowPlayingDescription(State state) {
656         handler.updateState(CHANNEL_NOWPLAYING_DESCRIPTION, state);
657     }
658
659     private void updateNowPlayingGenre(State state) {
660         handler.updateState(CHANNEL_NOWPLAYING_GENRE, state);
661     }
662
663     private void updateNowPlayingItemName(State state) {
664         handler.updateState(CHANNEL_NOWPLAYING_ITEMNAME, state);
665     }
666
667     private void updateNowPlayingStationLocation(State state) {
668         handler.updateState(CHANNEL_NOWPLAYING_STATIONLOCATION, state);
669     }
670
671     private void updateNowPlayingStationName(State state) {
672         handler.updateState(CHANNEL_NOWPLAYING_STATIONNAME, state);
673     }
674
675     private void updateNowPlayingTrack(State state) {
676         handler.updateState(CHANNEL_NOWPLAYING_TRACK, state);
677     }
678
679     private void updateRateEnabled(OnOffType state) {
680         handler.updateState(CHANNEL_RATEENABLED, state);
681     }
682
683     private void updateSkipEnabled(OnOffType state) {
684         handler.updateState(CHANNEL_SKIPENABLED, state);
685     }
686
687     private void updateSkipPreviousEnabled(OnOffType state) {
688         handler.updateState(CHANNEL_SKIPPREVIOUSENABLED, state);
689     }
690 }