[sonos] Fix warnings (#15708)

* Fix warnings
* Fix discovery error logging for offline devices

Fixes #6793

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2024-06-09 15:10:49 +02:00 committed by GitHub
parent 7516ca557d
commit ed9fbf0731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 149 additions and 126 deletions

View File

@ -22,7 +22,6 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sonos.internal.handler.ZonePlayerHandler;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioSinkSync;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FileAudioStream;
@ -39,7 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This makes a Sonos speaker to serve as an {@link AudioSink}-
* This makes a Sonos speaker to serve as an {@link org.openhab.core.audio.AudioSink}-
*
* @author Kai Kreuzer - Initial contribution and API
* @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException

View File

@ -25,6 +25,10 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.util.StringUtils;
@ -33,9 +37,7 @@ import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* The {@link SonosXMLParser} is a class of helper functions
@ -48,15 +50,18 @@ public class SonosXMLParser {
static final Logger LOGGER = LoggerFactory.getLogger(SonosXMLParser.class);
private static final MessageFormat METADATA_FORMAT = new MessageFormat(
"<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
+ "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
+ "xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" "
+ "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"
+ "<item id=\"{0}\" parentID=\"{1}\" restricted=\"true\">" + "<dc:title>{2}</dc:title>"
+ "<upnp:class>{3}</upnp:class>"
+ "<desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">" + "{4}</desc>"
+ "</item></DIDL-Lite>");
private static final String METADATA_FORMAT_PATTERN = """
<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" \
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" \
xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" \
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">\
<item id="{0}" parentID="{1}" restricted="true">\
<dc:title>{2}</dc:title>\
<upnp:class>{3}</upnp:class>\
<desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">{4}</desc>\
</item>\
</DIDL-Lite>\
""";
private enum Element {
TITLE,
@ -90,13 +95,11 @@ public class SonosXMLParser {
public static List<SonosAlarm> getAlarmsFromStringResult(String xml) {
AlarmHandler handler = new AlarmHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
LOGGER.error("Could not parse Alarms from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse Alarms from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse Alarms from string '{}'", xml);
}
return handler.getAlarms();
}
@ -108,13 +111,11 @@ public class SonosXMLParser {
public static List<SonosEntry> getEntriesFromString(String xml) {
EntryHandler handler = new EntryHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
LOGGER.error("Could not parse Entries from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse Entries from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse Entries from string '{}'", xml);
}
return handler.getArtists();
@ -127,18 +128,18 @@ public class SonosXMLParser {
* @param xml
* @return The value of the desc xml tag
* @throws SAXException
* @throws ParserConfigurationException
*/
public static @Nullable SonosResourceMetaData getResourceMetaData(String xml) throws SAXException {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
public static @Nullable SonosResourceMetaData getResourceMetaData(String xml)
throws SAXException, ParserConfigurationException {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
SAXParser saxParser = factory.newSAXParser();
ResourceMetaDataHandler handler = new ResourceMetaDataHandler();
reader.setContentHandler(handler);
try {
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
LOGGER.error("Could not parse Resource MetaData from String '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse Resource MetaData from string '{}'", xml);
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException e) {
LOGGER.warn("Could not parse Resource MetaData from string '{}'", xml);
}
return handler.getMetaData();
}
@ -150,14 +151,11 @@ public class SonosXMLParser {
public static List<SonosZoneGroup> getZoneGroupFromXML(String xml) {
ZoneGroupHandler handler = new ZoneGroupHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
// This should never happen - we're not performing I/O!
LOGGER.error("Could not parse ZoneGroup from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse ZoneGroup from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse ZoneGroup from string '{}'", xml);
}
return handler.getGroups();
@ -166,14 +164,11 @@ public class SonosXMLParser {
public static List<String> getRadioTimeFromXML(String xml) {
OpmlHandler handler = new OpmlHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
// This should never happen - we're not performing I/O!
LOGGER.error("Could not parse RadioTime from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse RadioTime from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse RadioTime from string '{}'", xml);
}
return handler.getTextFields();
@ -182,14 +177,11 @@ public class SonosXMLParser {
public static Map<String, String> getRenderingControlFromXML(String xml) {
RenderingControlEventHandler handler = new RenderingControlEventHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
// This should never happen - we're not performing I/O!
LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse Rendering Control from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse Rendering Control from string '{}'", xml);
}
return handler.getChanges();
}
@ -197,14 +189,11 @@ public class SonosXMLParser {
public static Map<String, String> getAVTransportFromXML(String xml) {
AVTransportEventHandler handler = new AVTransportEventHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
// This should never happen - we're not performing I/O!
LOGGER.error("Could not parse AV Transport from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse AV Transport from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse AV Transport from string '{}'", xml);
}
return handler.getChanges();
}
@ -212,14 +201,11 @@ public class SonosXMLParser {
public static SonosMetaData getMetaDataFromXML(String xml) {
MetaDataHandler handler = new MetaDataHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
// This should never happen - we're not performing I/O!
LOGGER.error("Could not parse MetaData from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse MetaData from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse MetaData from string '{}'", xml);
}
return handler.getMetaData();
@ -228,14 +214,11 @@ public class SonosXMLParser {
public static List<SonosMusicService> getMusicServicesFromXML(String xml) {
MusicServiceHandler handler = new MusicServiceHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(handler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
// This should never happen - we're not performing I/O!
LOGGER.error("Could not parse music services from string '{}'", xml);
} catch (SAXException s) {
LOGGER.error("Could not parse music services from string '{}'", xml);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(new StringReader(xml)), handler);
} catch (IOException | SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse music services from string '{}'", xml);
}
return handler.getServices();
}
@ -305,14 +288,14 @@ public class SonosXMLParser {
if (curIgnore == null) {
curIgnore = new ArrayList<>();
curIgnore.add("DIDL-Lite");
curIgnore.add("type");
curIgnore.add("ordinal");
curIgnore.add("description");
curIgnore.add("r:type");
curIgnore.add("r:ordinal");
curIgnore.add("r:description");
ignore = curIgnore;
}
if (!curIgnore.contains(localName)) {
LOGGER.debug("Did not recognise element named {}", localName);
if (!curIgnore.contains(qName)) {
LOGGER.debug("Did not recognise element named {}", qName);
}
element = null;
break;
@ -373,7 +356,7 @@ public class SonosXMLParser {
if (!desc.toString().isEmpty()) {
try {
md = getResourceMetaData(desc.toString());
} catch (SAXException ignore) {
} catch (SAXException | ParserConfigurationException ignore) {
LOGGER.debug("Failed to parse embeded", ignore);
}
}
@ -715,13 +698,14 @@ public class SonosXMLParser {
* The events are all of the form <qName val="value"/> so we can get all
* the info we need from here.
*/
if (localName == null) {
// this means that localName isn't defined in EventType, which is expected for some elements
LOGGER.info("{} is not defined in EventType. ", localName);
if (qName == null) {
// this means that qName isn't defined in EventType, which is expected for some elements
LOGGER.info("{} is not defined in EventType. ", qName);
} else {
String val = attributes == null ? null : attributes.getValue("val");
if (val != null) {
changes.put(localName, val);
String key = qName.contains(":") ? qName.split(":")[1] : qName;
changes.put(key, val);
}
}
}
@ -749,7 +733,7 @@ public class SonosXMLParser {
@Override
public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
@Nullable Attributes attributes) throws SAXException {
String name = localName == null ? "" : localName;
String name = qName == null ? "" : qName;
switch (name) {
case "item":
currentElement = CurrentElement.item;
@ -761,25 +745,25 @@ public class SonosXMLParser {
case "res":
currentElement = CurrentElement.res;
break;
case "streamContent":
case "r:streamContent":
currentElement = CurrentElement.streamContent;
break;
case "albumArtURI":
case "upnp:albumArtURI":
currentElement = CurrentElement.albumArtURI;
break;
case "title":
case "dc:title":
currentElement = CurrentElement.title;
break;
case "class":
case "upnp:class":
currentElement = CurrentElement.upnpClass;
break;
case "creator":
case "dc:creator":
currentElement = CurrentElement.creator;
break;
case "album":
case "upnp:album":
currentElement = CurrentElement.album;
break;
case "albumArtist":
case "r:albumArtist":
currentElement = CurrentElement.albumArtist;
break;
default:
@ -928,15 +912,16 @@ public class SonosXMLParser {
}
}
public static @Nullable String getRoomName(String descriptorXML) {
public static @Nullable String getRoomName(URL descriptorURL) {
RoomNameHandler roomNameHandler = new RoomNameHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(roomNameHandler);
URL url = new URL(descriptorXML);
reader.parse(new InputSource(url.openStream()));
} catch (IOException | SAXException e) {
LOGGER.error("Could not parse Sonos room name from string '{}'", descriptorXML);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(descriptorURL.openStream()), roomNameHandler);
} catch (SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse Sonos room name from URL '{}'", descriptorURL);
} catch (IOException e) {
LOGGER.debug("Could not fetch descriptor XML from URL '{}': {}", descriptorURL, e.getMessage());
}
return roomNameHandler.getRoomName();
}
@ -949,7 +934,7 @@ public class SonosXMLParser {
@Override
public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
@Nullable Attributes attributes) throws SAXException {
if ("roomName".equalsIgnoreCase(localName)) {
if ("roomName".equalsIgnoreCase(qName)) {
roomNameTag = true;
}
}
@ -970,12 +955,13 @@ public class SonosXMLParser {
public static @Nullable String parseModelDescription(URL descriptorURL) {
ModelNameHandler modelNameHandler = new ModelNameHandler();
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(modelNameHandler);
URL url = new URL(descriptorURL.toString());
reader.parse(new InputSource(url.openStream()));
} catch (IOException | SAXException e) {
LOGGER.error("Could not parse Sonos model name from string '{}'", descriptorURL.toString());
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new InputSource(descriptorURL.openStream()), modelNameHandler);
} catch (SAXException | ParserConfigurationException e) {
LOGGER.warn("Could not parse Sonos model name from URL '{}'", descriptorURL);
} catch (IOException e) {
LOGGER.debug("Could not fetch descriptor XML from URL '{}': {}", descriptorURL, e.getMessage());
}
return modelNameHandler.getModelName();
}
@ -988,7 +974,7 @@ public class SonosXMLParser {
@Override
public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
@Nullable Attributes attributes) throws SAXException {
if ("modelName".equalsIgnoreCase(localName)) {
if ("modelName".equalsIgnoreCase(qName)) {
modelNameTag = true;
}
}
@ -1079,8 +1065,6 @@ public class SonosXMLParser {
title = StringUtils.escapeXml(title);
String metadata = METADATA_FORMAT.format(new Object[] { id, parentId, title, upnpClass, desc });
return metadata;
return new MessageFormat(METADATA_FORMAT_PATTERN).format(new Object[] { id, parentId, title, upnpClass, desc });
}
}

View File

@ -108,6 +108,6 @@ public class ZonePlayerDiscoveryParticipant implements UpnpDiscoveryParticipant
}
private @Nullable String getSonosRoomName(RemoteDevice device) {
return SonosXMLParser.getRoomName(device.getIdentity().getDescriptorURL().toString());
return SonosXMLParser.getRoomName(device.getIdentity().getDescriptorURL());
}
}

View File

@ -1689,11 +1689,12 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
*/
protected void saveState() {
synchronized (stateLock) {
savedState = new SonosZonePlayerState();
String currentURI = getCurrentURI();
SonosZonePlayerState savedState = new SonosZonePlayerState();
savedState.transportState = getTransportState();
savedState.volume = getVolume();
this.savedState = savedState;
String currentURI = getCurrentURI();
if (currentURI != null) {
if (isPlayingStreamOrRadio(currentURI)) {
@ -2523,7 +2524,6 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
}
}
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public boolean publicAddress(LineInType lineInType) {
// check if sourcePlayer has a line-in connected
if ((lineInType != LineInType.DIGITAL && isAnalogLineInConnected())
@ -2536,7 +2536,7 @@ public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOPartici
for (String player : group.getMembers()) {
try {
ZonePlayerHandler somePlayer = getHandlerByName(player);
if (somePlayer != this) {
if (!somePlayer.equals(this)) {
somePlayer.becomeStandAlonePlayer();
somePlayer.stop();
addMember(StringType.valueOf(somePlayer.getUDN()));

View File

@ -79,4 +79,43 @@ public class SonosXMLParserTest {
assertEquals("Paris, France", result.get(2));
}
}
@Test
public void getMetaDataFromXML() throws IOException {
InputStream resourceStream = getClass().getResourceAsStream("/MetaData.xml");
assertNotNull(resourceStream);
final String xml = new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8);
SonosMetaData sonosMetaData = SonosXMLParser.getMetaDataFromXML(xml);
assertEquals("-1", sonosMetaData.getId());
assertEquals("-1", sonosMetaData.getParentId());
assertEquals("Turn Down for What - Single", sonosMetaData.getAlbum());
assertEquals("DJ Snake & Lil Jon", sonosMetaData.getCreator());
assertEquals("Turn Down for What", sonosMetaData.getTitle());
assertEquals("object.item.audioItem.musicTrack", sonosMetaData.getUpnpClass());
assertEquals("x-sonosapi-hls-static:librarytrack%3ai.eoD8VQ5SZOB8QX7?sid=204&flags=8232&sn=9",
sonosMetaData.getResource());
assertEquals(
"/getaa?s=1&u=x-sonosapi-hls-static%3alibrarytrack%253ai.eoD8VQ5SZOB8QX7%3fsid%3d204%26flags%3d8232%26sn%3d9",
sonosMetaData.getAlbumArtUri());
}
@Test
public void compileMetadataString() {
SonosEntry sonosEntry = new SonosEntry("1", "Can't Buy Me Love", "0", "A Hard Day's Night", "", "",
"object.item.audioItem.musicTrack", "");
String expected = """
<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" \
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" \
xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" \
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">\
<item id="1" parentID="0" restricted="true">\
<dc:title>Can&apos;t Buy Me Love</dc:title>\
<upnp:class>object.item.audioItem.musicTrack</upnp:class>\
<desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc>\
</item>\
</DIDL-Lite>\
""";
String actual = SonosXMLParser.compileMetadataString(sonosEntry);
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1 @@
<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="-1" parentID="-1" restricted="true"><res protocolInfo="sonos.com-http:*:application/x-mpegURL:*" duration="0:03:33">x-sonosapi-hls-static:librarytrack%3ai.eoD8VQ5SZOB8QX7?sid=204&amp;flags=8232&amp;sn=9</res><r:streamContent></r:streamContent><r:radioShowMd></r:radioShowMd><r:streamInfo>bd:16,sr:22050,c:3,l:0,d:0</r:streamInfo><upnp:albumArtURI>/getaa?s=1&amp;u=x-sonosapi-hls-static%3alibrarytrack%253ai.eoD8VQ5SZOB8QX7%3fsid%3d204%26flags%3d8232%26sn%3d9</upnp:albumArtURI><dc:title>Turn Down for What</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:creator>DJ Snake &amp; Lil Jon</dc:creator><upnp:album>Turn Down for What - Single</upnp:album></item></DIDL-Lite>