[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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link APIRequest} class handles the API requests
*
* @author Thomas Traunbauer - Initial contribution
*/
@NonNullByDefault
public enum APIRequest {
KEY("key"),
SELECT("select"),

View File

@ -12,11 +12,15 @@
*/
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
*
* @author Thomas Traunbauer - Initial contribution
*/
@NonNullByDefault
public interface AvailableSources {
public boolean isBluetoothAvailable();

View File

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

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
/**
@ -19,6 +21,7 @@ import org.openhab.core.thing.Thing;
*
* @author Ivaylo Ivanov - Initial contribution
*/
@NonNullByDefault
public class BoseSoundTouchConfiguration {
// Device configuration parameters;
@ -26,10 +29,10 @@ public class BoseSoundTouchConfiguration {
public static final String MAC_ADDRESS = Thing.PROPERTY_MAC_ADDRESS;
public static final String APP_KEY = "appKey";
public String host;
public String macAddress;
public String appKey;
public @Nullable String host;
public @Nullable String macAddress;
public @Nullable String appKey;
// 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
@ -31,11 +33,12 @@ import org.osgi.service.component.annotations.Reference;
*
* @author Christian Niessner - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bosesoundtouch")
public class BoseSoundTouchHandlerFactory extends BaseThingHandlerFactory {
private StorageService storageService;
private BoseStateDescriptionOptionProvider stateOptionProvider;
private @Nullable StorageService storageService;
private @Nullable BoseStateDescriptionOptionProvider stateOptionProvider;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@ -43,12 +46,19 @@ public class BoseSoundTouchHandlerFactory extends BaseThingHandlerFactory {
}
@Override
protected ThingHandler createHandler(Thing thing) {
Storage<ContentItem> storage = storageService.getStorage(thing.getUID().toString(),
ContentItem.class.getClassLoader());
BoseSoundTouchHandler handler = new BoseSoundTouchHandler(thing, new PresetContainer(storage),
stateOptionProvider);
return handler;
protected @Nullable ThingHandler createHandler(Thing thing) {
StorageService localStorageService = storageService;
if (localStorageService != null) {
Storage<ContentItem> storage = localStorageService.getStorage(thing.getUID().toString(),
ContentItem.class.getClassLoader());
BoseStateDescriptionOptionProvider localDescriptionOptionProvider = stateOptionProvider;
if (localDescriptionOptionProvider != null) {
BoseSoundTouchHandler handler = new BoseSoundTouchHandler(thing, new PresetContainer(storage),
localDescriptionOptionProvider);
return handler;
}
}
return null;
}
@Reference

View File

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

View File

@ -12,11 +12,15 @@
*/
package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Configuration class for soundtouch notification channel
*
* @author Ivaylo Ivanov - Initial contribution
*/
@NonNullByDefault
public class BoseSoundTouchNotificationChannelConfiguration {
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_MESSAGE = "notificationMessage";
public Integer notificationVolume;
public String notificationService;
public String notificationReason;
public String notificationMessage;
public @Nullable Integer notificationVolume;
public @Nullable String notificationService;
public @Nullable String notificationReason;
public @Nullable String notificationMessage;
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) {

View File

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

View File

@ -16,7 +16,8 @@ import java.util.HashMap;
import java.util.Map;
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 com.google.gson.annotations.Expose;
@ -27,31 +28,18 @@ import com.google.gson.annotations.Expose;
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
@NonNullByDefault
public class ContentItem {
private String source;
private String sourceAccount;
private String location;
private boolean presetable;
private String itemName;
private int presetID;
private String containerArt;
private String source = "";
private @Nullable String sourceAccount;
private @Nullable String location;
private boolean presetable = false;
private @Nullable String itemName;
private int presetID = 0;
private @Nullable String containerArt;
@Expose
private final Map<String, String> additionalAttributes;
/**
* 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<>();
}
private final Map<String, String> additionalAttributes = new HashMap<>();
/**
* Returns true if this ContentItem is defined as Preset
@ -74,11 +62,13 @@ public class ContentItem {
public boolean isValid() {
if (getOperationMode() == OperationModeType.STANDBY) {
return true;
}
if (itemName == null || source == null || itemName.isEmpty() || source.isEmpty()) {
return false;
} 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
*/
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (obj instanceof ContentItem) {
ContentItem other = (ContentItem) obj;
if (!Objects.equals(other.source, this.source)) {
return false;
}
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 Objects.equals(other.source, this.source) || Objects.equals(other.sourceAccount, this.sourceAccount)
|| other.presetable == this.presetable || Objects.equals(other.location, this.location)
|| Objects.equals(other.itemName, this.itemName);
}
return super.equals(obj);
}
@ -118,15 +95,18 @@ public class ContentItem {
*/
public OperationModeType getOperationMode() {
OperationModeType operationMode = OperationModeType.OTHER;
if (source == null || source.equals("")) {
if ("".equals(source)) {
return OperationModeType.OTHER;
}
if (source.contains("PRODUCT")) {
if (sourceAccount.contains("TV")) {
operationMode = OperationModeType.TV;
}
if (sourceAccount.contains("HDMI")) {
operationMode = OperationModeType.HDMI1;
String localSourceAccount = sourceAccount;
if (localSourceAccount != null) {
if (localSourceAccount.contains("TV")) {
operationMode = OperationModeType.TV;
}
if (localSourceAccount.contains("HDMI")) {
operationMode = OperationModeType.HDMI1;
}
}
return operationMode;
}
@ -174,15 +154,15 @@ public class ContentItem {
return source;
}
public String getSourceAccount() {
public @Nullable String getSourceAccount() {
return sourceAccount;
}
public String getLocation() {
public @Nullable String getLocation() {
return location;
}
public String getItemName() {
public @Nullable String getItemName() {
return itemName;
}
@ -194,10 +174,28 @@ public class ContentItem {
return presetID;
}
public String getContainerArt() {
public @Nullable String getContainerArt() {
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
*
@ -223,19 +221,20 @@ public class ContentItem {
break;
default:
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) {
sbXml.append(" location=\"").append(StringEscapeUtils.escapeXml(location)).append("\"");
}
if (sourceAccount != null) {
sbXml.append(" sourceAccount=\"").append(StringEscapeUtils.escapeXml(sourceAccount)).append("\"");
String localSourceAccount = sourceAccount;
if (localSourceAccount != null) {
sbXml.append(" sourceAccount=\"").append(escapeXml(localSourceAccount)).append("\"");
}
sbXml.append(" isPresetable=\"").append(presetable).append("\"");
for (Map.Entry<String, String> aae : additionalAttributes.entrySet()) {
sbXml.append(" ").append(aae.getKey()).append("=\"")
.append(StringEscapeUtils.escapeXml(aae.getValue())).append("\"");
sbXml.append(" ").append(aae.getKey()).append("=\"").append(escapeXml(aae.getValue())).append("\"");
}
sbXml.append(">");
if (itemName != null) {
@ -264,6 +263,7 @@ public class ContentItem {
// buffer.append(presetID);
// 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ContentItemMaker} class makes ContentItems for sources
*
* @author Thomas Traunbauer - Initial contribution
*/
@NonNullByDefault
public class ContentItemMaker {
private final PresetContainer presetContainer;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,11 +12,14 @@
*/
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
*
* @author Christian Niessner - Initial contribution
*/
@NonNullByDefault
public enum RemoteKeyType {
PLAY,
PAUSE,

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ package org.openhab.binding.bosesoundtouch.internal.discovery;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
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
*/
@NonNullByDefault
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
*/
public static String getContentOfFirstElement(String content, String element) {
if (content == null) {
return "";
}
String beginTag = "<" + element + ">";
String endTag = "</" + element + ">";
@ -39,7 +38,8 @@ public class DiscoveryUtil {
int endIndex = content.indexOf(endTag);
if (startIndex != -1 && endIndex != -1) {
return content.substring(startIndex, endIndex);
String result = content.substring(startIndex, endIndex);
return result != null ? result : "";
} else {
return "";
}

View File

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

View File

@ -27,6 +27,8 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
@ -75,6 +77,7 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - code clean up
* @author Alexander Kostadinov - Handling of websocket ping-pong mechanism for thing status check
*/
@NonNullByDefault
public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocketListener, WebSocketFrameListener {
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 ScheduledFuture<?> connectionChecker;
private WebSocketClient client;
private volatile Session session;
private volatile CommandExecutor commandExecutor;
private @Nullable ScheduledFuture<?> connectionChecker;
private @Nullable WebSocketClient client;
private @Nullable volatile Session session;
private @Nullable volatile CommandExecutor commandExecutor;
private volatile int missedPongsCount = 0;
private XMLResponseProcessor xmlResponseProcessor;
@ -94,7 +97,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
private PresetContainer presetContainer;
private BoseStateDescriptionOptionProvider stateOptionProvider;
private Future<?> sessionFuture;
private @Nullable Future<?> sessionFuture;
/**
* Creates a new instance of this class for the {@link Thing}.
@ -120,9 +123,12 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
@Override
public void dispose() {
if (connectionChecker != null && !connectionChecker.isCancelled()) {
connectionChecker.cancel(true);
connectionChecker = null;
ScheduledFuture<?> localConnectionChecker = connectionChecker;
if (localConnectionChecker != null) {
if (!localConnectionChecker.isCancelled()) {
localConnectionChecker.cancel(true);
connectionChecker = null;
}
}
closeConnection();
super.dispose();
@ -146,7 +152,8 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
@Override
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.",
getDeviceName(), command, channelUID);
return;
@ -157,7 +164,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
if (command.equals(RefreshType.REFRESH)) {
switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_BASS:
commandExecutor.getInformations(APIRequest.BASS);
localCommandExecutor.getInformations(APIRequest.BASS);
break;
case CHANNEL_KEY_CODE:
// refresh makes no sense... ?
@ -174,10 +181,10 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
case CHANNEL_RATEENABLED:
case CHANNEL_SKIPENABLED:
case CHANNEL_SKIPPREVIOUSENABLED:
commandExecutor.getInformations(APIRequest.NOW_PLAYING);
localCommandExecutor.getInformations(APIRequest.NOW_PLAYING);
break;
case CHANNEL_VOLUME:
commandExecutor.getInformations(APIRequest.VOLUME);
localCommandExecutor.getInformations(APIRequest.VOLUME);
break;
default:
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()) {
case CHANNEL_POWER:
if (command instanceof OnOffType) {
commandExecutor.postPower((OnOffType) command);
localCommandExecutor.postPower((OnOffType) command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_VOLUME:
if (command instanceof PercentType) {
commandExecutor.postVolume((PercentType) command);
localCommandExecutor.postVolume((PercentType) command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_MUTE:
if (command instanceof OnOffType) {
commandExecutor.postVolumeMuted((OnOffType) command);
localCommandExecutor.postVolumeMuted((OnOffType) command);
} else {
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();
try {
OperationModeType mode = OperationModeType.valueOf(cmd);
commandExecutor.postOperationMode(mode);
localCommandExecutor.postOperationMode(mode);
} catch (IllegalArgumentException iae) {
logger.warn("{}: OperationMode \"{}\" is not valid!", getDeviceName(), cmd);
}
@ -220,28 +227,28 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
break;
case CHANNEL_PLAYER_CONTROL:
if ((command instanceof PlayPauseType) || (command instanceof NextPreviousType)) {
commandExecutor.postPlayerControl(command);
localCommandExecutor.postPlayerControl(command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_PRESET:
if (command instanceof DecimalType) {
commandExecutor.postPreset((DecimalType) command);
localCommandExecutor.postPreset((DecimalType) command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_BASS:
if (command instanceof DecimalType) {
commandExecutor.postBass((DecimalType) command);
localCommandExecutor.postBass((DecimalType) command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_SAVE_AS_PRESET:
if (command instanceof DecimalType) {
commandExecutor.addCurrentContentItemToPresetContainer((DecimalType) command);
localCommandExecutor.addCurrentContentItemToPresetContainer((DecimalType) command);
} else {
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();
try {
RemoteKeyType keyCommand = RemoteKeyType.valueOf(cmd);
commandExecutor.postRemoteKey(keyCommand);
localCommandExecutor.postRemoteKey(keyCommand);
} catch (IllegalArgumentException e) {
logger.debug("{}: Unhandled remote key: {}", getDeviceName(), cmd);
}
@ -262,7 +269,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
if (channel != null) {
ChannelTypeUID chTypeUid = channel.getChannelTypeUID();
if (chTypeUid != null) {
switch (channel.getChannelTypeUID().getId()) {
switch (chTypeUid.getId()) {
case CHANNEL_NOTIFICATION_SOUND:
String appKey = Objects.toString(getConfig().get(BoseSoundTouchConfiguration.APP_KEY),
null);
@ -273,8 +280,8 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
.getConfiguration()
.as(BoseSoundTouchNotificationChannelConfiguration.class);
if (!url.isEmpty()) {
commandExecutor.playNotificationSound(appKey, notificationConfiguration,
url);
localCommandExecutor.playNotificationSound(appKey,
notificationConfiguration, url);
}
}
} else {
@ -295,7 +302,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
*
* @return the CommandExecutor of this handler
*/
public CommandExecutor getCommandExecutor() {
public @Nullable CommandExecutor getCommandExecutor() {
return commandExecutor;
}
@ -304,7 +311,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
*
* @return the Session this handler has opened
*/
public Session getSession() {
public @Nullable Session getSession() {
return session;
}
@ -313,7 +320,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
*
* @return the name of the device delivered from itself
*/
public String getDeviceName() {
public @Nullable String getDeviceName() {
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
*/
public String getDeviceType() {
public @Nullable String getDeviceType() {
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")
*/
public String getMacAddress() {
public @Nullable String getMacAddress() {
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
*/
public String getIPAddress() {
public @Nullable String getIPAddress() {
return (String) getThing().getConfiguration().getProperties().get(BoseSoundTouchConfiguration.HOST);
}
@ -359,7 +366,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
}
@Override
public void onWebSocketConnect(Session session) {
public void onWebSocketConnect(@Nullable Session session) {
logger.debug("{}: onWebSocketConnect('{}')", getDeviceName(), session);
this.session = session;
commandExecutor = new CommandExecutor(this);
@ -367,88 +374,106 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
}
@Override
public void onWebSocketError(Throwable e) {
logger.debug("{}: Error during websocket communication: {}", getDeviceName(), e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
if (commandExecutor != null) {
commandExecutor.postOperationMode(OperationModeType.OFFLINE);
public void onWebSocketError(@Nullable Throwable e) {
Throwable localThrowable = (e != null) ? e
: new IllegalStateException("Null Exception passed to onWebSocketError");
logger.debug("{}: Error during websocket communication: {}", getDeviceName(), localThrowable.getMessage(),
localThrowable);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, localThrowable.getMessage());
CommandExecutor localCommandExecutor = commandExecutor;
if (localCommandExecutor != null) {
localCommandExecutor.postOperationMode(OperationModeType.OFFLINE);
commandExecutor = null;
}
if (session != null) {
session.close(StatusCode.SERVER_ERROR, getDeviceName() + ": Failure: " + e.getMessage());
Session localSession = session;
if (localSession != null) {
localSession.close(StatusCode.SERVER_ERROR, getDeviceName() + ": Failure: " + localThrowable.getMessage());
session = null;
}
}
@Override
public void onWebSocketText(String msg) {
public void onWebSocketText(@Nullable String msg) {
logger.debug("{}: onWebSocketText('{}')", getDeviceName(), msg);
try {
xmlResponseProcessor.handleMessage(msg);
String localMessage = msg;
if (localMessage != null) {
xmlResponseProcessor.handleMessage(localMessage);
}
} catch (Exception e) {
logger.warn("{}: Could not parse XML from string '{}'.", getDeviceName(), msg, e);
}
}
@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...
logger.debug("{}: onWebSocketBinary({}, {}, '{}')", getDeviceName(), pos, len, Arrays.toString(arr));
logger.debug("{}: onWebSocketBinary({}, {}, '{}')", getDeviceName(), offset, len, Arrays.toString(payload));
}
@Override
public void onWebSocketClose(int code, String reason) {
public void onWebSocketClose(int code, @Nullable String reason) {
logger.debug("{}: onClose({}, '{}')", getDeviceName(), code, reason);
missedPongsCount = 0;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
if (commandExecutor != null) {
commandExecutor.postOperationMode(OperationModeType.OFFLINE);
CommandExecutor localCommandExecutor = commandExecutor;
if (localCommandExecutor != null) {
localCommandExecutor.postOperationMode(OperationModeType.OFFLINE);
}
}
@Override
public void onWebSocketFrame(Frame frame) {
if (frame.getType() == Type.PONG) {
missedPongsCount = 0;
public void onWebSocketFrame(@Nullable Frame frame) {
Frame localFrame = frame;
if (localFrame != null) {
if (localFrame.getType() == Type.PONG) {
missedPongsCount = 0;
}
}
}
private synchronized void openConnection() {
closeConnection();
try {
client = new WebSocketClient();
WebSocketClient localClient = new WebSocketClient();
// 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
String wsUrl = "ws://" + getIPAddress() + ":8080/";
logger.debug("{}: Connecting to: {}", getDeviceName(), wsUrl);
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols("gabbo");
client.setStopTimeout(1000);
client.start();
sessionFuture = client.connect(this, new URI(wsUrl), request);
localClient.setStopTimeout(1000);
localClient.start();
sessionFuture = localClient.connect(this, new URI(wsUrl), request);
client = localClient;
} catch (Exception e) {
onWebSocketError(e);
}
}
private synchronized void closeConnection() {
if (session != null) {
Session localSession = this.session;
if (localSession != null) {
try {
session.close(StatusCode.NORMAL, "Binding shutdown");
localSession.close(StatusCode.NORMAL, "Binding shutdown");
} catch (Exception e) {
logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(),
e.getClass().getName(), e.getMessage());
}
session = null;
}
if (sessionFuture != null && !sessionFuture.isDone()) {
sessionFuture.cancel(true);
Future<?> localSessionFuture = sessionFuture;
if (localSessionFuture != null) {
if (!localSessionFuture.isDone()) {
localSessionFuture.cancel(true);
}
}
if (client != null) {
WebSocketClient localClient = client;
if (localClient != null) {
try {
client.stop();
client.destroy();
localClient.stop();
localClient.destroy();
} catch (Exception e) {
logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(),
e.getClass().getName(), e.getMessage());
@ -464,23 +489,25 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
|| commandExecutor == null) {
openConnection(); // try to reconnect....
}
Session localSession = this.session;
if (localSession != null) {
if (getThing().getStatus() == ThingStatus.ONLINE && localSession.isOpen()) {
try {
localSession.getRemote().sendPing(null);
missedPongsCount++;
} catch (IOException e) {
onWebSocketError(e);
closeConnection();
openConnection();
}
if (getThing().getStatus() == ThingStatus.ONLINE && this.session != null && this.session.isOpen()) {
try {
this.session.getRemote().sendPing(null);
missedPongsCount++;
} catch (IOException | NullPointerException e) {
onWebSocketError(e);
closeConnection();
openConnection();
}
if (missedPongsCount >= MAX_MISSED_PONGS_COUNT) {
logger.debug("{}: Closing connection because of too many missed PONGs: {} (max allowed {}) ",
getDeviceName(), missedPongsCount, MAX_MISSED_PONGS_COUNT);
missedPongsCount = 0;
closeConnection();
openConnection();
if (missedPongsCount >= MAX_MISSED_PONGS_COUNT) {
logger.debug("{}: Closing connection because of too many missed PONGs: {} (max allowed {}) ",
getDeviceName(), missedPongsCount, MAX_MISSED_PONGS_COUNT);
missedPongsCount = 0;
closeConnection();
openConnection();
}
}
}
}
@ -494,7 +521,7 @@ public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocket
public void handleGroupUpdated(BoseSoundTouchConfiguration masterPlayerConfiguration) {
String deviceId = getMacAddress();
if (masterPlayerConfiguration != null && masterPlayerConfiguration.macAddress != null) {
if (masterPlayerConfiguration.macAddress != null) {
// Stereo pair
if (Objects.equals(masterPlayerConfiguration.macAddress, deviceId)) {
if (getThing().getThingTypeUID().equals(BST_10_THING_TYPE_UID)) {