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