[bosesoundtouch] Improve SAT errors and remove dependency (#13842)

Signed-off-by: Leo Siepel <leosiepel@gmail.com>
This commit is contained in:
lsiepel 2022-12-10 09:42:09 +01:00 committed by GitHub
parent 92310fab2f
commit db86d291da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 473 additions and 346 deletions

View File

@ -12,11 +12,15 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link APIRequest} class handles the API requests * The {@link APIRequest} class handles the API requests
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public enum APIRequest { public enum APIRequest {
KEY("key"), KEY("key"),
SELECT("select"), SELECT("select"),

View File

@ -12,11 +12,15 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link AvailableSources} is used to find out, which sources and functions are available * The {@link AvailableSources} is used to find out, which sources and functions are available
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public interface AvailableSources { public interface AvailableSources {
public boolean isBluetoothAvailable(); public boolean isBluetoothAvailable();

View File

@ -19,6 +19,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
/** /**
@ -28,6 +29,7 @@ import org.openhab.core.thing.ThingTypeUID;
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class BoseSoundTouchBindingConstants { public class BoseSoundTouchBindingConstants {
public static final String BINDING_ID = "bosesoundtouch"; public static final String BINDING_ID = "bosesoundtouch";

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
/** /**
@ -19,6 +21,7 @@ import org.openhab.core.thing.Thing;
* *
* @author Ivaylo Ivanov - Initial contribution * @author Ivaylo Ivanov - Initial contribution
*/ */
@NonNullByDefault
public class BoseSoundTouchConfiguration { public class BoseSoundTouchConfiguration {
// Device configuration parameters; // Device configuration parameters;
@ -26,10 +29,10 @@ public class BoseSoundTouchConfiguration {
public static final String MAC_ADDRESS = Thing.PROPERTY_MAC_ADDRESS; public static final String MAC_ADDRESS = Thing.PROPERTY_MAC_ADDRESS;
public static final String APP_KEY = "appKey"; public static final String APP_KEY = "appKey";
public String host; public @Nullable String host;
public String macAddress; public @Nullable String macAddress;
public String appKey; public @Nullable String appKey;
// Not an actual configuration field, but it will contain the name of the group (in case of Stereo Pair) // Not an actual configuration field, but it will contain the name of the group (in case of Stereo Pair)
public String groupName; public String groupName = "";
} }

View File

@ -14,6 +14,8 @@ package org.openhab.binding.bosesoundtouch.internal;
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.SUPPORTED_THING_TYPES_UIDS; import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.SUPPORTED_THING_TYPES_UIDS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler; import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.storage.Storage; import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService; import org.openhab.core.storage.StorageService;
@ -31,11 +33,12 @@ import org.osgi.service.component.annotations.Reference;
* *
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
*/ */
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bosesoundtouch") @Component(service = ThingHandlerFactory.class, configurationPid = "binding.bosesoundtouch")
public class BoseSoundTouchHandlerFactory extends BaseThingHandlerFactory { public class BoseSoundTouchHandlerFactory extends BaseThingHandlerFactory {
private StorageService storageService; private @Nullable StorageService storageService;
private BoseStateDescriptionOptionProvider stateOptionProvider; private @Nullable BoseStateDescriptionOptionProvider stateOptionProvider;
@Override @Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) { public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@ -43,13 +46,20 @@ public class BoseSoundTouchHandlerFactory extends BaseThingHandlerFactory {
} }
@Override @Override
protected ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
Storage<ContentItem> storage = storageService.getStorage(thing.getUID().toString(), StorageService localStorageService = storageService;
if (localStorageService != null) {
Storage<ContentItem> storage = localStorageService.getStorage(thing.getUID().toString(),
ContentItem.class.getClassLoader()); ContentItem.class.getClassLoader());
BoseStateDescriptionOptionProvider localDescriptionOptionProvider = stateOptionProvider;
if (localDescriptionOptionProvider != null) {
BoseSoundTouchHandler handler = new BoseSoundTouchHandler(thing, new PresetContainer(storage), BoseSoundTouchHandler handler = new BoseSoundTouchHandler(thing, new PresetContainer(storage),
stateOptionProvider); localDescriptionOptionProvider);
return handler; return handler;
} }
}
return null;
}
@Reference @Reference
protected void setStorageService(StorageService storageService) { protected void setStorageService(StorageService storageService) {

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link BoseSoundTouchNotFoundException} class is an exception * The {@link BoseSoundTouchNotFoundException} class is an exception
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class BoseSoundTouchNotFoundException extends Exception { public class BoseSoundTouchNotFoundException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,11 +12,15 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* Configuration class for soundtouch notification channel * Configuration class for soundtouch notification channel
* *
* @author Ivaylo Ivanov - Initial contribution * @author Ivaylo Ivanov - Initial contribution
*/ */
@NonNullByDefault
public class BoseSoundTouchNotificationChannelConfiguration { public class BoseSoundTouchNotificationChannelConfiguration {
public static final String MIN_FIRMWARE = "14"; public static final String MIN_FIRMWARE = "14";
@ -27,13 +31,13 @@ public class BoseSoundTouchNotificationChannelConfiguration {
public static final String NOTIFICATION_REASON = "notificationReason"; public static final String NOTIFICATION_REASON = "notificationReason";
public static final String NOTIFICATION_MESSAGE = "notificationMessage"; public static final String NOTIFICATION_MESSAGE = "notificationMessage";
public Integer notificationVolume; public @Nullable Integer notificationVolume;
public String notificationService; public @Nullable String notificationService;
public String notificationReason; public @Nullable String notificationReason;
public String notificationMessage; public @Nullable String notificationMessage;
public static boolean isSupportedFirmware(String firmware) { public static boolean isSupportedFirmware(String firmware) {
return firmware != null && firmware.compareTo(MIN_FIRMWARE) > 0; return firmware.compareTo(MIN_FIRMWARE) > 0;
} }
public static boolean isSupportedHardware(String hardware) { public static boolean isSupportedHardware(String hardware) {

View File

@ -18,6 +18,9 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.Session;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler; import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType; import org.openhab.core.library.types.NextPreviousType;
@ -36,16 +39,17 @@ import org.slf4j.LoggerFactory;
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
* @author Kai Kreuzer - code clean up * @author Kai Kreuzer - code clean up
*/ */
@NonNullByDefault
public class CommandExecutor implements AvailableSources { public class CommandExecutor implements AvailableSources {
private final Logger logger = LoggerFactory.getLogger(CommandExecutor.class); private final Logger logger = LoggerFactory.getLogger(CommandExecutor.class);
private final BoseSoundTouchHandler handler; private final BoseSoundTouchHandler handler;
private boolean currentMuted; private boolean currentMuted;
private ContentItem currentContentItem; private @Nullable ContentItem currentContentItem = null;
private OperationModeType currentOperationMode; private @Nullable OperationModeType currentOperationMode;
private Map<String, Boolean> mapOfAvailableFunctions; private final Map<String, Boolean> mapOfAvailableFunctions = new HashMap<>();
/** /**
* Creates a new instance of this class * Creates a new instance of this class
@ -54,7 +58,8 @@ public class CommandExecutor implements AvailableSources {
*/ */
public CommandExecutor(BoseSoundTouchHandler handler) { public CommandExecutor(BoseSoundTouchHandler handler) {
this.handler = handler; this.handler = handler;
init(); getInformations(APIRequest.INFO);
currentOperationMode = OperationModeType.OFFLINE;
} }
/** /**
@ -66,11 +71,7 @@ public class CommandExecutor implements AvailableSources {
public void updatePresetContainerFromPlayer(Map<Integer, ContentItem> playerPresets) { public void updatePresetContainerFromPlayer(Map<Integer, ContentItem> playerPresets) {
playerPresets.forEach((k, v) -> { playerPresets.forEach((k, v) -> {
try { try {
if (v != null) {
handler.getPresetContainer().put(k, v); handler.getPresetContainer().put(k, v);
} else {
handler.getPresetContainer().remove(k);
}
} catch (ContentItemNotPresetableException e) { } catch (ContentItemNotPresetableException e) {
logger.debug("{}: ContentItem is not presetable", handler.getDeviceName()); logger.debug("{}: ContentItem is not presetable", handler.getDeviceName());
} }
@ -104,7 +105,10 @@ public class CommandExecutor implements AvailableSources {
*/ */
public void addCurrentContentItemToPresetContainer(DecimalType command) { public void addCurrentContentItemToPresetContainer(DecimalType command) {
if (command.intValue() > 6) { if (command.intValue() > 6) {
addContentItemToPresetContainer(command.intValue(), currentContentItem); ContentItem localContentItem = currentContentItem;
if (localContentItem != null) {
addContentItemToPresetContainer(command.intValue(), localContentItem);
}
} else { } else {
logger.warn("{}: Only PresetID >6 is allowed", handler.getDeviceName()); logger.warn("{}: Only PresetID >6 is allowed", handler.getDeviceName());
} }
@ -118,9 +122,12 @@ public class CommandExecutor implements AvailableSources {
public void getInformations(APIRequest apiRequest) { public void getInformations(APIRequest apiRequest) {
String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + apiRequest String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + apiRequest
+ "\" method=\"GET\"><request requestID=\"0\"><info type=\"new\"/></request></header></msg>"; + "\" method=\"GET\"><request requestID=\"0\"><info type=\"new\"/></request></header></msg>";
handler.getSession().getRemote().sendStringByFuture(msg); Session localSession = handler.getSession();
if (localSession != null) {
localSession.getRemote().sendStringByFuture(msg);
logger.debug("{}: sending request: {}", handler.getDeviceName(), msg); logger.debug("{}: sending request: {}", handler.getDeviceName(), msg);
} }
}
/** /**
* Sets the current ContentItem if it is valid, and inits an update of the operating values * Sets the current ContentItem if it is valid, and inits an update of the operating values
@ -128,17 +135,19 @@ public class CommandExecutor implements AvailableSources {
* @param contentItem * @param contentItem
*/ */
public void setCurrentContentItem(ContentItem contentItem) { public void setCurrentContentItem(ContentItem contentItem) {
if ((contentItem != null) && (contentItem.isValid())) { if (contentItem.isValid()) {
ContentItem psFound = null; ContentItem psFound = null;
if (handler.getPresetContainer() != null) {
Collection<ContentItem> listOfPresets = handler.getPresetContainer().getAllPresets(); Collection<ContentItem> listOfPresets = handler.getPresetContainer().getAllPresets();
for (ContentItem ps : listOfPresets) { for (ContentItem ps : listOfPresets) {
if (ps.isPresetable()) { if (ps.isPresetable()) {
if (ps.getLocation().equals(contentItem.getLocation())) { String localLocation = ps.getLocation();
if (localLocation != null) {
if (localLocation.equals(contentItem.getLocation())) {
psFound = ps; psFound = ps;
} }
} }
} }
}
int presetID = 0; int presetID = 0;
if (psFound != null) { if (psFound != null) {
presetID = psFound.getPresetID(); presetID = psFound.getPresetID();
@ -146,7 +155,7 @@ public class CommandExecutor implements AvailableSources {
contentItem.setPresetID(presetID); contentItem.setPresetID(presetID);
currentContentItem = contentItem; currentContentItem = contentItem;
}
} }
updateOperatingValues(); updateOperatingValues();
} }
@ -350,20 +359,10 @@ public class CommandExecutor implements AvailableSources {
handler.updateState(CHANNEL_PRESET, state); handler.updateState(CHANNEL_PRESET, state);
} }
private void init() {
getInformations(APIRequest.INFO);
currentOperationMode = OperationModeType.OFFLINE;
currentContentItem = null;
mapOfAvailableFunctions = new HashMap<>();
}
private void postContentItem(ContentItem contentItem) { private void postContentItem(ContentItem contentItem) {
if (contentItem != null) {
setCurrentContentItem(contentItem); setCurrentContentItem(contentItem);
sendPostRequestInWebSocket("select", "", contentItem.generateXML()); sendPostRequestInWebSocket("select", "", contentItem.generateXML());
} }
}
private void sendPostRequestInWebSocket(String url, String postData) { private void sendPostRequestInWebSocket(String url, String postData) {
sendPostRequestInWebSocket(url, "", postData); sendPostRequestInWebSocket(url, "", postData);
@ -374,19 +373,21 @@ public class CommandExecutor implements AvailableSources {
String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + url String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + url
+ "\" method=\"POST\"><request requestID=\"" + id + "\"><info " + infoAddon + "\" method=\"POST\"><request requestID=\"" + id + "\"><info " + infoAddon
+ " type=\"new\"/></request></header><body>" + postData + "</body></msg>"; + " type=\"new\"/></request></header><body>" + postData + "</body></msg>";
try { Session localSession = handler.getSession();
handler.getSession().getRemote().sendStringByFuture(msg); if (localSession != null) {
localSession.getRemote().sendStringByFuture(msg);
logger.debug("{}: sending request: {}", handler.getDeviceName(), msg); logger.debug("{}: sending request: {}", handler.getDeviceName(), msg);
} catch (NullPointerException e) { } else {
handler.onWebSocketError(e); handler.onWebSocketError(new NullPointerException("NPE: Session is unexpected null"));
} }
} }
private void updateOperatingValues() { private void updateOperatingValues() {
OperationModeType operationMode; OperationModeType operationMode;
if (currentContentItem != null) { ContentItem localContentItem = currentContentItem;
updatePresetGUIState(new DecimalType(currentContentItem.getPresetID())); if (localContentItem != null) {
operationMode = currentContentItem.getOperationMode(); updatePresetGUIState(new DecimalType(localContentItem.getPresetID()));
operationMode = localContentItem.getOperationMode();
} else { } else {
operationMode = OperationModeType.STANDBY; operationMode = OperationModeType.STANDBY;
} }

View File

@ -16,7 +16,8 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import org.apache.commons.lang3.StringEscapeUtils; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.StateOption; import org.openhab.core.types.StateOption;
import com.google.gson.annotations.Expose; import com.google.gson.annotations.Expose;
@ -27,31 +28,18 @@ import com.google.gson.annotations.Expose;
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class ContentItem { public class ContentItem {
private String source; private String source = "";
private String sourceAccount; private @Nullable String sourceAccount;
private String location; private @Nullable String location;
private boolean presetable; private boolean presetable = false;
private String itemName; private @Nullable String itemName;
private int presetID; private int presetID = 0;
private String containerArt; private @Nullable String containerArt;
@Expose @Expose
private final Map<String, String> additionalAttributes; private final Map<String, String> additionalAttributes = new HashMap<>();
/**
* Creates a new instance of this class
*/
public ContentItem() {
source = "";
sourceAccount = null;
location = null;
presetable = false;
itemName = null;
presetID = 0;
containerArt = null;
additionalAttributes = new HashMap<>();
}
/** /**
* Returns true if this ContentItem is defined as Preset * Returns true if this ContentItem is defined as Preset
@ -74,11 +62,13 @@ public class ContentItem {
public boolean isValid() { public boolean isValid() {
if (getOperationMode() == OperationModeType.STANDBY) { if (getOperationMode() == OperationModeType.STANDBY) {
return true; return true;
}
if (itemName == null || source == null || itemName.isEmpty() || source.isEmpty()) {
return false;
} else { } else {
return true; String localItemName = itemName;
if (localItemName != null) {
return !(localItemName.isEmpty() || source.isEmpty());
} else {
return false;
}
} }
} }
@ -88,25 +78,12 @@ public class ContentItem {
* @return true if source, sourceAccount, location, itemName, and presetable are equal * @return true if source, sourceAccount, location, itemName, and presetable are equal
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (obj instanceof ContentItem) { if (obj instanceof ContentItem) {
ContentItem other = (ContentItem) obj; ContentItem other = (ContentItem) obj;
if (!Objects.equals(other.source, this.source)) { return Objects.equals(other.source, this.source) || Objects.equals(other.sourceAccount, this.sourceAccount)
return false; || other.presetable == this.presetable || Objects.equals(other.location, this.location)
} || Objects.equals(other.itemName, this.itemName);
if (!Objects.equals(other.sourceAccount, this.sourceAccount)) {
return false;
}
if (other.presetable != this.presetable) {
return false;
}
if (!Objects.equals(other.location, this.location)) {
return false;
}
if (!Objects.equals(other.itemName, this.itemName)) {
return false;
}
return true;
} }
return super.equals(obj); return super.equals(obj);
} }
@ -118,16 +95,19 @@ public class ContentItem {
*/ */
public OperationModeType getOperationMode() { public OperationModeType getOperationMode() {
OperationModeType operationMode = OperationModeType.OTHER; OperationModeType operationMode = OperationModeType.OTHER;
if (source == null || source.equals("")) { if ("".equals(source)) {
return OperationModeType.OTHER; return OperationModeType.OTHER;
} }
if (source.contains("PRODUCT")) { if (source.contains("PRODUCT")) {
if (sourceAccount.contains("TV")) { String localSourceAccount = sourceAccount;
if (localSourceAccount != null) {
if (localSourceAccount.contains("TV")) {
operationMode = OperationModeType.TV; operationMode = OperationModeType.TV;
} }
if (sourceAccount.contains("HDMI")) { if (localSourceAccount.contains("HDMI")) {
operationMode = OperationModeType.HDMI1; operationMode = OperationModeType.HDMI1;
} }
}
return operationMode; return operationMode;
} }
try { try {
@ -174,15 +154,15 @@ public class ContentItem {
return source; return source;
} }
public String getSourceAccount() { public @Nullable String getSourceAccount() {
return sourceAccount; return sourceAccount;
} }
public String getLocation() { public @Nullable String getLocation() {
return location; return location;
} }
public String getItemName() { public @Nullable String getItemName() {
return itemName; return itemName;
} }
@ -194,10 +174,28 @@ public class ContentItem {
return presetID; return presetID;
} }
public String getContainerArt() { public @Nullable String getContainerArt() {
return containerArt; return containerArt;
} }
/**
* Simple method to escape XML special characters in String.
* There are five XML Special characters which needs to be escaped :
* & - &amp;
* < - &lt;
* > - &gt;
* " - &quot;
* ' - &apos;
*/
private String escapeXml(String xml) {
xml = xml.replaceAll("&", "&amp;");
xml = xml.replaceAll("<", "&lt;");
xml = xml.replaceAll(">", "&gt;");
xml = xml.replaceAll("\"", "&quot;");
xml = xml.replaceAll("'", "&apos;");
return xml;
}
/** /**
* Returns the XML Code that is needed to switch to this ContentItem * Returns the XML Code that is needed to switch to this ContentItem
* *
@ -223,19 +221,20 @@ public class ContentItem {
break; break;
default: default:
StringBuilder sbXml = new StringBuilder("<ContentItem"); StringBuilder sbXml = new StringBuilder("<ContentItem");
if (source != null) {
sbXml.append(" source=\"").append(StringEscapeUtils.escapeXml(source)).append("\""); sbXml.append(" source=\"").append(escapeXml(source)).append("\"");
String localLocation = location;
if (localLocation != null) {
sbXml.append(" location=\"").append(escapeXml(localLocation)).append("\"");
} }
if (location != null) { String localSourceAccount = sourceAccount;
sbXml.append(" location=\"").append(StringEscapeUtils.escapeXml(location)).append("\""); if (localSourceAccount != null) {
} sbXml.append(" sourceAccount=\"").append(escapeXml(localSourceAccount)).append("\"");
if (sourceAccount != null) {
sbXml.append(" sourceAccount=\"").append(StringEscapeUtils.escapeXml(sourceAccount)).append("\"");
} }
sbXml.append(" isPresetable=\"").append(presetable).append("\""); sbXml.append(" isPresetable=\"").append(presetable).append("\"");
for (Map.Entry<String, String> aae : additionalAttributes.entrySet()) { for (Map.Entry<String, String> aae : additionalAttributes.entrySet()) {
sbXml.append(" ").append(aae.getKey()).append("=\"") sbXml.append(" ").append(aae.getKey()).append("=\"").append(escapeXml(aae.getValue())).append("\"");
.append(StringEscapeUtils.escapeXml(aae.getValue())).append("\"");
} }
sbXml.append(">"); sbXml.append(">");
if (itemName != null) { if (itemName != null) {
@ -264,6 +263,7 @@ public class ContentItem {
// buffer.append(presetID); // buffer.append(presetID);
// return buffer.toString(); // return buffer.toString();
// } // }
return itemName; String localString = itemName;
return (localString != null) ? localString : "";
} }
} }

View File

@ -14,11 +14,14 @@ package org.openhab.binding.bosesoundtouch.internal;
import java.util.Collection; import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link ContentItemMaker} class makes ContentItems for sources * The {@link ContentItemMaker} class makes ContentItems for sources
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class ContentItemMaker { public class ContentItemMaker {
private final PresetContainer presetContainer; private final PresetContainer presetContainer;

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link ContentItemNotPresetableException} class is an exception * The {@link ContentItemNotPresetableException} class is an exception
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class ContentItemNotPresetableException extends NoPresetFoundException { public class ContentItemNotPresetableException extends NoPresetFoundException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link NoInternetRadioPresetFoundException} class is an exception * The {@link NoInternetRadioPresetFoundException} class is an exception
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class NoInternetRadioPresetFoundException extends NoPresetFoundException { public class NoInternetRadioPresetFoundException extends NoPresetFoundException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link NoPresetFoundException} class is an exception * The {@link NoPresetFoundException} class is an exception
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class NoPresetFoundException extends Exception { public class NoPresetFoundException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link NoStoredMusicPresetFoundException} class is an exception * The {@link NoStoredMusicPresetFoundException} class is an exception
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class NoStoredMusicPresetFoundException extends NoPresetFoundException { public class NoStoredMusicPresetFoundException extends NoPresetFoundException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link OperationModeNotAvailableException} class is an exception * The {@link OperationModeNotAvailableException} class is an exception
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class OperationModeNotAvailableException extends Exception { public class OperationModeNotAvailableException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,12 +12,15 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link OperationModeType} class is holding all OperationModes * The {@link OperationModeType} class is holding all OperationModes
* *
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public enum OperationModeType { public enum OperationModeType {
OFFLINE, OFFLINE,
STANDBY, STANDBY,

View File

@ -17,8 +17,11 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.storage.DeletableStorage; import org.openhab.core.storage.DeletableStorage;
import org.openhab.core.storage.Storage; import org.openhab.core.storage.Storage;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -30,11 +33,12 @@ import org.slf4j.LoggerFactory;
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
* @author Kai Kreuzer - Refactored it to use storage instead of file * @author Kai Kreuzer - Refactored it to use storage instead of file
*/ */
@NonNullByDefault
public class PresetContainer { public class PresetContainer {
private final Logger logger = LoggerFactory.getLogger(PresetContainer.class); private final Logger logger = LoggerFactory.getLogger(PresetContainer.class);
private HashMap<Integer, ContentItem> mapOfPresets; private final Map<Integer, ContentItem> mapOfPresets = new HashMap<>();
private Storage<ContentItem> storage; private Storage<ContentItem> storage;
/** /**
@ -42,11 +46,6 @@ public class PresetContainer {
*/ */
public PresetContainer(Storage<ContentItem> storage) { public PresetContainer(Storage<ContentItem> storage) {
this.storage = storage; this.storage = storage;
init();
}
private void init() {
this.mapOfPresets = new HashMap<>();
readFromStorage(); readFromStorage();
} }
@ -133,10 +132,12 @@ public class PresetContainer {
} }
private void readFromStorage() { private void readFromStorage() {
Collection<ContentItem> items = storage.getValues(); Collection<@Nullable ContentItem> items = storage.getValues();
for (ContentItem item : items) { for (ContentItem item : items) {
try { try {
if (item != null) {
put(item.getPresetID(), item); put(item.getPresetID(), item);
}
} catch (ContentItemNotPresetableException e) { } catch (ContentItemNotPresetableException e) {
logger.debug("Item '{}' is not presetable - ignoring it.", item.getItemName()); logger.debug("Item '{}' is not presetable - ignoring it.", item.getItemName());
} }

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link RemoteKeyType} class is holding the Keys on a remote. For simulating key presses * The {@link RemoteKeyType} class is holding the Keys on a remote. For simulating key presses
* *
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
*/ */
@NonNullByDefault
public enum RemoteKeyType { public enum RemoteKeyType {
PLAY, PLAY,
PAUSE, PAUSE,

View File

@ -12,12 +12,15 @@
*/ */
package org.openhab.binding.bosesoundtouch.internal; package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link XMLHandlerState} class defines the XML States provided from Bose Soundtouch * The {@link XMLHandlerState} class defines the XML States provided from Bose Soundtouch
* *
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public enum XMLHandlerState { public enum XMLHandlerState {
INIT, INIT,
Msg, Msg,

View File

@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Stack; import java.util.Stack;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler; import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
@ -54,8 +55,8 @@ public class XMLResponseHandler extends DefaultHandler {
private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap; private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap;
private Stack<XMLHandlerState> states; private final Stack<XMLHandlerState> states = new Stack<>();
private XMLHandlerState state; private XMLHandlerState state = XMLHandlerState.INIT;
private boolean msgHeaderWasValid; private boolean msgHeaderWasValid;
private ContentItem contentItem; private ContentItem contentItem;
@ -63,10 +64,10 @@ public class XMLResponseHandler extends DefaultHandler {
private OnOffType rateEnabled; private OnOffType rateEnabled;
private OnOffType skipEnabled; private OnOffType skipEnabled;
private OnOffType skipPreviousEnabled; private OnOffType skipPreviousEnabled;
private State nowPlayingSource; private State nowPlayingSource;
private BoseSoundTouchConfiguration masterDeviceId; private BoseSoundTouchConfiguration masterDeviceId;
String deviceId; String deviceId;
private Map<Integer, ContentItem> playerPresets; private Map<Integer, ContentItem> playerPresets;
@ -82,11 +83,11 @@ public class XMLResponseHandler extends DefaultHandler {
this.handler = handler; this.handler = handler;
this.commandExecutor = handler.getCommandExecutor(); this.commandExecutor = handler.getCommandExecutor();
this.stateSwitchingMap = stateSwitchingMap; this.stateSwitchingMap = stateSwitchingMap;
init();
} }
@Override @Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { public void startElement(@Nullable String uri, @Nullable String localName, @Nullable String qName,
@Nullable Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes); super.startElement(uri, localName, qName, attributes);
logger.trace("{}: startElement('{}'; state: {})", handler.getDeviceName(), localName, state); logger.trace("{}: startElement('{}'; state: {})", handler.getDeviceName(), localName, state);
states.push(state); states.push(state);
@ -95,6 +96,12 @@ public class XMLResponseHandler extends DefaultHandler {
state = XMLHandlerState.Unprocessed; // set default value; we avoid default in select to have the compiler state = XMLHandlerState.Unprocessed; // set default value; we avoid default in select to have the compiler
// showing a // showing a
// warning for unhandled states // warning for unhandled states
XMLHandlerState localState = null;
if (stateMap != null) {
localState = stateMap.get(localName);
}
switch (curState) { switch (curState) {
case INIT: case INIT:
if ("updates".equals(localName)) { if ("updates".equals(localName)) {
@ -105,12 +112,9 @@ public class XMLResponseHandler extends DefaultHandler {
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
} else { } else {
state = stateMap.get(localName); if (localState == null) {
if (state == null) { logger.debug("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
localName); localName);
}
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
} }
@ -131,10 +135,9 @@ public class XMLResponseHandler extends DefaultHandler {
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
} else { } else {
if (logger.isDebugEnabled()) { logger.debug("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName); localName);
}
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
break; break;
@ -142,10 +145,8 @@ public class XMLResponseHandler extends DefaultHandler {
if ("request".equals(localName)) { if ("request".equals(localName)) {
state = XMLHandlerState.Unprocessed; // TODO implement request id / response tracking... state = XMLHandlerState.Unprocessed; // TODO implement request id / response tracking...
} else { } else {
if (logger.isDebugEnabled()) { logger.debug("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName); localName);
}
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
break; break;
@ -161,7 +162,10 @@ public class XMLResponseHandler extends DefaultHandler {
skipEnabled = OnOffType.OFF; skipEnabled = OnOffType.OFF;
skipPreviousEnabled = OnOffType.OFF; skipPreviousEnabled = OnOffType.OFF;
state = XMLHandlerState.NowPlaying; state = XMLHandlerState.NowPlaying;
String source = attributes.getValue("source"); String source = "";
if (attributes != null) {
source = attributes.getValue("source");
}
if (nowPlayingSource == null || !nowPlayingSource.toString().equals(source)) { if (nowPlayingSource == null || !nowPlayingSource.toString().equals(source)) {
// source changed // source changed
nowPlayingSource = new StringType(source); nowPlayingSource = new StringType(source);
@ -192,14 +196,11 @@ public class XMLResponseHandler extends DefaultHandler {
state = XMLHandlerState.Presets; state = XMLHandlerState.Presets;
} else if ("group".equals(localName)) { } else if ("group".equals(localName)) {
this.masterDeviceId = new BoseSoundTouchConfiguration(); this.masterDeviceId = new BoseSoundTouchConfiguration();
state = stateMap.get(localName);
} else { } else {
state = stateMap.get(localName); if (localState == null) {
if (state == null) { logger.debug("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
localName); localName);
}
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} else if (state != XMLHandlerState.Volume && state != XMLHandlerState.Presets } else if (state != XMLHandlerState.Volume && state != XMLHandlerState.Presets
&& state != XMLHandlerState.Group && state != XMLHandlerState.Unprocessed) { && state != XMLHandlerState.Group && state != XMLHandlerState.Unprocessed) {
@ -213,63 +214,78 @@ public class XMLResponseHandler extends DefaultHandler {
case Presets: case Presets:
if ("preset".equals(localName)) { if ("preset".equals(localName)) {
state = XMLHandlerState.Preset; state = XMLHandlerState.Preset;
String id = attributes.getValue("id"); String id = "0";
if (attributes != null) {
id = attributes.getValue("id");
}
if (contentItem == null) { if (contentItem == null) {
contentItem = new ContentItem(); contentItem = new ContentItem();
} }
contentItem.setPresetID(Integer.parseInt(id)); contentItem.setPresetID(Integer.parseInt(id));
} else { } else {
if (logger.isDebugEnabled()) { logger.debug("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName); localName);
}
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
break; break;
case Sources: case Sources:
if ("sourceItem".equals(localName)) { if ("sourceItem".equals(localName)) {
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
String source = attributes.getValue("source"); String source = "";
String sourceAccount = attributes.getValue("sourceAccount"); String status = "";
String status = attributes.getValue("status"); String sourceAccount = "";
if (status.equals("READY")) { if (attributes != null) {
if (source.equals("AUX")) { source = attributes.getValue("source");
if (sourceAccount.equals("AUX")) { sourceAccount = attributes.getValue("sourceAccount");
status = attributes.getValue("status");
}
if ("READY".equals(status)) {
switch (source) {
case "AUX":
if ("AUX".equals(sourceAccount)) {
commandExecutor.setAUXAvailable(true); commandExecutor.setAUXAvailable(true);
} }
if (sourceAccount.equals("AUX1")) { if ("AUX1".equals(sourceAccount)) {
commandExecutor.setAUX1Available(true); commandExecutor.setAUX1Available(true);
} }
if (sourceAccount.equals("AUX2")) { if ("AUX2".equals(sourceAccount)) {
commandExecutor.setAUX2Available(true); commandExecutor.setAUX2Available(true);
} }
if (sourceAccount.equals("AUX3")) { if ("AUX3".equals(sourceAccount)) {
commandExecutor.setAUX3Available(true); commandExecutor.setAUX3Available(true);
} }
} break;
if (source.equals("STORED_MUSIC")) { case "STORED_MUSIC":
commandExecutor.setStoredMusicAvailable(true); commandExecutor.setStoredMusicAvailable(true);
} break;
if (source.equals("INTERNET_RADIO")) { case "INTERNET_RADIO":
commandExecutor.setInternetRadioAvailable(true); commandExecutor.setInternetRadioAvailable(true);
} break;
if (source.equals("BLUETOOTH")) { case "BLUETOOTH":
commandExecutor.setBluetoothAvailable(true); commandExecutor.setBluetoothAvailable(true);
} break;
if (source.equals("PRODUCT")) { case "PRODUCT":
if (sourceAccount.equals("TV")) { switch (sourceAccount) {
case "TV":
commandExecutor.setTVAvailable(true); commandExecutor.setTVAvailable(true);
} break;
if (sourceAccount.equals("HDMI_1")) { case "HDMI_1":
commandExecutor.setHDMI1Available(true); commandExecutor.setHDMI1Available(true);
break;
default:
logger.debug("{}: has an unknown source account: '{}'", handler.getDeviceName(),
sourceAccount);
break;
} }
default:
logger.debug("{}: has an unknown source: '{}'", handler.getDeviceName(), source);
break;
} }
} }
} else { } else {
if (logger.isDebugEnabled()) { logger.debug("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName); localName);
}
state = XMLHandlerState.Unprocessed; state = XMLHandlerState.Unprocessed;
} }
break; break;
@ -350,10 +366,28 @@ public class XMLResponseHandler extends DefaultHandler {
if (contentItem == null) { if (contentItem == null) {
contentItem = new ContentItem(); contentItem = new ContentItem();
} }
contentItem.setSource(attributes.getValue("source")); String source = "";
contentItem.setSourceAccount(attributes.getValue("sourceAccount")); String location = "";
contentItem.setLocation(attributes.getValue("location")); String sourceAccount = "";
contentItem.setPresetable(Boolean.parseBoolean(attributes.getValue("isPresetable"))); Boolean isPresetable = false;
if (attributes != null) {
source = attributes.getValue("source");
sourceAccount = attributes.getValue("sourceAccount");
location = attributes.getValue("location");
isPresetable = Boolean.parseBoolean(attributes.getValue("isPresetable"));
if (source != null) {
contentItem.setSource(source);
}
if (sourceAccount != null) {
contentItem.setSourceAccount(sourceAccount);
}
if (location != null) {
contentItem.setLocation(location);
}
contentItem.setPresetable(isPresetable);
for (int attrId = 0; attrId < attributes.getLength(); attrId++) { for (int attrId = 0; attrId < attributes.getLength(); attrId++) {
String attrName = attributes.getLocalName(attrId); String attrName = attributes.getLocalName(attrId);
if ("source".equalsIgnoreCase(attrName)) { if ("source".equalsIgnoreCase(attrName)) {
@ -368,10 +402,13 @@ public class XMLResponseHandler extends DefaultHandler {
if ("isPresetable".equalsIgnoreCase(attrName)) { if ("isPresetable".equalsIgnoreCase(attrName)) {
continue; continue;
} }
if (attrName != null) {
contentItem.setAdditionalAttribute(attrName, attributes.getValue(attrId)); contentItem.setAdditionalAttribute(attrName, attributes.getValue(attrId));
} }
} }
} }
}
}
@Override @Override
public void endElement(String uri, String localName, String qName) throws SAXException { public void endElement(String uri, String localName, String qName) throws SAXException {
@ -599,8 +636,9 @@ public class XMLResponseHandler extends DefaultHandler {
super.skippedEntity(name); super.skippedEntity(name);
} }
private boolean checkDeviceId(String localName, Attributes attributes, boolean allowFromMaster) { private boolean checkDeviceId(@Nullable String localName, @Nullable Attributes attributes,
String deviceID = attributes.getValue("deviceID"); boolean allowFromMaster) {
String deviceID = (attributes != null) ? attributes.getValue("deviceID") : null;
if (deviceID == null) { if (deviceID == null) {
logger.warn("{}: No device-ID in entity {}", handler.getDeviceName(), localName); logger.warn("{}: No device-ID in entity {}", handler.getDeviceName(), localName);
return false; return false;
@ -613,12 +651,6 @@ public class XMLResponseHandler extends DefaultHandler {
return false; return false;
} }
private void init() {
states = new Stack<>();
state = XMLHandlerState.INIT;
nowPlayingSource = null;
}
private XMLHandlerState nextState(Map<String, XMLHandlerState> stateMap, XMLHandlerState curState, private XMLHandlerState nextState(Map<String, XMLHandlerState> stateMap, XMLHandlerState curState,
String localName) { String localName) {
XMLHandlerState state = stateMap.get(localName); XMLHandlerState state = stateMap.get(localName);
@ -632,6 +664,7 @@ public class XMLResponseHandler extends DefaultHandler {
} }
private void setConfigOption(String option, String value) { private void setConfigOption(String option, String value) {
if (option != null) {
Map<String, String> prop = handler.getThing().getProperties(); Map<String, String> prop = handler.getThing().getProperties();
String cur = prop.get(option); String cur = prop.get(option);
if (cur == null || !cur.equals(value)) { if (cur == null || !cur.equals(value)) {
@ -639,6 +672,7 @@ public class XMLResponseHandler extends DefaultHandler {
handler.getThing().setProperty(option, value); handler.getThing().setProperty(option, value);
} }
} }
}
private void updateNowPlayingAlbum(State state) { private void updateNowPlayingAlbum(State state) {
handler.updateState(CHANNEL_NOWPLAYING_ALBUM, state); handler.updateState(CHANNEL_NOWPLAYING_ALBUM, state);

View File

@ -17,11 +17,15 @@ import java.io.StringReader;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler; import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import org.xml.sax.XMLReader; import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
/** /**
* The {@link XMLResponseProcessor} class handles the XML mapping * The {@link XMLResponseProcessor} class handles the XML mapping
@ -29,18 +33,22 @@ import org.xml.sax.helpers.XMLReaderFactory;
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class XMLResponseProcessor { public class XMLResponseProcessor {
private BoseSoundTouchHandler handler; private BoseSoundTouchHandler handler;
private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap; private final Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap = new HashMap<>();
public XMLResponseProcessor(BoseSoundTouchHandler handler) { public XMLResponseProcessor(BoseSoundTouchHandler handler) {
this.handler = handler; this.handler = handler;
init(); init();
} }
public void handleMessage(String msg) throws SAXException, IOException { public void handleMessage(String msg) throws SAXException, IOException, ParserConfigurationException {
XMLReader reader = XMLReaderFactory.createXMLReader(); SAXParserFactory parserFactory = SAXParserFactory.newInstance();
SAXParser parser = parserFactory.newSAXParser();
XMLReader reader = parser.getXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setContentHandler(new XMLResponseHandler(handler, stateSwitchingMap)); reader.setContentHandler(new XMLResponseHandler(handler, stateSwitchingMap));
reader.parse(new InputSource(new StringReader(msg))); reader.parse(new InputSource(new StringReader(msg)));
@ -48,8 +56,6 @@ public class XMLResponseProcessor {
// initializes our XML parsing state machine // initializes our XML parsing state machine
private void init() { private void init() {
stateSwitchingMap = new HashMap<>();
Map<String, XMLHandlerState> msgInitMap = new HashMap<>(); Map<String, XMLHandlerState> msgInitMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.INIT, msgInitMap); stateSwitchingMap.put(XMLHandlerState.INIT, msgInitMap);
msgInitMap.put("msg", XMLHandlerState.Msg); msgInitMap.put("msg", XMLHandlerState.Msg);

View File

@ -14,6 +14,7 @@ package org.openhab.binding.bosesoundtouch.internal.discovery;
import java.io.IOException; import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.io.net.http.HttpUtil;
/** /**
@ -21,6 +22,7 @@ import org.openhab.core.io.net.http.HttpUtil;
* *
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
public class DiscoveryUtil { public class DiscoveryUtil {
/** /**
@ -29,9 +31,6 @@ public class DiscoveryUtil {
* This is a quick and dirty method, it always delivers the first appearance of content in an element * This is a quick and dirty method, it always delivers the first appearance of content in an element
*/ */
public static String getContentOfFirstElement(String content, String element) { public static String getContentOfFirstElement(String content, String element) {
if (content == null) {
return "";
}
String beginTag = "<" + element + ">"; String beginTag = "<" + element + ">";
String endTag = "</" + element + ">"; String endTag = "</" + element + ">";
@ -39,7 +38,8 @@ public class DiscoveryUtil {
int endIndex = content.indexOf(endTag); int endIndex = content.indexOf(endTag);
if (startIndex != -1 && endIndex != -1) { if (startIndex != -1 && endIndex != -1) {
return content.substring(startIndex, endIndex); String result = content.substring(startIndex, endIndex);
return result != null ? result : "";
} else { } else {
return ""; return "";
} }

View File

@ -26,6 +26,8 @@ import java.util.Set;
import javax.jmdns.ServiceInfo; import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchConfiguration; import org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchConfiguration;
import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@ -44,6 +46,7 @@ import org.slf4j.LoggerFactory;
* @author Christian Niessner - Initial contribution * @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution * @author Thomas Traunbauer - Initial contribution
*/ */
@NonNullByDefault
@Component(configurationPid = "discovery.bosesoundtouch") @Component(configurationPid = "discovery.bosesoundtouch")
public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant { public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant {
@ -55,7 +58,7 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
} }
@Override @Override
public DiscoveryResult createResult(ServiceInfo info) { public @Nullable DiscoveryResult createResult(ServiceInfo info) {
DiscoveryResult result = null; DiscoveryResult result = null;
ThingUID uid = getThingUID(info); ThingUID uid = getThingUID(info);
if (uid != null) { if (uid != null) {
@ -89,9 +92,10 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
} }
properties.put(BoseSoundTouchConfiguration.HOST, addrs[0].getHostAddress()); properties.put(BoseSoundTouchConfiguration.HOST, addrs[0].getHostAddress());
if (getMacAddress(info) != null) { byte[] localMacAddress = getMacAddress(info);
if (localMacAddress.length > 0) {
properties.put(BoseSoundTouchConfiguration.MAC_ADDRESS, properties.put(BoseSoundTouchConfiguration.MAC_ADDRESS,
new String(getMacAddress(info), StandardCharsets.UTF_8)); new String(localMacAddress, StandardCharsets.UTF_8));
} }
// Set manufacturer as thing property (if available) // Set manufacturer as thing property (if available)
@ -105,7 +109,7 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
} }
@Override @Override
public ThingUID getThingUID(ServiceInfo info) { public @Nullable ThingUID getThingUID(ServiceInfo info) {
logger.trace("ServiceInfo: {}", info); logger.trace("ServiceInfo: {}", info);
ThingTypeUID typeUID = getThingTypeUID(info); ThingTypeUID typeUID = getThingTypeUID(info);
if (typeUID != null) { if (typeUID != null) {
@ -113,10 +117,8 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
if (info.getType().equals(getServiceType())) { if (info.getType().equals(getServiceType())) {
logger.trace("Discovered a Bose SoundTouch thing with name '{}'", info.getName()); logger.trace("Discovered a Bose SoundTouch thing with name '{}'", info.getName());
byte[] mac = getMacAddress(info); byte[] mac = getMacAddress(info);
if (mac != null) { if (mac.length > 0) {
return new ThingUID(typeUID, new String(mac, StandardCharsets.UTF_8)); return new ThingUID(typeUID, new String(mac, StandardCharsets.UTF_8));
} else {
return null;
} }
} }
} }
@ -129,13 +131,13 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
return "_soundtouch._tcp.local."; return "_soundtouch._tcp.local.";
} }
private ThingTypeUID getThingTypeUID(ServiceInfo info) { private @Nullable ThingTypeUID getThingTypeUID(ServiceInfo info) {
InetAddress[] addrs = info.getInetAddresses(); InetAddress[] addrs = info.getInetAddresses();
if (addrs.length > 0) { if (addrs.length > 0) {
String ip = addrs[0].getHostAddress(); String ip = addrs[0].getHostAddress();
String deviceId = null; String deviceId = null;
byte[] mac = getMacAddress(info); byte[] mac = getMacAddress(info);
if (mac != null) { if (mac.length > 0) {
deviceId = new String(mac, StandardCharsets.UTF_8); deviceId = new String(mac, StandardCharsets.UTF_8);
} }
String deviceType; String deviceType;
@ -143,6 +145,7 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
String content = DiscoveryUtil.executeUrl("http://" + ip + ":8090/info"); String content = DiscoveryUtil.executeUrl("http://" + ip + ":8090/info");
deviceType = DiscoveryUtil.getContentOfFirstElement(content, "type"); deviceType = DiscoveryUtil.getContentOfFirstElement(content, "type");
} catch (IOException e) { } catch (IOException e) {
logger.debug("Ignoring IOException during Discovery: {}", e.getMessage());
return null; return null;
} }
@ -163,6 +166,7 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
return BST_10_THING_TYPE_UID; return BST_10_THING_TYPE_UID;
} }
} catch (IOException e) { } catch (IOException e) {
logger.debug("Ignoring IOException during Discovery: {}", e.getMessage());
return null; return null;
} }
} }
@ -190,24 +194,21 @@ public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant
} }
private byte[] getMacAddress(ServiceInfo info) { private byte[] getMacAddress(ServiceInfo info) {
if (info != null) {
// sometimes we see empty messages - ignore them // sometimes we see empty messages - ignore them
if (!info.hasData()) { if (!info.hasData()) {
return null; return new byte[0];
} }
byte[] mac = info.getPropertyBytes("MAC"); byte[] mac = info.getPropertyBytes("MAC");
if (mac == null) { if (mac == null) {
logger.warn("SoundTouch Device {} delivered no MAC address!", info.getName()); logger.warn("SoundTouch Device {} delivered no MAC address!", info.getName());
return null; return new byte[0];
} }
if (mac.length != 12) { if (mac.length != 12) {
BigInteger bi = new BigInteger(1, mac); BigInteger bi = new BigInteger(1, mac);
logger.warn("SoundTouch Device {} delivered an invalid MAC address: 0x{}", info.getName(), logger.warn("SoundTouch Device {} delivered an invalid MAC address: 0x{}", info.getName(),
String.format("%0" + (mac.length << 1) + "X", bi)); String.format("%0" + (mac.length << 1) + "X", bi));
return null; return new byte[0];
} }
return mac; return mac;
} }
return null;
}
} }

View File

@ -27,6 +27,8 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener; import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
@ -75,6 +77,7 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - code clean up * @author Kai Kreuzer - code clean up
* @author Alexander Kostadinov - Handling of websocket ping-pong mechanism for thing status check * @author Alexander Kostadinov - Handling of websocket ping-pong mechanism for thing status check
*/ */
@NonNullByDefault
public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocketListener, WebSocketFrameListener { public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocketListener, WebSocketFrameListener {
private static final int MAX_MISSED_PONGS_COUNT = 2; private static final int MAX_MISSED_PONGS_COUNT = 2;
@ -83,10 +86,10 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
private final Logger logger = LoggerFactory.getLogger(BoseSoundTouchHandler.class); private final Logger logger = LoggerFactory.getLogger(BoseSoundTouchHandler.class);
private ScheduledFuture<?> connectionChecker; private @Nullable ScheduledFuture<?> connectionChecker;
private WebSocketClient client; private @Nullable WebSocketClient client;
private volatile Session session; private @Nullable volatile Session session;
private volatile CommandExecutor commandExecutor; private @Nullable volatile CommandExecutor commandExecutor;
private volatile int missedPongsCount = 0; private volatile int missedPongsCount = 0;
private XMLResponseProcessor xmlResponseProcessor; private XMLResponseProcessor xmlResponseProcessor;
@ -94,7 +97,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
private PresetContainer presetContainer; private PresetContainer presetContainer;
private BoseStateDescriptionOptionProvider stateOptionProvider; private BoseStateDescriptionOptionProvider stateOptionProvider;
private Future<?> sessionFuture; private @Nullable Future<?> sessionFuture;
/** /**
* Creates a new instance of this class for the {@link Thing}. * Creates a new instance of this class for the {@link Thing}.
@ -120,10 +123,13 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
@Override @Override
public void dispose() { public void dispose() {
if (connectionChecker != null && !connectionChecker.isCancelled()) { ScheduledFuture<?> localConnectionChecker = connectionChecker;
connectionChecker.cancel(true); if (localConnectionChecker != null) {
if (!localConnectionChecker.isCancelled()) {
localConnectionChecker.cancel(true);
connectionChecker = null; connectionChecker = null;
} }
}
closeConnection(); closeConnection();
super.dispose(); super.dispose();
} }
@ -146,7 +152,8 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
if (commandExecutor == null) { CommandExecutor localCommandExecutor = commandExecutor;
if (localCommandExecutor == null) {
logger.debug("{}: Can't handle command '{}' for channel '{}' because of not initialized connection.", logger.debug("{}: Can't handle command '{}' for channel '{}' because of not initialized connection.",
getDeviceName(), command, channelUID); getDeviceName(), command, channelUID);
return; return;
@ -157,7 +164,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
if (command.equals(RefreshType.REFRESH)) { if (command.equals(RefreshType.REFRESH)) {
switch (channelUID.getIdWithoutGroup()) { switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_BASS: case CHANNEL_BASS:
commandExecutor.getInformations(APIRequest.BASS); localCommandExecutor.getInformations(APIRequest.BASS);
break; break;
case CHANNEL_KEY_CODE: case CHANNEL_KEY_CODE:
// refresh makes no sense... ? // refresh makes no sense... ?
@ -174,10 +181,10 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
case CHANNEL_RATEENABLED: case CHANNEL_RATEENABLED:
case CHANNEL_SKIPENABLED: case CHANNEL_SKIPENABLED:
case CHANNEL_SKIPPREVIOUSENABLED: case CHANNEL_SKIPPREVIOUSENABLED:
commandExecutor.getInformations(APIRequest.NOW_PLAYING); localCommandExecutor.getInformations(APIRequest.NOW_PLAYING);
break; break;
case CHANNEL_VOLUME: case CHANNEL_VOLUME:
commandExecutor.getInformations(APIRequest.VOLUME); localCommandExecutor.getInformations(APIRequest.VOLUME);
break; break;
default: default:
logger.debug("{} : Got command '{}' for channel '{}' which is unhandled!", getDeviceName(), command, logger.debug("{} : Got command '{}' for channel '{}' which is unhandled!", getDeviceName(), command,
@ -188,21 +195,21 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
switch (channelUID.getIdWithoutGroup()) { switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_POWER: case CHANNEL_POWER:
if (command instanceof OnOffType) { if (command instanceof OnOffType) {
commandExecutor.postPower((OnOffType) command); localCommandExecutor.postPower((OnOffType) command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
break; break;
case CHANNEL_VOLUME: case CHANNEL_VOLUME:
if (command instanceof PercentType) { if (command instanceof PercentType) {
commandExecutor.postVolume((PercentType) command); localCommandExecutor.postVolume((PercentType) command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
break; break;
case CHANNEL_MUTE: case CHANNEL_MUTE:
if (command instanceof OnOffType) { if (command instanceof OnOffType) {
commandExecutor.postVolumeMuted((OnOffType) command); localCommandExecutor.postVolumeMuted((OnOffType) command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
@ -212,7 +219,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
String cmd = command.toString().toUpperCase().trim(); String cmd = command.toString().toUpperCase().trim();
try { try {
OperationModeType mode = OperationModeType.valueOf(cmd); OperationModeType mode = OperationModeType.valueOf(cmd);
commandExecutor.postOperationMode(mode); localCommandExecutor.postOperationMode(mode);
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
logger.warn("{}: OperationMode \"{}\" is not valid!", getDeviceName(), cmd); logger.warn("{}: OperationMode \"{}\" is not valid!", getDeviceName(), cmd);
} }
@ -220,28 +227,28 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
break; break;
case CHANNEL_PLAYER_CONTROL: case CHANNEL_PLAYER_CONTROL:
if ((command instanceof PlayPauseType) || (command instanceof NextPreviousType)) { if ((command instanceof PlayPauseType) || (command instanceof NextPreviousType)) {
commandExecutor.postPlayerControl(command); localCommandExecutor.postPlayerControl(command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
break; break;
case CHANNEL_PRESET: case CHANNEL_PRESET:
if (command instanceof DecimalType) { if (command instanceof DecimalType) {
commandExecutor.postPreset((DecimalType) command); localCommandExecutor.postPreset((DecimalType) command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
break; break;
case CHANNEL_BASS: case CHANNEL_BASS:
if (command instanceof DecimalType) { if (command instanceof DecimalType) {
commandExecutor.postBass((DecimalType) command); localCommandExecutor.postBass((DecimalType) command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
break; break;
case CHANNEL_SAVE_AS_PRESET: case CHANNEL_SAVE_AS_PRESET:
if (command instanceof DecimalType) { if (command instanceof DecimalType) {
commandExecutor.addCurrentContentItemToPresetContainer((DecimalType) command); localCommandExecutor.addCurrentContentItemToPresetContainer((DecimalType) command);
} else { } else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command); logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
} }
@ -251,7 +258,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
String cmd = command.toString().toUpperCase().trim(); String cmd = command.toString().toUpperCase().trim();
try { try {
RemoteKeyType keyCommand = RemoteKeyType.valueOf(cmd); RemoteKeyType keyCommand = RemoteKeyType.valueOf(cmd);
commandExecutor.postRemoteKey(keyCommand); localCommandExecutor.postRemoteKey(keyCommand);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.debug("{}: Unhandled remote key: {}", getDeviceName(), cmd); logger.debug("{}: Unhandled remote key: {}", getDeviceName(), cmd);
} }
@ -262,7 +269,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
if (channel != null) { if (channel != null) {
ChannelTypeUID chTypeUid = channel.getChannelTypeUID(); ChannelTypeUID chTypeUid = channel.getChannelTypeUID();
if (chTypeUid != null) { if (chTypeUid != null) {
switch (channel.getChannelTypeUID().getId()) { switch (chTypeUid.getId()) {
case CHANNEL_NOTIFICATION_SOUND: case CHANNEL_NOTIFICATION_SOUND:
String appKey = Objects.toString(getConfig().get(BoseSoundTouchConfiguration.APP_KEY), String appKey = Objects.toString(getConfig().get(BoseSoundTouchConfiguration.APP_KEY),
null); null);
@ -273,8 +280,8 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
.getConfiguration() .getConfiguration()
.as(BoseSoundTouchNotificationChannelConfiguration.class); .as(BoseSoundTouchNotificationChannelConfiguration.class);
if (!url.isEmpty()) { if (!url.isEmpty()) {
commandExecutor.playNotificationSound(appKey, notificationConfiguration, localCommandExecutor.playNotificationSound(appKey,
url); notificationConfiguration, url);
} }
} }
} else { } else {
@ -295,7 +302,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
* *
* @return the CommandExecutor of this handler * @return the CommandExecutor of this handler
*/ */
public CommandExecutor getCommandExecutor() { public @Nullable CommandExecutor getCommandExecutor() {
return commandExecutor; return commandExecutor;
} }
@ -304,7 +311,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
* *
* @return the Session this handler has opened * @return the Session this handler has opened
*/ */
public Session getSession() { public @Nullable Session getSession() {
return session; return session;
} }
@ -313,7 +320,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
* *
* @return the name of the device delivered from itself * @return the name of the device delivered from itself
*/ */
public String getDeviceName() { public @Nullable String getDeviceName() {
return getThing().getProperties().get(DEVICE_INFO_NAME); return getThing().getProperties().get(DEVICE_INFO_NAME);
} }
@ -322,7 +329,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
* *
* @return the type of the device delivered from itself * @return the type of the device delivered from itself
*/ */
public String getDeviceType() { public @Nullable String getDeviceType() {
return getThing().getProperties().get(DEVICE_INFO_TYPE); return getThing().getProperties().get(DEVICE_INFO_TYPE);
} }
@ -331,7 +338,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
* *
* @return the MAC Address of this device (in format "123456789ABC") * @return the MAC Address of this device (in format "123456789ABC")
*/ */
public String getMacAddress() { public @Nullable String getMacAddress() {
return ((String) getThing().getConfiguration().get(BoseSoundTouchConfiguration.MAC_ADDRESS)).replaceAll(":", return ((String) getThing().getConfiguration().get(BoseSoundTouchConfiguration.MAC_ADDRESS)).replaceAll(":",
""); "");
} }
@ -341,7 +348,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
* *
* @return the IP Address of this device * @return the IP Address of this device
*/ */
public String getIPAddress() { public @Nullable String getIPAddress() {
return (String) getThing().getConfiguration().getProperties().get(BoseSoundTouchConfiguration.HOST); return (String) getThing().getConfiguration().getProperties().get(BoseSoundTouchConfiguration.HOST);
} }
@ -359,7 +366,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
} }
@Override @Override
public void onWebSocketConnect(Session session) { public void onWebSocketConnect(@Nullable Session session) {
logger.debug("{}: onWebSocketConnect('{}')", getDeviceName(), session); logger.debug("{}: onWebSocketConnect('{}')", getDeviceName(), session);
this.session = session; this.session = session;
commandExecutor = new CommandExecutor(this); commandExecutor = new CommandExecutor(this);
@ -367,88 +374,106 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
} }
@Override @Override
public void onWebSocketError(Throwable e) { public void onWebSocketError(@Nullable Throwable e) {
logger.debug("{}: Error during websocket communication: {}", getDeviceName(), e.getMessage(), e); Throwable localThrowable = (e != null) ? e
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); : new IllegalStateException("Null Exception passed to onWebSocketError");
if (commandExecutor != null) { logger.debug("{}: Error during websocket communication: {}", getDeviceName(), localThrowable.getMessage(),
commandExecutor.postOperationMode(OperationModeType.OFFLINE); localThrowable);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, localThrowable.getMessage());
CommandExecutor localCommandExecutor = commandExecutor;
if (localCommandExecutor != null) {
localCommandExecutor.postOperationMode(OperationModeType.OFFLINE);
commandExecutor = null; commandExecutor = null;
} }
if (session != null) { Session localSession = session;
session.close(StatusCode.SERVER_ERROR, getDeviceName() + ": Failure: " + e.getMessage()); if (localSession != null) {
localSession.close(StatusCode.SERVER_ERROR, getDeviceName() + ": Failure: " + localThrowable.getMessage());
session = null; session = null;
} }
} }
@Override @Override
public void onWebSocketText(String msg) { public void onWebSocketText(@Nullable String msg) {
logger.debug("{}: onWebSocketText('{}')", getDeviceName(), msg); logger.debug("{}: onWebSocketText('{}')", getDeviceName(), msg);
try { try {
xmlResponseProcessor.handleMessage(msg); String localMessage = msg;
if (localMessage != null) {
xmlResponseProcessor.handleMessage(localMessage);
}
} catch (Exception e) { } catch (Exception e) {
logger.warn("{}: Could not parse XML from string '{}'.", getDeviceName(), msg, e); logger.warn("{}: Could not parse XML from string '{}'.", getDeviceName(), msg, e);
} }
} }
@Override @Override
public void onWebSocketBinary(byte[] arr, int pos, int len) { public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
// we don't expect binary data so just dump if we get some... // we don't expect binary data so just dump if we get some...
logger.debug("{}: onWebSocketBinary({}, {}, '{}')", getDeviceName(), pos, len, Arrays.toString(arr)); logger.debug("{}: onWebSocketBinary({}, {}, '{}')", getDeviceName(), offset, len, Arrays.toString(payload));
} }
@Override @Override
public void onWebSocketClose(int code, String reason) { public void onWebSocketClose(int code, @Nullable String reason) {
logger.debug("{}: onClose({}, '{}')", getDeviceName(), code, reason); logger.debug("{}: onClose({}, '{}')", getDeviceName(), code, reason);
missedPongsCount = 0; missedPongsCount = 0;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
if (commandExecutor != null) { CommandExecutor localCommandExecutor = commandExecutor;
commandExecutor.postOperationMode(OperationModeType.OFFLINE); if (localCommandExecutor != null) {
localCommandExecutor.postOperationMode(OperationModeType.OFFLINE);
} }
} }
@Override @Override
public void onWebSocketFrame(Frame frame) { public void onWebSocketFrame(@Nullable Frame frame) {
if (frame.getType() == Type.PONG) { Frame localFrame = frame;
if (localFrame != null) {
if (localFrame.getType() == Type.PONG) {
missedPongsCount = 0; missedPongsCount = 0;
} }
} }
}
private synchronized void openConnection() { private synchronized void openConnection() {
closeConnection(); closeConnection();
try { try {
client = new WebSocketClient(); WebSocketClient localClient = new WebSocketClient();
// we need longer timeouts for web socket. // we need longer timeouts for web socket.
client.setMaxIdleTimeout(360 * 1000); localClient.setMaxIdleTimeout(360 * 1000);
// Port seems to be hard coded, therefore no user input or discovery is necessary // Port seems to be hard coded, therefore no user input or discovery is necessary
String wsUrl = "ws://" + getIPAddress() + ":8080/"; String wsUrl = "ws://" + getIPAddress() + ":8080/";
logger.debug("{}: Connecting to: {}", getDeviceName(), wsUrl); logger.debug("{}: Connecting to: {}", getDeviceName(), wsUrl);
ClientUpgradeRequest request = new ClientUpgradeRequest(); ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("gabbo"); request.setSubProtocols("gabbo");
client.setStopTimeout(1000); localClient.setStopTimeout(1000);
client.start(); localClient.start();
sessionFuture = client.connect(this, new URI(wsUrl), request); sessionFuture = localClient.connect(this, new URI(wsUrl), request);
client = localClient;
} catch (Exception e) { } catch (Exception e) {
onWebSocketError(e); onWebSocketError(e);
} }
} }
private synchronized void closeConnection() { private synchronized void closeConnection() {
if (session != null) { Session localSession = this.session;
if (localSession != null) {
try { try {
session.close(StatusCode.NORMAL, "Binding shutdown"); localSession.close(StatusCode.NORMAL, "Binding shutdown");
} catch (Exception e) { } catch (Exception e) {
logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(), logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(),
e.getClass().getName(), e.getMessage()); e.getClass().getName(), e.getMessage());
} }
session = null; session = null;
} }
if (sessionFuture != null && !sessionFuture.isDone()) { Future<?> localSessionFuture = sessionFuture;
sessionFuture.cancel(true); if (localSessionFuture != null) {
if (!localSessionFuture.isDone()) {
localSessionFuture.cancel(true);
} }
if (client != null) { }
WebSocketClient localClient = client;
if (localClient != null) {
try { try {
client.stop(); localClient.stop();
client.destroy(); localClient.destroy();
} catch (Exception e) { } catch (Exception e) {
logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(), logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(),
e.getClass().getName(), e.getMessage()); e.getClass().getName(), e.getMessage());
@ -464,12 +489,13 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
|| commandExecutor == null) { || commandExecutor == null) {
openConnection(); // try to reconnect.... openConnection(); // try to reconnect....
} }
Session localSession = this.session;
if (getThing().getStatus() == ThingStatus.ONLINE && this.session != null && this.session.isOpen()) { if (localSession != null) {
if (getThing().getStatus() == ThingStatus.ONLINE && localSession.isOpen()) {
try { try {
this.session.getRemote().sendPing(null); localSession.getRemote().sendPing(null);
missedPongsCount++; missedPongsCount++;
} catch (IOException | NullPointerException e) { } catch (IOException e) {
onWebSocketError(e); onWebSocketError(e);
closeConnection(); closeConnection();
openConnection(); openConnection();
@ -484,6 +510,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
} }
} }
} }
}
public void refreshPresetChannel() { public void refreshPresetChannel() {
List<StateOption> stateOptions = presetContainer.getAllPresets().stream().map(e -> e.toStateOption()) List<StateOption> stateOptions = presetContainer.getAllPresets().stream().map(e -> e.toStateOption())
@ -494,7 +521,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
public void handleGroupUpdated(BoseSoundTouchConfiguration masterPlayerConfiguration) { public void handleGroupUpdated(BoseSoundTouchConfiguration masterPlayerConfiguration) {
String deviceId = getMacAddress(); String deviceId = getMacAddress();
if (masterPlayerConfiguration != null && masterPlayerConfiguration.macAddress != null) { if (masterPlayerConfiguration.macAddress != null) {
// Stereo pair // Stereo pair
if (Objects.equals(masterPlayerConfiguration.macAddress, deviceId)) { if (Objects.equals(masterPlayerConfiguration.macAddress, deviceId)) {
if (getThing().getThingTypeUID().equals(BST_10_THING_TYPE_UID)) { if (getThing().getThingTypeUID().equals(BST_10_THING_TYPE_UID)) {