mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[bosesoundtouch] Improve SAT errors and remove dependency (#13842)
Signed-off-by: Leo Siepel <leosiepel@gmail.com>
This commit is contained in:
parent
92310fab2f
commit
db86d291da
@ -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"),
|
||||
|
@ -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();
|
||||
|
@ -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";
|
||||
|
@ -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 = "";
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 :
|
||||
* & - &
|
||||
* < - <
|
||||
* > - >
|
||||
* " - "
|
||||
* ' - '
|
||||
*/
|
||||
private String escapeXml(String xml) {
|
||||
xml = xml.replaceAll("&", "&");
|
||||
xml = xml.replaceAll("<", "<");
|
||||
xml = xml.replaceAll(">", ">");
|
||||
xml = xml.replaceAll("\"", """);
|
||||
xml = xml.replaceAll("'", "'");
|
||||
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 : "";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 "";
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
Loading…
Reference in New Issue
Block a user