2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bosesoundtouch.internal;
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;
20 import java.util.HashMap;
22 import java.util.Objects;
23 import java.util.Stack;
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;
42 * The {@link XMLResponseHandler} class handles the XML communication with the Soundtouch
44 * @author Christian Niessner - Initial contribution
45 * @author Thomas Traunbauer - Initial contribution
46 * @author Kai Kreuzer - code clean up
48 public class XMLResponseHandler extends DefaultHandler {
50 private final Logger logger = LoggerFactory.getLogger(XMLResponseHandler.class);
52 private BoseSoundTouchHandler handler;
53 private CommandExecutor commandExecutor;
55 private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap;
57 private Stack<XMLHandlerState> states;
58 private XMLHandlerState state;
59 private boolean msgHeaderWasValid;
61 private ContentItem contentItem;
62 private boolean volumeMuteEnabled;
63 private OnOffType rateEnabled;
64 private OnOffType skipEnabled;
65 private OnOffType skipPreviousEnabled;
67 private State nowPlayingSource;
69 private BoseSoundTouchConfiguration masterDeviceId;
72 private Map<Integer, ContentItem> playerPresets;
75 * Creates a new instance of this class
78 * @param stateSwitchingMap the stateSwitchingMap is the XMLState Map, that says which Flags are computed
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;
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);
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
97 // warning for unhandled states
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;
105 state = XMLHandlerState.Unprocessed;
108 state = stateMap.get(localName);
110 if (logger.isDebugEnabled()) {
111 logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
114 state = XMLHandlerState.Unprocessed;
119 if ("header".equals(localName)) {
121 if (checkDeviceId(localName, attributes, false)) {
122 state = XMLHandlerState.MsgHeader;
123 msgHeaderWasValid = true;
125 state = XMLHandlerState.Unprocessed;
127 } else if ("body".equals(localName)) {
128 if (msgHeaderWasValid) {
129 state = XMLHandlerState.MsgBody;
131 state = XMLHandlerState.Unprocessed;
134 if (logger.isDebugEnabled()) {
135 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
138 state = XMLHandlerState.Unprocessed;
142 if ("request".equals(localName)) {
143 state = XMLHandlerState.Unprocessed; // TODO implement request id / response tracking...
145 if (logger.isDebugEnabled()) {
146 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
149 state = XMLHandlerState.Unprocessed;
153 if ("nowPlaying".equals(localName)) {
155 * if (!checkDeviceId(localName, attributes, true)) {
156 * state = XMLHandlerState.Unprocessed;
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)) {
167 nowPlayingSource = new StringType(source);
168 // reset enabled states
169 updateRateEnabled(OnOffType.OFF);
170 updateSkipEnabled(OnOffType.OFF);
171 updateSkipPreviousEnabled(OnOffType.OFF);
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);
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);
192 state = XMLHandlerState.Presets;
193 } else if ("group".equals(localName)) {
194 this.masterDeviceId = new BoseSoundTouchConfiguration();
195 state = stateMap.get(localName);
197 state = stateMap.get(localName);
199 if (logger.isDebugEnabled()) {
200 logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
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;
214 if ("preset".equals(localName)) {
215 state = XMLHandlerState.Preset;
216 String id = attributes.getValue("id");
217 if (contentItem == null) {
218 contentItem = new ContentItem();
220 contentItem.setPresetID(Integer.parseInt(id));
222 if (logger.isDebugEnabled()) {
223 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
226 state = XMLHandlerState.Unprocessed;
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);
240 if (sourceAccount.equals("AUX1")) {
241 commandExecutor.setAUX1Available(true);
243 if (sourceAccount.equals("AUX2")) {
244 commandExecutor.setAUX2Available(true);
246 if (sourceAccount.equals("AUX3")) {
247 commandExecutor.setAUX3Available(true);
250 if (source.equals("STORED_MUSIC")) {
251 commandExecutor.setStoredMusicAvailable(true);
253 if (source.equals("INTERNET_RADIO")) {
254 commandExecutor.setInternetRadioAvailable(true);
256 if (source.equals("BLUETOOTH")) {
257 commandExecutor.setBluetoothAvailable(true);
259 if (source.equals("PRODUCT")) {
260 if (sourceAccount.equals("TV")) {
261 commandExecutor.setTVAvailable(true);
263 if (sourceAccount.equals("HDMI_1")) {
264 commandExecutor.setHDMI1Available(true);
269 if (logger.isDebugEnabled()) {
270 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
273 state = XMLHandlerState.Unprocessed;
276 // auto go trough the state map
292 state = nextState(stateMap, curState, localName);
294 case BassCapabilities:
295 state = nextState(stateMap, curState, localName);
297 // all entities without any children expected..
304 case ContentItemItemName:
305 case ContentItemContainerArt:
308 case InfoFirmwareVersion:
310 case NowPlayingAlbum:
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:
325 case VolumeMuteEnabled:
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,
332 state = XMLHandlerState.Unprocessed;
335 if (logger.isDebugEnabled()) {
336 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
339 state = XMLHandlerState.Unprocessed;
342 // all further things are also unprocessed
343 state = XMLHandlerState.Unprocessed;
345 case UnprocessedNoTextExpected:
346 state = XMLHandlerState.UnprocessedNoTextExpected;
349 if (state == XMLHandlerState.ContentItem) {
350 if (contentItem == null) {
351 contentItem = new ContentItem();
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)) {
362 if ("location".equalsIgnoreCase(attrName)) {
365 if ("sourceAccount".equalsIgnoreCase(attrName)) {
368 if ("isPresetable".equalsIgnoreCase(attrName)) {
371 contentItem.setAdditionalAttribute(attrName, attributes.getValue(attrId));
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();
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);
394 if (state == XMLHandlerState.NowPlaying) {
395 // update now playing name...
396 updateNowPlayingItemName(new StringType(contentItem.getItemName()));
397 commandExecutor.setCurrentContentItem(contentItem);
401 if (state == XMLHandlerState.Presets) {
402 playerPresets.put(contentItem.getPresetID(), contentItem);
407 if (state == XMLHandlerState.MsgBody) {
408 updateRateEnabled(rateEnabled);
409 updateSkipEnabled(skipEnabled);
410 updateSkipPreviousEnabled(skipPreviousEnabled);
413 // handle special tags..
415 // request current bass level
416 commandExecutor.getInformations(APIRequest.BASS);
419 commandExecutor.getInformations(APIRequest.VOLUME);
421 case NowPlayingRateEnabled:
422 rateEnabled = OnOffType.ON;
424 case NowPlayingSkipEnabled:
425 skipEnabled = OnOffType.ON;
427 case NowPlayingSkipPreviousEnabled:
428 skipPreviousEnabled = OnOffType.ON;
431 OnOffType muted = volumeMuteEnabled ? OnOffType.ON : OnOffType.OFF;
432 commandExecutor.setCurrentMuted(volumeMuteEnabled);
433 commandExecutor.postVolumeMuted(muted);
436 commandExecutor.getInformations(APIRequest.GET_ZONE);
439 commandExecutor.updatePresetContainerFromPlayer(playerPresets);
440 playerPresets = null;
443 handler.handleGroupUpdated(masterDeviceId);
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);
469 case NowPlayingRateEnabled:
470 case NowPlayingSkipEnabled:
471 case NowPlayingSkipPreviousEnabled:
473 case UnprocessedNoTextExpected:
477 logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
478 new String(ch, start, length));
480 case BassMin: // @TODO - find out how to dynamically change "channel-type" bass configuration
481 case BassMax: // based on these values...
485 // this are currently unprocessed values.
487 case BassCapabilities:
488 logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
489 new String(ch, start, length));
495 commandExecutor.updateBassLevelGUIState(new DecimalType(new String(ch, start, length)));
498 setConfigOption(DEVICE_INFO_NAME, new String(ch, start, length));
501 setConfigOption(DEVICE_INFO_TYPE, new String(ch, start, length));
502 setConfigOption(PROPERTY_MODEL_ID, new String(ch, start, length));
505 setConfigOption(PROPERTY_HARDWARE_VERSION, new String(ch, start, length));
507 case InfoFirmwareVersion:
508 String[] fwVersion = new String(ch, start, length).split(" ");
509 setConfigOption(PROPERTY_FIRMWARE_VERSION, fwVersion[0]);
512 boolean bassAvailable = Boolean.parseBoolean(new String(ch, start, length));
513 commandExecutor.setBassAvailable(bassAvailable);
515 case NowPlayingAlbum:
516 updateNowPlayingAlbum(new StringType(new String(ch, start, length)));
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);
525 updateNowPlayingArtwork(image);
527 updateNowPlayingArtwork(UnDefType.UNDEF);
531 updateNowPlayingArtwork(UnDefType.UNDEF);
534 case NowPlayingArtist:
535 updateNowPlayingArtist(new StringType(new String(ch, start, length)));
537 case ContentItemItemName:
538 contentItem.setItemName(new String(ch, start, length));
540 case ContentItemContainerArt:
541 contentItem.setContainerArt(new String(ch, start, length));
543 case NowPlayingDescription:
544 updateNowPlayingDescription(new StringType(new String(ch, start, length)));
546 case NowPlayingGenre:
547 updateNowPlayingGenre(new StringType(new String(ch, start, length)));
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);
557 case NowPlayingStationLocation:
558 updateNowPlayingStationLocation(new StringType(new String(ch, start, length)));
560 case NowPlayingStationName:
561 updateNowPlayingStationName(new StringType(new String(ch, start, length)));
563 case NowPlayingTrack:
564 updateNowPlayingTrack(new StringType(new String(ch, start, length)));
567 commandExecutor.updateVolumeGUIState(new PercentType(Integer.parseInt(new String(ch, start, length))));
569 case VolumeMuteEnabled:
570 volumeMuteEnabled = Boolean.parseBoolean(new String(ch, start, length));
571 commandExecutor.setCurrentMuted(volumeMuteEnabled);
574 if (masterDeviceId != null) {
575 masterDeviceId.macAddress = new String(ch, start, length);
579 if (masterDeviceId != null) {
580 masterDeviceId.groupName = new String(ch, start, length);
584 deviceId = new String(ch, start, length);
587 if (masterDeviceId != null && Objects.equals(masterDeviceId.macAddress, deviceId)) {
588 masterDeviceId.host = new String(ch, start, length);
598 public void skippedEntity(String name) throws SAXException {
599 super.skippedEntity(name);
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);
608 if (deviceID.equals(handler.getMacAddress())) {
611 logger.warn("{}: Wrong device-ID in entity '{}': Got: '{}', expected: '{}'", handler.getDeviceName(), localName,
612 deviceID, handler.getMacAddress());
616 private void init() {
617 states = new Stack<>();
618 state = XMLHandlerState.INIT;
619 nowPlayingSource = null;
622 private XMLHandlerState nextState(Map<String, XMLHandlerState> stateMap, XMLHandlerState curState,
624 XMLHandlerState state = stateMap.get(localName);
626 if (logger.isDebugEnabled()) {
627 logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState, localName);
629 state = XMLHandlerState.Unprocessed;
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);
643 private void updateNowPlayingAlbum(State state) {
644 handler.updateState(CHANNEL_NOWPLAYING_ALBUM, state);
647 private void updateNowPlayingArtwork(State state) {
648 handler.updateState(CHANNEL_NOWPLAYING_ARTWORK, state);
651 private void updateNowPlayingArtist(State state) {
652 handler.updateState(CHANNEL_NOWPLAYING_ARTIST, state);
655 private void updateNowPlayingDescription(State state) {
656 handler.updateState(CHANNEL_NOWPLAYING_DESCRIPTION, state);
659 private void updateNowPlayingGenre(State state) {
660 handler.updateState(CHANNEL_NOWPLAYING_GENRE, state);
663 private void updateNowPlayingItemName(State state) {
664 handler.updateState(CHANNEL_NOWPLAYING_ITEMNAME, state);
667 private void updateNowPlayingStationLocation(State state) {
668 handler.updateState(CHANNEL_NOWPLAYING_STATIONLOCATION, state);
671 private void updateNowPlayingStationName(State state) {
672 handler.updateState(CHANNEL_NOWPLAYING_STATIONNAME, state);
675 private void updateNowPlayingTrack(State state) {
676 handler.updateState(CHANNEL_NOWPLAYING_TRACK, state);
679 private void updateRateEnabled(OnOffType state) {
680 handler.updateState(CHANNEL_RATEENABLED, state);
683 private void updateSkipEnabled(OnOffType state) {
684 handler.updateState(CHANNEL_SKIPENABLED, state);
687 private void updateSkipPreviousEnabled(OnOffType state) {
688 handler.updateState(CHANNEL_SKIPPREVIOUSENABLED, state);