added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bosesoundtouch-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-bosesoundtouch" description="Bose SoundTouch Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-http</feature>
<feature>openhab-transport-mdns</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bosesoundtouch/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link APIRequest} class handles the API requests
*
* @author Thomas Traunbauer - Initial contribution
*/
public enum APIRequest {
KEY("key"),
SELECT("select"),
SOURCES("sources"),
BASSCAPABILITIES("bassCapabilities"),
BASS("bass"),
GET_ZONE("getZone"),
SET_ZONE("setZone"),
ADD_ZONE_SLAVE("addZoneSlave"),
REMOVE_ZONE_SLAVE("removeZoneSlave"),
NOW_PLAYING("now_playing"),
TRACK_INFO("trackInfo"),
VOLUME("volume"),
PRESETS("presets"),
INFO("info"),
NAME("name"),
GET_GROUP("getGroup");
private String name;
private APIRequest(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link AvailableSources} is used to find out, which sources and functions are available
*
* @author Thomas Traunbauer - Initial contribution
*/
public interface AvailableSources {
public boolean isBluetoothAvailable();
public boolean isAUXAvailable();
public boolean isAUX1Available();
public boolean isAUX2Available();
public boolean isAUX3Available();
public boolean isTVAvailable();
public boolean isHDMI1Available();
public boolean isInternetRadioAvailable();
public boolean isStoredMusicAvailable();
public boolean isBassAvailable();
public void setAUXAvailable(boolean aux);
public void setAUX1Available(boolean aux1);
public void setAUX2Available(boolean aux2);
public void setAUX3Available(boolean aux3);
public void setStoredMusicAvailable(boolean storedMusic);
public void setInternetRadioAvailable(boolean internetRadio);
public void setBluetoothAvailable(boolean bluetooth);
public void setTVAvailable(boolean tv);
public void setHDMI1Available(boolean hdmi1);
public void setBassAvailable(boolean bass);
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BoseSoundTouchBindinConstantsg} class defines common constants, which are
* used across the whole binding.
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
public class BoseSoundTouchBindingConstants {
public static final String BINDING_ID = "bosesoundtouch";
// List of all Thing Type UIDs
public static final ThingTypeUID BST_UNKNOWN_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "device");
public static final ThingTypeUID BST_10_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "10");
public static final ThingTypeUID BST_20_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "20");
public static final ThingTypeUID BST_30_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "30");
public static final ThingTypeUID BST_300_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "300");
public static final ThingTypeUID BST_WLA_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "wirelessLinkAdapter");
public static final ThingTypeUID BST_WSMS_THING_TYPE_UID = new ThingTypeUID(BINDING_ID,
"waveSoundTouchMusicSystemIV");
public static final ThingTypeUID BST_SA5A_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "sa5Amplifier");
public static final Set<ThingTypeUID> SUPPORTED_KNOWN_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(BST_UNKNOWN_THING_TYPE_UID, BST_10_THING_TYPE_UID, BST_20_THING_TYPE_UID, BST_30_THING_TYPE_UID,
BST_300_THING_TYPE_UID, BST_WLA_THING_TYPE_UID, BST_WSMS_THING_TYPE_UID, BST_SA5A_THING_TYPE_UID)
.collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>(SUPPORTED_KNOWN_THING_TYPES_UIDS);
// Partial list of Channel Type IDs
public static final String CHANNEL_TYPE_OPERATION_MODE_DEFAULT = "operationMode_default";
public static final String CHANNEL_TYPE_OPERATION_MODE_BST_10_20_30 = "operationMode_BST_10_20_30";
public static final String CHANNEL_TYPE_OPERATION_MODE_BST_300 = "operationMode_BST_300";
public static final String CHANNEL_TYPE_OPERATION_MODE_BST_SA5A = "operationMode_BST_SA5_Amplifier";
public static final String CHANNEL_TYPE_OPERATION_MODE_BST_WLA = "operationMode_BST_WLA";
// List of all Channel IDs
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_VOLUME = "volume";
public static final String CHANNEL_MUTE = "mute";
public static final String CHANNEL_OPERATIONMODE = "operationMode";
public static final String CHANNEL_PLAYER_CONTROL = "playerControl";
public static final String CHANNEL_PRESET = "preset";
public static final String CHANNEL_BASS = "bass";
public static final String CHANNEL_RATEENABLED = "rateEnabled";
public static final String CHANNEL_SKIPENABLED = "skipEnabled";
public static final String CHANNEL_SKIPPREVIOUSENABLED = "skipPreviousEnabled";
public static final String CHANNEL_SAVE_AS_PRESET = "saveAsPreset";
public static final String CHANNEL_KEY_CODE = "keyCode";
public static final String CHANNEL_NOWPLAYING_ALBUM = "nowPlayingAlbum";
public static final String CHANNEL_NOWPLAYING_ARTWORK = "nowPlayingArtwork";
public static final String CHANNEL_NOWPLAYING_ARTIST = "nowPlayingArtist";
public static final String CHANNEL_NOWPLAYING_DESCRIPTION = "nowPlayingDescription";
public static final String CHANNEL_NOWPLAYING_GENRE = "nowPlayingGenre";
public static final String CHANNEL_NOWPLAYING_ITEMNAME = "nowPlayingItemName";
public static final String CHANNEL_NOWPLAYING_STATIONLOCATION = "nowPlayingStationLocation";
public static final String CHANNEL_NOWPLAYING_STATIONNAME = "nowPlayingStationName";
public static final String CHANNEL_NOWPLAYING_TRACK = "nowPlayingTrack";
public static final String CHANNEL_NOTIFICATION_SOUND = "notificationsound";
public static final List<String> CHANNEL_IDS = Collections.unmodifiableList(
Stream.of(CHANNEL_POWER, CHANNEL_VOLUME, CHANNEL_MUTE, CHANNEL_OPERATIONMODE, CHANNEL_PLAYER_CONTROL,
CHANNEL_PRESET, CHANNEL_BASS, CHANNEL_RATEENABLED, CHANNEL_SKIPENABLED, CHANNEL_SKIPPREVIOUSENABLED,
CHANNEL_SAVE_AS_PRESET, CHANNEL_KEY_CODE, CHANNEL_NOWPLAYING_ALBUM, CHANNEL_NOWPLAYING_ARTWORK,
CHANNEL_NOWPLAYING_ARTIST, CHANNEL_NOWPLAYING_DESCRIPTION, CHANNEL_NOWPLAYING_GENRE,
CHANNEL_NOWPLAYING_ITEMNAME, CHANNEL_NOWPLAYING_STATIONLOCATION, CHANNEL_NOWPLAYING_STATIONNAME,
CHANNEL_NOWPLAYING_TRACK, CHANNEL_NOTIFICATION_SOUND).collect(Collectors.toList()));
// Device information parameters;
public static final String DEVICE_INFO_NAME = "INFO_NAME";
public static final String DEVICE_INFO_TYPE = "INFO_TYPE";
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import org.openhab.core.thing.Thing;
/**
* Configuration class for soundtouch
*
* @author Ivaylo Ivanov - Initial contribution
*/
public class BoseSoundTouchConfiguration {
// Device configuration parameters;
public static final String HOST = "host";
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;
// Not an actual configuration field, but it will contain the name of the group (in case of Stereo Pair)
public String groupName;
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.SUPPORTED_THING_TYPES_UIDS;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link BoseSoundTouchHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Christian Niessner - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bosesoundtouch")
public class BoseSoundTouchHandlerFactory extends BaseThingHandlerFactory {
private StorageService storageService;
private BoseStateDescriptionOptionProvider stateOptionProvider;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@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;
}
@Reference
protected void setStorageService(StorageService storageService) {
this.storageService = storageService;
}
protected void unsetStorageService(StorageService storageService) {
this.storageService = null;
}
@Reference
protected void setPresetChannelTypeProvider(BoseStateDescriptionOptionProvider stateOptionProvider) {
this.stateOptionProvider = stateOptionProvider;
}
protected void unsetPresetChannelTypeProvider(BoseStateDescriptionOptionProvider stateOptionProvider) {
this.stateOptionProvider = null;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link BoseSoundTouchNotFoundException} class is an exception
*
* @author Thomas Traunbauer - Initial contribution
*/
public class BoseSoundTouchNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
public BoseSoundTouchNotFoundException() {
super();
}
public BoseSoundTouchNotFoundException(String message) {
super(message);
}
public BoseSoundTouchNotFoundException(String message, Throwable cause) {
super(message, cause);
}
public BoseSoundTouchNotFoundException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* Configuration class for soundtouch notification channel
*
* @author Ivaylo Ivanov - Initial contribution
*/
public class BoseSoundTouchNotificationChannelConfiguration {
public static final String MIN_FIRMWARE = "14";
public static final String MODEL_TYPE = "sm2";
public static final String NOTIFICATION_VOLUME = "notificationVolume";
public static final String NOTIFICATION_SERVICE = "notificationService";
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 static boolean isSupportedFirmware(String firmware) {
return firmware != null && firmware.compareTo(MIN_FIRMWARE) > 0;
}
public static boolean isSupportedHardware(String hardware) {
return MODEL_TYPE.equals(hardware);
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Ivaylo Ivanov - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, BoseStateDescriptionOptionProvider.class })
@NonNullByDefault
public class BoseStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,530 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CommandExecutor} class executes commands on the websocket
*
* @author Thomas Traunbauer - Initial contribution
* @author Kai Kreuzer - code clean up
*/
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 Map<String, Boolean> mapOfAvailableFunctions;
/**
* Creates a new instance of this class
*
* @param handler the handler that created this CommandExecutor
*/
public CommandExecutor(BoseSoundTouchHandler handler) {
this.handler = handler;
init();
}
/**
* Synchronizes the underlying storage container with the current value for the presets stored on the player
* by updating the available ones and deleting the cleared ones
*
* @param playerPresets a Map<Integer, ContentItems> containing the items currently stored on the player
*/
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);
}
} catch (ContentItemNotPresetableException e) {
logger.debug("{}: ContentItem is not presetable", handler.getDeviceName());
}
});
handler.refreshPresetChannel();
}
/**
* Adds a ContentItem to the PresetContainer
*
* @param id the id the ContentItem should be reached
* @param contentItem the contentItem that should be saved as PRESET. Note that a eventually set presetID of the
* ContentItem will be overwritten with id
*/
public void addContentItemToPresetContainer(int id, ContentItem contentItem) {
contentItem.setPresetID(id);
try {
handler.getPresetContainer().put(id, contentItem);
} catch (ContentItemNotPresetableException e) {
logger.debug("{}: ContentItem is not presetable", handler.getDeviceName());
}
handler.refreshPresetChannel();
}
/**
* Adds the current selected ContentItem to the PresetContainer
*
* @param command the command is a DecimalType, thats intValue will be used as id. The id the ContentItem should be
* reached
*/
public void addCurrentContentItemToPresetContainer(DecimalType command) {
if (command.intValue() > 6) {
addContentItemToPresetContainer(command.intValue(), currentContentItem);
} else {
logger.warn("{}: Only PresetID >6 is allowed", handler.getDeviceName());
}
}
/**
* Initializes a API Request on this device
*
* @param apiRequest the apiRequest thats informations should be collected
*/
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);
}
/**
* Sets the current ContentItem if it is valid, and inits an update of the operating values
*
* @param contentItem
*/
public void setCurrentContentItem(ContentItem contentItem) {
if ((contentItem != null) && (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())) {
psFound = ps;
}
}
}
int presetID = 0;
if (psFound != null) {
presetID = psFound.getPresetID();
}
contentItem.setPresetID(presetID);
currentContentItem = contentItem;
}
}
updateOperatingValues();
}
/**
* Sets the device is currently muted
*
* @param muted
*/
public void setCurrentMuted(boolean muted) {
currentMuted = muted;
}
/**
* Post Bass on the device
*
* @param command the command is Type of DecimalType
*/
public void postBass(DecimalType command) {
if (isBassAvailable()) {
sendPostRequestInWebSocket("bass",
"<bass deviceID=\"" + handler.getMacAddress() + "\"" + ">" + command.intValue() + "</bass>");
} else {
logger.warn("{}: Bass modification not supported for this device", handler.getDeviceName());
}
}
/**
* Post OperationMode on the device
*
* @param command the command is Type of OperationModeType
*/
public void postOperationMode(OperationModeType command) {
if (command == OperationModeType.STANDBY) {
postPower(OnOffType.OFF);
} else {
try {
ContentItemMaker contentItemMaker = new ContentItemMaker(this, handler.getPresetContainer());
ContentItem contentItem = contentItemMaker.getContentItem(command);
postContentItem(contentItem);
} catch (OperationModeNotAvailableException e) {
logger.warn("{}: OperationMode \"{}\" is not supported yet", handler.getDeviceName(),
command.toString());
} catch (NoInternetRadioPresetFoundException e) {
logger.warn("{}: Unable to switch to mode \"INTERNET_RADIO\". No PRESET defined",
handler.getDeviceName());
} catch (NoStoredMusicPresetFoundException e) {
logger.warn("{}: Unable to switch to mode: \"STORED_MUSIC\". No PRESET defined",
handler.getDeviceName());
}
updateOperatingValues();
}
}
/**
* Post PlayerControl on the device
*
* @param command the command is Type of Command
*/
public void postPlayerControl(Command command) {
if (command.equals(PlayPauseType.PLAY)) {
if (currentOperationMode == OperationModeType.STANDBY) {
postRemoteKey(RemoteKeyType.POWER);
} else {
postRemoteKey(RemoteKeyType.PLAY);
}
} else if (command.equals(PlayPauseType.PAUSE)) {
postRemoteKey(RemoteKeyType.PAUSE);
} else if (command.equals(NextPreviousType.NEXT)) {
postRemoteKey(RemoteKeyType.NEXT_TRACK);
} else if (command.equals(NextPreviousType.PREVIOUS)) {
postRemoteKey(RemoteKeyType.PREV_TRACK);
}
}
/**
* Post Power on the device
*
* @param command the command is Type of OnOffType
*/
public void postPower(OnOffType command) {
if (command.equals(OnOffType.ON)) {
if (currentOperationMode == OperationModeType.STANDBY) {
postRemoteKey(RemoteKeyType.POWER);
}
} else if (command.equals(OnOffType.OFF)) {
if (currentOperationMode != OperationModeType.STANDBY) {
postRemoteKey(RemoteKeyType.POWER);
}
}
updateOperatingValues();
}
/**
* Post Preset on the device
*
* @param command the command is Type of DecimalType
*/
public void postPreset(DecimalType command) {
ContentItem item = null;
try {
item = handler.getPresetContainer().get(command.intValue());
postContentItem(item);
} catch (NoPresetFoundException e) {
logger.warn("{}: No preset found at id: {}", handler.getDeviceName(), command.intValue());
}
}
/**
* Post RemoteKey on the device
*
* @param command the command is Type of RemoteKeyType
*/
public void postRemoteKey(RemoteKeyType key) {
sendPostRequestInWebSocket("key", "mainNode=\"keyPress\"",
"<key state=\"press\" sender=\"Gabbo\">" + key.name() + "</key>");
sendPostRequestInWebSocket("key", "mainNode=\"keyRelease\"",
"<key state=\"release\" sender=\"Gabbo\">" + key.name() + "</key>");
}
/**
* Post Volume on the device
*
* @param command the command is Type of PercentType
*/
public void postVolume(PercentType command) {
sendPostRequestInWebSocket("volume",
"<volume deviceID=\"" + handler.getMacAddress() + "\"" + ">" + command.intValue() + "</volume>");
}
/**
* Post VolumeMute on the device
*
* @param command the command is Type of OnOffType
*/
public void postVolumeMuted(OnOffType command) {
if (command.equals(OnOffType.ON)) {
if (!currentMuted) {
currentMuted = true;
postRemoteKey(RemoteKeyType.MUTE);
}
} else if (command.equals(OnOffType.OFF)) {
if (currentMuted) {
currentMuted = false;
postRemoteKey(RemoteKeyType.MUTE);
}
}
}
/**
* Update GUI for Basslevel
*
* @param state the state is Type of DecimalType
*/
public void updateBassLevelGUIState(DecimalType state) {
handler.updateState(CHANNEL_BASS, state);
}
/**
* Update GUI for Volume
*
* @param state the state is Type of PercentType
*/
public void updateVolumeGUIState(PercentType state) {
handler.updateState(CHANNEL_VOLUME, state);
}
/**
* Update GUI for OperationMode
*
* @param state the state is Type of StringType
*/
public void updateOperationModeGUIState(StringType state) {
handler.updateState(CHANNEL_OPERATIONMODE, state);
}
/**
* Update GUI for PlayerControl
*
* @param state the state is Type of State
*/
public void updatePlayerControlGUIState(State state) {
handler.updateState(CHANNEL_PLAYER_CONTROL, state);
}
/**
* Update GUI for Power
*
* @param state the state is Type of OnOffType
*/
public void updatePowerStateGUIState(OnOffType state) {
handler.updateState(CHANNEL_POWER, state);
}
/**
* Update GUI for Preset
*
* @param state the state is Type of DecimalType
*/
public void updatePresetGUIState(DecimalType state) {
handler.updateState(CHANNEL_PRESET, state);
}
private void init() {
getInformations(APIRequest.INFO);
currentOperationMode = OperationModeType.OFFLINE;
currentContentItem = null;
mapOfAvailableFunctions = new HashMap<>();
}
private void postContentItem(ContentItem contentItem) {
if (contentItem != null) {
setCurrentContentItem(contentItem);
sendPostRequestInWebSocket("select", "", contentItem.generateXML());
}
}
private void sendPostRequestInWebSocket(String url, String postData) {
sendPostRequestInWebSocket(url, "", postData);
}
private void sendPostRequestInWebSocket(String url, String infoAddon, String postData) {
int id = 0;
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);
logger.debug("{}: sending request: {}", handler.getDeviceName(), msg);
} catch (NullPointerException e) {
handler.onWebSocketError(e);
}
}
private void updateOperatingValues() {
OperationModeType operationMode;
if (currentContentItem != null) {
updatePresetGUIState(new DecimalType(currentContentItem.getPresetID()));
operationMode = currentContentItem.getOperationMode();
} else {
operationMode = OperationModeType.STANDBY;
}
updateOperationModeGUIState(new StringType(operationMode.toString()));
currentOperationMode = operationMode;
if (currentOperationMode == OperationModeType.STANDBY) {
updatePowerStateGUIState(OnOffType.OFF);
updatePlayerControlGUIState(PlayPauseType.PAUSE);
} else {
updatePowerStateGUIState(OnOffType.ON);
}
}
@Override
public boolean isBluetoothAvailable() {
return isSourceAvailable("bluetooth");
}
@Override
public boolean isAUXAvailable() {
return isSourceAvailable("aux");
}
@Override
public boolean isAUX1Available() {
return isSourceAvailable("aux1");
}
@Override
public boolean isAUX2Available() {
return isSourceAvailable("aux2");
}
@Override
public boolean isAUX3Available() {
return isSourceAvailable("aux3");
}
@Override
public boolean isTVAvailable() {
return isSourceAvailable("tv");
}
@Override
public boolean isHDMI1Available() {
return isSourceAvailable("hdmi1");
}
@Override
public boolean isInternetRadioAvailable() {
return isSourceAvailable("internetRadio");
}
@Override
public boolean isStoredMusicAvailable() {
return isSourceAvailable("storedMusic");
}
@Override
public boolean isBassAvailable() {
return isSourceAvailable("bass");
}
@Override
public void setBluetoothAvailable(boolean bluetooth) {
mapOfAvailableFunctions.put("bluetooth", bluetooth);
}
@Override
public void setAUXAvailable(boolean aux) {
mapOfAvailableFunctions.put("aux", aux);
}
@Override
public void setAUX1Available(boolean aux1) {
mapOfAvailableFunctions.put("aux1", aux1);
}
@Override
public void setAUX2Available(boolean aux2) {
mapOfAvailableFunctions.put("aux2", aux2);
}
@Override
public void setAUX3Available(boolean aux3) {
mapOfAvailableFunctions.put("aux3", aux3);
}
@Override
public void setStoredMusicAvailable(boolean storedMusic) {
mapOfAvailableFunctions.put("storedMusic", storedMusic);
}
@Override
public void setInternetRadioAvailable(boolean internetRadio) {
mapOfAvailableFunctions.put("internetRadio", internetRadio);
}
@Override
public void setTVAvailable(boolean tv) {
mapOfAvailableFunctions.put("tv", tv);
}
@Override
public void setHDMI1Available(boolean hdmi1) {
mapOfAvailableFunctions.put("hdmi1", hdmi1);
}
@Override
public void setBassAvailable(boolean bass) {
mapOfAvailableFunctions.put("bass", bass);
}
private boolean isSourceAvailable(String source) {
Boolean isAvailable = mapOfAvailableFunctions.get(source);
if (isAvailable == null) {
return false;
} else {
return isAvailable;
}
}
public void playNotificationSound(String appKey, BoseSoundTouchNotificationChannelConfiguration notificationConfig,
String fileUrl) {
String msg = "<play_info>" + "<app_key>" + appKey + "</app_key>" + "<url>" + fileUrl + "</url>" + "<service>"
+ notificationConfig.notificationService + "</service>"
+ (notificationConfig.notificationReason != null
? "<reason>" + notificationConfig.notificationReason + "</reason>"
: "")
+ (notificationConfig.notificationMessage != null
? "<message>" + notificationConfig.notificationMessage + "</message>"
: "")
+ (notificationConfig.notificationVolume != null
? "<volume>" + notificationConfig.notificationVolume + "</volume>"
: "")
+ "</play_info>";
sendPostRequestInWebSocket("speaker", msg);
}
}

View File

@@ -0,0 +1,278 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringEscapeUtils;
import org.openhab.core.types.StateOption;
import com.google.gson.annotations.Expose;
/**
* The {@link ContentItem} class manages a ContentItem
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
public class ContentItem {
private String source;
private String sourceAccount;
private String location;
private boolean presetable;
private String itemName;
private int presetID;
private 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<>();
}
/**
* Returns true if this ContentItem is defined as Preset
*
* @return true if this ContentItem is defined as Preset
*/
public boolean isPreset() {
if (presetable) {
return presetID > 0;
} else {
return false;
}
}
/**
* Returns true if all necessary stats are set
*
* @return true if all necessary stats are set
*/
public boolean isValid() {
if (getOperationMode() == OperationModeType.STANDBY) {
return true;
}
if (itemName == null || source == null || itemName.isEmpty() || source.isEmpty()) {
return false;
} else {
return true;
}
}
/**
* Returns true if source, sourceAccount, location, itemName, and presetable are equal
*
* @return true if source, sourceAccount, location, itemName, and presetable are equal
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof ContentItem) {
ContentItem other = (ContentItem) obj;
if (!isEqual(other.source, this.source)) {
return false;
}
if (!isEqual(other.sourceAccount, this.sourceAccount)) {
return false;
}
if (other.presetable != this.presetable) {
return false;
}
if (!isEqual(other.location, this.location)) {
return false;
}
if (!isEqual(other.itemName, this.itemName)) {
return false;
}
return true;
}
return super.equals(obj);
}
/**
* Returns the operation Mode, depending on the stats that are set
*
* @return the operation Mode, depending on the stats that are set
*/
public OperationModeType getOperationMode() {
OperationModeType operationMode = OperationModeType.OTHER;
if (source == null || source.equals("")) {
return OperationModeType.OTHER;
}
if (source.contains("PRODUCT")) {
if (sourceAccount.contains("TV")) {
operationMode = OperationModeType.TV;
}
if (sourceAccount.contains("HDMI")) {
operationMode = OperationModeType.HDMI1;
}
return operationMode;
}
try {
operationMode = OperationModeType.valueOf(source);
return operationMode;
} catch (IllegalArgumentException iae) {
return OperationModeType.OTHER;
}
}
public void setSource(String source) {
this.source = source;
}
public void setSourceAccount(String sourceAccount) {
this.sourceAccount = sourceAccount;
}
public void setLocation(String location) {
this.location = location;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public void setAdditionalAttribute(String name, String value) {
this.additionalAttributes.put(name, value);
}
public void setPresetable(boolean presetable) {
this.presetable = presetable;
}
public void setPresetID(int presetID) {
this.presetID = presetID;
}
public void setContainerArt(String containerArt) {
this.containerArt = containerArt;
}
public String getSource() {
return source;
}
public String getSourceAccount() {
return sourceAccount;
}
public String getLocation() {
return location;
}
public String getItemName() {
return itemName;
}
public boolean isPresetable() {
return presetable;
}
public int getPresetID() {
return presetID;
}
public String getContainerArt() {
return containerArt;
}
/**
* Returns the XML Code that is needed to switch to this ContentItem
*
* @return the XML Code that is needed to switch to this ContentItem
*/
public String generateXML() {
String xml;
switch (getOperationMode()) {
case BLUETOOTH:
xml = "<ContentItem source=\"BLUETOOTH\"></ContentItem>";
break;
case AUX:
case AUX1:
case AUX2:
case AUX3:
xml = "<ContentItem source=\"AUX\" sourceAccount=\"" + sourceAccount + "\"></ContentItem>";
break;
case TV:
xml = "<ContentItem source=\"PRODUCT\" sourceAccount=\"TV\" isPresetable=\"false\" />";
break;
case HDMI1:
xml = "<ContentItem source=\"PRODUCT\" sourceAccount=\"HDMI_1\" isPresetable=\"false\" />";
break;
default:
StringBuilder sbXml = new StringBuilder("<ContentItem");
if (source != null) {
sbXml.append(" source=\"").append(StringEscapeUtils.escapeXml(source)).append("\"");
}
if (location != null) {
sbXml.append(" location=\"").append(StringEscapeUtils.escapeXml(location)).append("\"");
}
if (sourceAccount != null) {
sbXml.append(" sourceAccount=\"").append(StringEscapeUtils.escapeXml(sourceAccount)).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(">");
if (itemName != null) {
sbXml.append("<itemName>").append(itemName).append("</itemName>");
}
if (containerArt != null) {
sbXml.append("<containerArt>").append(containerArt).append("</containerArt>");
}
sbXml.append("</ContentItem>");
xml = sbXml.toString();
break;
}
return xml;
}
public StateOption toStateOption() {
String stateOptionLabel = String.valueOf(presetID) + ": " + itemName;
return new StateOption(String.valueOf(presetID), stateOptionLabel);
}
@Override
public String toString() {
// if (presetID >= 1 && presetID <= 6) {
// StringBuilder buffer = new StringBuilder();
// buffer.append("PRESET_");
// buffer.append(presetID);
// return buffer.toString();
// }
return itemName;
}
private boolean isEqual(String s1, String s2) {
if (s1 == s2) {
return true;
}
if (s1 == null || s2 == null) {
return false;
}
return s1.equals(s2);
}
}

View File

@@ -0,0 +1,236 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import java.util.Collection;
/**
* The {@link ContentItemMaker} class makes ContentItems for sources
*
* @author Thomas Traunbauer - Initial contribution
*/
public class ContentItemMaker {
private final PresetContainer presetContainer;
private final CommandExecutor commandExecutor;
/**
* Creates a new instance of this class
*/
public ContentItemMaker(CommandExecutor commandExecutor, PresetContainer presetContainer) {
this.commandExecutor = commandExecutor;
this.presetContainer = presetContainer;
}
/**
* Returns a valid ContentItem, to switch to
*
* @param operationModeType
*
* @throws OperationModeNotAvailableException if OperationMode is not supported yet or on this device
* @throws NoInternetRadioPresetFoundException if OperationMode is INTERNET_RADIO and no PRESET is defined
* @throws NoStoredMusicPresetFoundException if OperationMode is STORED_MUSIC and no PRESET is defined
*/
public ContentItem getContentItem(OperationModeType operationModeType) throws OperationModeNotAvailableException,
NoInternetRadioPresetFoundException, NoStoredMusicPresetFoundException {
switch (operationModeType) {
case OFFLINE:
case OTHER:
case STANDBY:
throw new OperationModeNotAvailableException();
case AMAZON:
return getAmazon();
case AUX:
return getAUX();
case AUX1:
return getAUX1();
case AUX2:
return getAUX2();
case AUX3:
return getAUX3();
case BLUETOOTH:
return getBluetooth();
case DEEZER:
return getDeezer();
case HDMI1:
return getHDMI();
case INTERNET_RADIO:
return getInternetRadio();
case PANDORA:
return getPandora();
case SIRIUSXM:
return getSiriusxm();
case SPOTIFY:
return getSpotify();
case STORED_MUSIC:
return getStoredMusic();
case TV:
return getTV();
default:
throw new OperationModeNotAvailableException();
}
}
private ContentItem getAmazon() throws OperationModeNotAvailableException {
throw new OperationModeNotAvailableException();
}
private ContentItem getAUX() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isAUXAvailable()) {
contentItem = new ContentItem();
contentItem.setSource("AUX");
contentItem.setSourceAccount("AUX");
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
private ContentItem getAUX1() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isAUX1Available()) {
contentItem = new ContentItem();
contentItem.setSource("AUX");
contentItem.setSourceAccount("AUX1");
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
private ContentItem getAUX2() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isAUX2Available()) {
contentItem = new ContentItem();
contentItem.setSource("AUX");
contentItem.setSourceAccount("AUX2");
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
private ContentItem getAUX3() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isAUX3Available()) {
contentItem = new ContentItem();
contentItem.setSource("AUX");
contentItem.setSourceAccount("AUX3");
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
private ContentItem getBluetooth() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isBluetoothAvailable()) {
contentItem = new ContentItem();
contentItem.setSource("BLUETOOTH");
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
private ContentItem getDeezer() throws OperationModeNotAvailableException {
throw new OperationModeNotAvailableException();
}
private ContentItem getHDMI() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isHDMI1Available()) {
contentItem = new ContentItem();
contentItem.setSource("PRODUCT");
contentItem.setSourceAccount("HDMI_1");
contentItem.setPresetable(false);
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
private ContentItem getInternetRadio() throws NoInternetRadioPresetFoundException {
ContentItem contentItem = null;
if (commandExecutor.isInternetRadioAvailable()) {
Collection<ContentItem> listOfPresets = presetContainer.getAllPresets();
for (ContentItem iteratedItem : listOfPresets) {
if ((contentItem == null) && (iteratedItem.getOperationMode() == OperationModeType.INTERNET_RADIO)) {
contentItem = iteratedItem;
}
}
}
if (contentItem != null) {
return contentItem;
} else {
throw new NoInternetRadioPresetFoundException();
}
}
private ContentItem getPandora() throws OperationModeNotAvailableException {
throw new OperationModeNotAvailableException();
}
private ContentItem getSiriusxm() throws OperationModeNotAvailableException {
throw new OperationModeNotAvailableException();
}
private ContentItem getSpotify() throws OperationModeNotAvailableException {
throw new OperationModeNotAvailableException();
}
private ContentItem getStoredMusic() throws NoStoredMusicPresetFoundException {
ContentItem contentItem = null;
if (commandExecutor.isStoredMusicAvailable()) {
Collection<ContentItem> listOfPresets = presetContainer.getAllPresets();
for (ContentItem iteratedItem : listOfPresets) {
if ((contentItem == null) && (iteratedItem.getOperationMode() == OperationModeType.STORED_MUSIC)) {
contentItem = iteratedItem;
}
}
}
if (contentItem != null) {
return contentItem;
} else {
throw new NoStoredMusicPresetFoundException();
}
}
private ContentItem getTV() throws OperationModeNotAvailableException {
ContentItem contentItem = null;
if (commandExecutor.isTVAvailable()) {
contentItem = new ContentItem();
contentItem.setSource("PRODUCT");
contentItem.setSourceAccount("TV");
contentItem.setPresetable(false);
}
if (contentItem != null) {
return contentItem;
} else {
throw new OperationModeNotAvailableException();
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link ContentItemNotPresetableException} class is an exception
*
* @author Thomas Traunbauer - Initial contribution
*/
public class ContentItemNotPresetableException extends NoPresetFoundException {
private static final long serialVersionUID = 1L;
public ContentItemNotPresetableException() {
super();
}
public ContentItemNotPresetableException(String message) {
super(message);
}
public ContentItemNotPresetableException(String message, Throwable cause) {
super(message, cause);
}
public ContentItemNotPresetableException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link NoInternetRadioPresetFoundException} class is an exception
*
* @author Thomas Traunbauer - Initial contribution
*/
public class NoInternetRadioPresetFoundException extends NoPresetFoundException {
private static final long serialVersionUID = 1L;
public NoInternetRadioPresetFoundException() {
super();
}
public NoInternetRadioPresetFoundException(String message) {
super(message);
}
public NoInternetRadioPresetFoundException(String message, Throwable cause) {
super(message, cause);
}
public NoInternetRadioPresetFoundException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link NoPresetFoundException} class is an exception
*
* @author Thomas Traunbauer - Initial contribution
*/
public class NoPresetFoundException extends Exception {
private static final long serialVersionUID = 1L;
public NoPresetFoundException() {
super();
}
public NoPresetFoundException(String message) {
super(message);
}
public NoPresetFoundException(String message, Throwable cause) {
super(message, cause);
}
public NoPresetFoundException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link NoStoredMusicPresetFoundException} class is an exception
*
* @author Thomas Traunbauer - Initial contribution
*/
public class NoStoredMusicPresetFoundException extends NoPresetFoundException {
private static final long serialVersionUID = 1L;
public NoStoredMusicPresetFoundException() {
super();
}
public NoStoredMusicPresetFoundException(String message) {
super(message);
}
public NoStoredMusicPresetFoundException(String message, Throwable cause) {
super(message, cause);
}
public NoStoredMusicPresetFoundException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link OperationModeNotAvailableException} class is an exception
*
* @author Thomas Traunbauer - Initial contribution
*/
public class OperationModeNotAvailableException extends Exception {
private static final long serialVersionUID = 1L;
public OperationModeNotAvailableException() {
super();
}
public OperationModeNotAvailableException(String message) {
super(message);
}
public OperationModeNotAvailableException(String message, Throwable cause) {
super(message, cause);
}
public OperationModeNotAvailableException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link OperationModeType} class is holding all OperationModes
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
public enum OperationModeType {
OFFLINE,
STANDBY,
INTERNET_RADIO,
BLUETOOTH,
AUX,
AUX1,
AUX2,
AUX3,
SPOTIFY,
PANDORA,
DEEZER,
SIRIUSXM,
STORED_MUSIC,
AMAZON,
TV,
HDMI1,
TUNEIN,
ALEXA,
OTHER;
private String name;
private OperationModeType() {
this.name = name();
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.storage.DeletableStorage;
import org.openhab.core.storage.Storage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PresetContainer} class manages a PresetContainer which contains all additional Presets
*
* @author Thomas Traunbauer - Initial contribution
* @author Kai Kreuzer - Refactored it to use storage instead of file
*/
public class PresetContainer {
private final Logger logger = LoggerFactory.getLogger(PresetContainer.class);
private HashMap<Integer, ContentItem> mapOfPresets;
private Storage<ContentItem> storage;
/**
* Creates a new instance of this class
*/
public PresetContainer(Storage<ContentItem> storage) {
this.storage = storage;
init();
}
private void init() {
this.mapOfPresets = new HashMap<>();
readFromStorage();
}
/**
* Returns a Collection of all Presets
*
* @param operationModeType
*/
public Collection<ContentItem> getAllPresets() {
return mapOfPresets.values();
}
/**
* Adds a ContentItem as Preset, with presetID. Note that a eventually existing id in preset will be overwritten by
* presetID
*
* @param presetID
* @param preset
*
* @throws ContentItemNotPresetableException if ContentItem is not presetable
*/
public void put(int presetID, ContentItem preset) throws ContentItemNotPresetableException {
preset.setPresetID(presetID);
if (preset.isPresetable()) {
mapOfPresets.put(presetID, preset);
writeToStorage();
} else {
throw new ContentItemNotPresetableException();
}
}
/**
* Remove the Preset stored under the specified Id
*
* @param presetID
*/
public void remove(int presetID) {
mapOfPresets.remove(presetID);
writeToStorage();
}
/**
* Returns the Preset with presetID
*
* @param presetID
*
* @throws NoPresetFoundException if Preset could not be found
*/
public ContentItem get(int presetID) throws NoPresetFoundException {
ContentItem psFound = mapOfPresets.get(presetID);
if (psFound != null) {
return psFound;
} else {
throw new NoPresetFoundException();
}
}
/**
* Deletes all presets from the storage.
*/
public void clear() {
if (storage instanceof DeletableStorage) {
((DeletableStorage<ContentItem>) storage).delete();
} else {
Collection<@NonNull String> keys = storage.getKeys();
keys.forEach(key -> storage.remove(key));
}
}
private void writeToStorage() {
Collection<ContentItem> colletionOfPresets = getAllPresets();
List<ContentItem> listOfPresets = new ArrayList<>();
listOfPresets.addAll(colletionOfPresets);
// Only binding presets get saved
for (Iterator<ContentItem> cii = listOfPresets.iterator(); cii.hasNext();) {
if (cii.next().getPresetID() <= 6) {
cii.remove();
}
}
if (!listOfPresets.isEmpty()) {
listOfPresets.forEach(item -> storage.put(String.valueOf(item.getPresetID()), item));
}
}
private void readFromStorage() {
Collection<ContentItem> items = storage.getValues();
for (ContentItem item : items) {
try {
put(item.getPresetID(), item);
} catch (ContentItemNotPresetableException e) {
logger.debug("Item '{}' is not presetable - ignoring it.", item.getItemName());
}
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link RemoteKeyType} class is holding the Keys on a remote. For simulating key presses
*
* @author Christian Niessner - Initial contribution
*/
public enum RemoteKeyType {
PLAY,
PAUSE,
STOP,
PREV_TRACK,
NEXT_TRACK,
THUMBS_UP,
THUMBS_DOWN,
BOOKMARK,
POWER,
MUTE,
VOLUME_UP,
VOLUME_DOWN,
PRESET_1,
PRESET_2,
PRESET_3,
PRESET_4,
PRESET_5,
PRESET_6,
AUX_INPUT,
SHUFFLE_OFF,
SHUFFLE_ON,
REPEAT_OFF,
REPEAT_ONE,
REPEAT_ALL,
PLAY_PAUSE,
ADD_FAVORITE,
REMOVE_FAVORITE,
INVALID_KEY;
private String name;
private RemoteKeyType() {
this.name = name();
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
/**
* The {@link XMLHandlerState} class defines the XML States provided from Bose Soundtouch
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
public enum XMLHandlerState {
INIT,
Msg,
MsgHeader,
MsgBody,
Bass,
BassActual,
BassTarget,
BassUpdated,
BassMin,
BassMax,
BassDefault,
ContentItem,
ContentItemItemName,
ContentItemContainerArt,
Group,
GroupName,
Components,
Component,
Info,
InfoName,
InfoType,
InfoModuleType,
InfoFirmwareVersion,
Presets,
Preset,
MasterDeviceId,
DeviceId,
DeviceIp,
NowPlaying,
NowPlayingAlbum,
NowPlayingArt,
NowPlayingArtist,
NowPlayingDescription,
NowPlayingGenre,
NowPlayingPlayStatus,
NowPlayingRateEnabled,
NowPlayingSkipEnabled,
NowPlayingSkipPreviousEnabled,
NowPlayingStationLocation,
NowPlayingStationName,
NowPlayingTrack,
Unprocessed, // unprocessed / ignored data
UnprocessedNoTextExpected, // unprocessed / ignored data
Updates,
Volume,
VolumeActual,
VolumeTarget,
VolumeUpdated,
VolumeMuteEnabled,
Zone,
ZoneMember,
ZoneUpdated,
Sources,
BassCapabilities,
BassAvailable,
}

View File

@@ -0,0 +1,690 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
import static org.openhab.core.thing.Thing.PROPERTY_HARDWARE_VERSION;
import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* The {@link XMLResponseHandler} class handles the XML communication with the Soundtouch
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
* @author Kai Kreuzer - code clean up
*/
public class XMLResponseHandler extends DefaultHandler {
private final Logger logger = LoggerFactory.getLogger(XMLResponseHandler.class);
private BoseSoundTouchHandler handler;
private CommandExecutor commandExecutor;
private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap;
private Stack<XMLHandlerState> states;
private XMLHandlerState state;
private boolean msgHeaderWasValid;
private ContentItem contentItem;
private boolean volumeMuteEnabled;
private OnOffType rateEnabled;
private OnOffType skipEnabled;
private OnOffType skipPreviousEnabled;
private State nowPlayingSource;
private BoseSoundTouchConfiguration masterDeviceId;
String deviceId;
private Map<Integer, ContentItem> playerPresets;
/**
* Creates a new instance of this class
*
* @param handler
* @param stateSwitchingMap the stateSwitchingMap is the XMLState Map, that says which Flags are computed
*/
public XMLResponseHandler(BoseSoundTouchHandler handler,
Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap) {
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 {
super.startElement(uri, localName, qName, attributes);
logger.trace("{}: startElement('{}'; state: {})", handler.getDeviceName(), localName, state);
states.push(state);
XMLHandlerState curState = state; // save for switch statement
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
switch (curState) {
case INIT:
if ("updates".equals(localName)) {
// it just seems to be a ping - havn't seen any data on it..
if (checkDeviceId(localName, attributes, false)) {
state = XMLHandlerState.Updates;
} else {
state = XMLHandlerState.Unprocessed;
}
} else {
state = stateMap.get(localName);
if (state == null) {
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}", handler.getDeviceName(), curState,
localName);
}
state = XMLHandlerState.Unprocessed;
}
}
break;
case Msg:
if ("header".equals(localName)) {
// message
if (checkDeviceId(localName, attributes, false)) {
state = XMLHandlerState.MsgHeader;
msgHeaderWasValid = true;
} else {
state = XMLHandlerState.Unprocessed;
}
} else if ("body".equals(localName)) {
if (msgHeaderWasValid) {
state = XMLHandlerState.MsgBody;
} else {
state = XMLHandlerState.Unprocessed;
}
} else {
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName);
}
state = XMLHandlerState.Unprocessed;
}
break;
case MsgHeader:
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);
}
state = XMLHandlerState.Unprocessed;
}
break;
case MsgBody:
if ("nowPlaying".equals(localName)) {
/*
* if (!checkDeviceId(localName, attributes, true)) {
* state = XMLHandlerState.Unprocessed;
* break;
* }
*/
rateEnabled = OnOffType.OFF;
skipEnabled = OnOffType.OFF;
skipPreviousEnabled = OnOffType.OFF;
state = XMLHandlerState.NowPlaying;
String source = attributes.getValue("source");
if (nowPlayingSource == null || !nowPlayingSource.toString().equals(source)) {
// source changed
nowPlayingSource = new StringType(source);
// reset enabled states
updateRateEnabled(OnOffType.OFF);
updateSkipEnabled(OnOffType.OFF);
updateSkipPreviousEnabled(OnOffType.OFF);
// clear all "nowPlaying" details on source change...
updateNowPlayingAlbum(UnDefType.NULL);
updateNowPlayingArtwork(UnDefType.NULL);
updateNowPlayingArtist(UnDefType.NULL);
updateNowPlayingDescription(UnDefType.NULL);
updateNowPlayingGenre(UnDefType.NULL);
updateNowPlayingItemName(UnDefType.NULL);
updateNowPlayingStationLocation(UnDefType.NULL);
updateNowPlayingStationName(UnDefType.NULL);
updateNowPlayingTrack(UnDefType.NULL);
}
} else if ("zone".equals(localName)) {
state = XMLHandlerState.Zone;
} else if ("presets".equals(localName)) {
// reset the current playerPrests
playerPresets = new HashMap<>();
for (int i = 1; i <= 6; i++) {
playerPresets.put(i, null);
}
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);
}
state = XMLHandlerState.Unprocessed;
} else if (state != XMLHandlerState.Volume && state != XMLHandlerState.Presets
&& state != XMLHandlerState.Group && state != XMLHandlerState.Unprocessed) {
if (!checkDeviceId(localName, attributes, false)) {
state = XMLHandlerState.Unprocessed;
break;
}
}
}
break;
case Presets:
if ("preset".equals(localName)) {
state = XMLHandlerState.Preset;
String 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);
}
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);
}
}
}
} else {
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName);
}
state = XMLHandlerState.Unprocessed;
}
break;
// auto go trough the state map
case Group:
case Zone:
case Bass:
case ContentItem:
case MasterDeviceId:
case GroupName:
case DeviceId:
case DeviceIp:
case Info:
case NowPlaying:
case Preset:
case Updates:
case Volume:
case Components:
case Component:
state = nextState(stateMap, curState, localName);
break;
case BassCapabilities:
state = nextState(stateMap, curState, localName);
break;
// all entities without any children expected..
case BassTarget:
case BassActual:
case BassUpdated:
case BassMin:
case BassMax:
case BassDefault:
case ContentItemItemName:
case ContentItemContainerArt:
case InfoName:
case InfoType:
case InfoFirmwareVersion:
case InfoModuleType:
case NowPlayingAlbum:
case NowPlayingArt:
case NowPlayingArtist:
case NowPlayingGenre:
case NowPlayingDescription:
case NowPlayingPlayStatus:
case NowPlayingRateEnabled:
case NowPlayingSkipEnabled:
case NowPlayingSkipPreviousEnabled:
case NowPlayingStationLocation:
case NowPlayingStationName:
case NowPlayingTrack:
case VolumeTarget:
case VolumeActual:
case VolumeUpdated:
case VolumeMuteEnabled:
case ZoneMember:
case ZoneUpdated: // currently this dosn't provide any zone details..
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName);
}
state = XMLHandlerState.Unprocessed;
break;
case BassAvailable:
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState,
localName);
}
state = XMLHandlerState.Unprocessed;
break;
case Unprocessed:
// all further things are also unprocessed
state = XMLHandlerState.Unprocessed;
break;
case UnprocessedNoTextExpected:
state = XMLHandlerState.UnprocessedNoTextExpected;
break;
}
if (state == XMLHandlerState.ContentItem) {
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;
}
if ("location".equalsIgnoreCase(attrName)) {
continue;
}
if ("sourceAccount".equalsIgnoreCase(attrName)) {
continue;
}
if ("isPresetable".equalsIgnoreCase(attrName)) {
continue;
}
contentItem.setAdditionalAttribute(attrName, attributes.getValue(attrId));
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
super.endElement(uri, localName, qName);
logger.trace("{}: endElement('{}')", handler.getDeviceName(), localName);
final XMLHandlerState prevState = state;
state = states.pop();
switch (prevState) {
case Info:
commandExecutor.getInformations(APIRequest.VOLUME);
commandExecutor.getInformations(APIRequest.PRESETS);
commandExecutor.getInformations(APIRequest.NOW_PLAYING);
commandExecutor.getInformations(APIRequest.GET_ZONE);
commandExecutor.getInformations(APIRequest.BASS);
commandExecutor.getInformations(APIRequest.SOURCES);
commandExecutor.getInformations(APIRequest.BASSCAPABILITIES);
commandExecutor.getInformations(APIRequest.GET_GROUP);
break;
case ContentItem:
if (state == XMLHandlerState.NowPlaying) {
// update now playing name...
updateNowPlayingItemName(new StringType(contentItem.getItemName()));
commandExecutor.setCurrentContentItem(contentItem);
}
break;
case Preset:
if (state == XMLHandlerState.Presets) {
playerPresets.put(contentItem.getPresetID(), contentItem);
contentItem = null;
}
break;
case NowPlaying:
if (state == XMLHandlerState.MsgBody) {
updateRateEnabled(rateEnabled);
updateSkipEnabled(skipEnabled);
updateSkipPreviousEnabled(skipPreviousEnabled);
}
break;
// handle special tags..
case BassUpdated:
// request current bass level
commandExecutor.getInformations(APIRequest.BASS);
break;
case VolumeUpdated:
commandExecutor.getInformations(APIRequest.VOLUME);
break;
case NowPlayingRateEnabled:
rateEnabled = OnOffType.ON;
break;
case NowPlayingSkipEnabled:
skipEnabled = OnOffType.ON;
break;
case NowPlayingSkipPreviousEnabled:
skipPreviousEnabled = OnOffType.ON;
break;
case Volume:
OnOffType muted = volumeMuteEnabled ? OnOffType.ON : OnOffType.OFF;
commandExecutor.setCurrentMuted(volumeMuteEnabled);
commandExecutor.postVolumeMuted(muted);
break;
case ZoneUpdated:
commandExecutor.getInformations(APIRequest.GET_ZONE);
break;
case Presets:
commandExecutor.updatePresetContainerFromPlayer(playerPresets);
playerPresets = null;
break;
case Group:
handler.handleGroupUpdated(masterDeviceId);
break;
default:
// no actions...
break;
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
logger.trace("{}: Text data during {}: '{}'", handler.getDeviceName(), state, new String(ch, start, length));
super.characters(ch, start, length);
switch (state) {
case INIT:
case Msg:
case MsgHeader:
case MsgBody:
case Bass:
case BassUpdated:
case Updates:
case Volume:
case VolumeUpdated:
case Info:
case Preset:
case Presets:
case NowPlaying:
case NowPlayingRateEnabled:
case NowPlayingSkipEnabled:
case NowPlayingSkipPreviousEnabled:
case ContentItem:
case UnprocessedNoTextExpected:
case Zone:
case ZoneUpdated:
case Sources:
logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
new String(ch, start, length));
break;
case BassMin: // @TODO - find out how to dynamically change "channel-type" bass configuration
case BassMax: // based on these values...
case BassDefault:
case BassTarget:
case VolumeTarget:
// this are currently unprocessed values.
break;
case BassCapabilities:
logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
new String(ch, start, length));
break;
case Unprocessed:
// drop quietly..
break;
case BassActual:
commandExecutor.updateBassLevelGUIState(new DecimalType(new String(ch, start, length)));
break;
case InfoName:
setConfigOption(DEVICE_INFO_NAME, new String(ch, start, length));
break;
case InfoType:
setConfigOption(DEVICE_INFO_TYPE, new String(ch, start, length));
setConfigOption(PROPERTY_MODEL_ID, new String(ch, start, length));
break;
case InfoModuleType:
setConfigOption(PROPERTY_HARDWARE_VERSION, new String(ch, start, length));
break;
case InfoFirmwareVersion:
String[] fwVersion = new String(ch, start, length).split(" ");
setConfigOption(PROPERTY_FIRMWARE_VERSION, fwVersion[0]);
break;
case BassAvailable:
boolean bassAvailable = Boolean.parseBoolean(new String(ch, start, length));
commandExecutor.setBassAvailable(bassAvailable);
break;
case NowPlayingAlbum:
updateNowPlayingAlbum(new StringType(new String(ch, start, length)));
break;
case NowPlayingArt:
String url = new String(ch, start, length);
if (url.startsWith("http")) {
// We download the cover art in a different thread to not delay the other operations
handler.getScheduler().submit(() -> {
RawType image = HttpUtil.downloadImage(url, true, 500000);
if (image != null) {
updateNowPlayingArtwork(image);
} else {
updateNowPlayingArtwork(UnDefType.UNDEF);
}
});
} else {
updateNowPlayingArtwork(UnDefType.UNDEF);
}
break;
case NowPlayingArtist:
updateNowPlayingArtist(new StringType(new String(ch, start, length)));
break;
case ContentItemItemName:
contentItem.setItemName(new String(ch, start, length));
break;
case ContentItemContainerArt:
contentItem.setContainerArt(new String(ch, start, length));
break;
case NowPlayingDescription:
updateNowPlayingDescription(new StringType(new String(ch, start, length)));
break;
case NowPlayingGenre:
updateNowPlayingGenre(new StringType(new String(ch, start, length)));
break;
case NowPlayingPlayStatus:
String playPauseState = new String(ch, start, length);
if ("PLAY_STATE".equals(playPauseState) || "BUFFERING_STATE".equals(playPauseState)) {
commandExecutor.updatePlayerControlGUIState(PlayPauseType.PLAY);
} else if ("STOP_STATE".equals(playPauseState) || "PAUSE_STATE".equals(playPauseState)) {
commandExecutor.updatePlayerControlGUIState(PlayPauseType.PAUSE);
}
break;
case NowPlayingStationLocation:
updateNowPlayingStationLocation(new StringType(new String(ch, start, length)));
break;
case NowPlayingStationName:
updateNowPlayingStationName(new StringType(new String(ch, start, length)));
break;
case NowPlayingTrack:
updateNowPlayingTrack(new StringType(new String(ch, start, length)));
break;
case VolumeActual:
commandExecutor.updateVolumeGUIState(new PercentType(Integer.parseInt(new String(ch, start, length))));
break;
case VolumeMuteEnabled:
volumeMuteEnabled = Boolean.parseBoolean(new String(ch, start, length));
commandExecutor.setCurrentMuted(volumeMuteEnabled);
break;
case MasterDeviceId:
if (masterDeviceId != null) {
masterDeviceId.macAddress = new String(ch, start, length);
}
break;
case GroupName:
if (masterDeviceId != null) {
masterDeviceId.groupName = new String(ch, start, length);
}
break;
case DeviceId:
deviceId = new String(ch, start, length);
break;
case DeviceIp:
if (masterDeviceId != null && Objects.equals(masterDeviceId.macAddress, deviceId)) {
masterDeviceId.host = new String(ch, start, length);
}
break;
default:
// do nothing
break;
}
}
@Override
public void skippedEntity(String name) throws SAXException {
super.skippedEntity(name);
}
private boolean checkDeviceId(String localName, Attributes attributes, boolean allowFromMaster) {
String deviceID = attributes.getValue("deviceID");
if (deviceID == null) {
logger.warn("{}: No device-ID in entity {}", handler.getDeviceName(), localName);
return false;
}
if (deviceID.equals(handler.getMacAddress())) {
return true;
}
logger.warn("{}: Wrong device-ID in entity '{}': Got: '{}', expected: '{}'", handler.getDeviceName(), localName,
deviceID, handler.getMacAddress());
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);
if (state == null) {
if (logger.isDebugEnabled()) {
logger.warn("{}: Unhandled XML entity during {}: '{}'", handler.getDeviceName(), curState, localName);
}
state = XMLHandlerState.Unprocessed;
}
return state;
}
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);
}
}
private void updateNowPlayingAlbum(State state) {
handler.updateState(CHANNEL_NOWPLAYING_ALBUM, state);
}
private void updateNowPlayingArtwork(State state) {
handler.updateState(CHANNEL_NOWPLAYING_ARTWORK, state);
}
private void updateNowPlayingArtist(State state) {
handler.updateState(CHANNEL_NOWPLAYING_ARTIST, state);
}
private void updateNowPlayingDescription(State state) {
handler.updateState(CHANNEL_NOWPLAYING_DESCRIPTION, state);
}
private void updateNowPlayingGenre(State state) {
handler.updateState(CHANNEL_NOWPLAYING_GENRE, state);
}
private void updateNowPlayingItemName(State state) {
handler.updateState(CHANNEL_NOWPLAYING_ITEMNAME, state);
}
private void updateNowPlayingStationLocation(State state) {
handler.updateState(CHANNEL_NOWPLAYING_STATIONLOCATION, state);
}
private void updateNowPlayingStationName(State state) {
handler.updateState(CHANNEL_NOWPLAYING_STATIONNAME, state);
}
private void updateNowPlayingTrack(State state) {
handler.updateState(CHANNEL_NOWPLAYING_TRACK, state);
}
private void updateRateEnabled(OnOffType state) {
handler.updateState(CHANNEL_RATEENABLED, state);
}
private void updateSkipEnabled(OnOffType state) {
handler.updateState(CHANNEL_SKIPENABLED, state);
}
private void updateSkipPreviousEnabled(OnOffType state) {
handler.updateState(CHANNEL_SKIPPREVIOUSENABLED, state);
}
}

View File

@@ -0,0 +1,173 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
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
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
public class XMLResponseProcessor {
private BoseSoundTouchHandler handler;
private Map<XMLHandlerState, Map<String, XMLHandlerState>> stateSwitchingMap;
public XMLResponseProcessor(BoseSoundTouchHandler handler) {
this.handler = handler;
init();
}
public void handleMessage(String msg) throws SAXException, IOException {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setContentHandler(new XMLResponseHandler(handler, stateSwitchingMap));
reader.parse(new InputSource(new StringReader(msg)));
}
// 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);
msgInitMap.put("SoundTouchSdkInfo", XMLHandlerState.Unprocessed);
msgInitMap.put("userActivityUpdate", XMLHandlerState.Unprocessed); // ignored..
Map<String, XMLHandlerState> msgBodyMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.MsgBody, msgBodyMap);
msgBodyMap.put("info", XMLHandlerState.Info);
msgBodyMap.put("volume", XMLHandlerState.Volume);
msgBodyMap.put("presets", XMLHandlerState.Presets);
msgBodyMap.put("key", XMLHandlerState.Unprocessed); // only confirmation of our key presses...
msgBodyMap.put("status", XMLHandlerState.Unprocessed); // only confirmation of commands sent to device...
msgBodyMap.put("zone", XMLHandlerState.Zone); // only confirmation of our key presses...
msgBodyMap.put("bass", XMLHandlerState.Bass);
msgBodyMap.put("sources", XMLHandlerState.Sources);
msgBodyMap.put("bassCapabilities", XMLHandlerState.BassCapabilities);
msgBodyMap.put("group", XMLHandlerState.Group);
// info message states
Map<String, XMLHandlerState> infoMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Info, infoMap);
infoMap.put("components", XMLHandlerState.Info);
infoMap.put("component", XMLHandlerState.Info);
infoMap.put("name", XMLHandlerState.InfoName);
infoMap.put("type", XMLHandlerState.InfoType);
infoMap.put("componentCategory", XMLHandlerState.Unprocessed);
infoMap.put("softwareVersion", XMLHandlerState.InfoFirmwareVersion);
infoMap.put("serialNumber", XMLHandlerState.Unprocessed);
infoMap.put("networkInfo", XMLHandlerState.Unprocessed);
infoMap.put("margeAccountUUID", XMLHandlerState.Unprocessed);
infoMap.put("margeURL", XMLHandlerState.Unprocessed);
infoMap.put("moduleType", XMLHandlerState.InfoModuleType);
infoMap.put("variant", XMLHandlerState.Unprocessed);
infoMap.put("variantMode", XMLHandlerState.Unprocessed);
infoMap.put("countryCode", XMLHandlerState.Unprocessed);
infoMap.put("regionCode", XMLHandlerState.Unprocessed);
Map<String, XMLHandlerState> updatesMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Updates, updatesMap);
updatesMap.put("clockDisplayUpdated", XMLHandlerState.Unprocessed); // can we get anything useful of that?
updatesMap.put("connectionStateUpdated", XMLHandlerState.UnprocessedNoTextExpected);
updatesMap.put("infoUpdated", XMLHandlerState.Unprocessed);
updatesMap.put("nowPlayingUpdated", XMLHandlerState.MsgBody);
updatesMap.put("nowSelectionUpdated", XMLHandlerState.Unprocessed); // TODO this seems to be quite a useful info
// what is currently played..
updatesMap.put("recentsUpdated", XMLHandlerState.Unprocessed);
updatesMap.put("volumeUpdated", XMLHandlerState.MsgBody);
updatesMap.put("zoneUpdated", XMLHandlerState.ZoneUpdated); // just notifies but dosn't provide details
updatesMap.put("bassUpdated", XMLHandlerState.BassUpdated);
updatesMap.put("presetsUpdated", XMLHandlerState.MsgBody);
updatesMap.put("groupUpdated", XMLHandlerState.MsgBody);
Map<String, XMLHandlerState> volume = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Volume, volume);
volume.put("targetvolume", XMLHandlerState.VolumeTarget);
volume.put("actualvolume", XMLHandlerState.VolumeActual);
volume.put("muteenabled", XMLHandlerState.VolumeMuteEnabled);
Map<String, XMLHandlerState> nowPlayingMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.NowPlaying, nowPlayingMap);
nowPlayingMap.put("album", XMLHandlerState.NowPlayingAlbum);
nowPlayingMap.put("art", XMLHandlerState.NowPlayingArt);
nowPlayingMap.put("artist", XMLHandlerState.NowPlayingArtist);
nowPlayingMap.put("ContentItem", XMLHandlerState.ContentItem);
nowPlayingMap.put("description", XMLHandlerState.NowPlayingDescription);
nowPlayingMap.put("playStatus", XMLHandlerState.NowPlayingPlayStatus);
nowPlayingMap.put("rateEnabled", XMLHandlerState.NowPlayingRateEnabled);
nowPlayingMap.put("skipEnabled", XMLHandlerState.NowPlayingSkipEnabled);
nowPlayingMap.put("skipPreviousEnabled", XMLHandlerState.NowPlayingSkipPreviousEnabled);
nowPlayingMap.put("stationLocation", XMLHandlerState.NowPlayingStationLocation);
nowPlayingMap.put("stationName", XMLHandlerState.NowPlayingStationName);
nowPlayingMap.put("track", XMLHandlerState.NowPlayingTrack);
nowPlayingMap.put("connectionStatusInfo", XMLHandlerState.Unprocessed); // TODO active when Source==Bluetooth
// TODO active when Source==Pandora and maybe also other sources - seems to be rating related
nowPlayingMap.put("time", XMLHandlerState.Unprocessed);
nowPlayingMap.put("rating", XMLHandlerState.Unprocessed);
nowPlayingMap.put("rateEnabled", XMLHandlerState.Unprocessed);
// ContentItem specifies a resource (that also could be bookmarked in a preset)
Map<String, XMLHandlerState> contentItemMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.ContentItem, contentItemMap);
contentItemMap.put("itemName", XMLHandlerState.ContentItemItemName);
contentItemMap.put("containerArt", XMLHandlerState.ContentItemContainerArt);
Map<String, XMLHandlerState> presetMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Preset, presetMap);
presetMap.put("ContentItem", XMLHandlerState.ContentItem);
Map<String, XMLHandlerState> zoneMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Zone, zoneMap);
zoneMap.put("member", XMLHandlerState.ZoneMember);
Map<String, XMLHandlerState> bassMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Bass, bassMap);
bassMap.put("targetbass", XMLHandlerState.BassTarget);
bassMap.put("actualbass", XMLHandlerState.BassActual);
Map<String, XMLHandlerState> sourceMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Sources, sourceMap);
Map<String, XMLHandlerState> bassCapabilitiesMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.BassCapabilities, bassCapabilitiesMap);
bassCapabilitiesMap.put("bassAvailable", XMLHandlerState.BassAvailable);
bassCapabilitiesMap.put("bassMin", XMLHandlerState.BassMin);
bassCapabilitiesMap.put("bassMax", XMLHandlerState.BassMax);
bassCapabilitiesMap.put("bassDefault", XMLHandlerState.BassDefault);
Map<String, XMLHandlerState> groupsMap = new HashMap<>();
stateSwitchingMap.put(XMLHandlerState.Group, groupsMap);
groupsMap.put("name", XMLHandlerState.GroupName);
groupsMap.put("masterDeviceId", XMLHandlerState.MasterDeviceId);
groupsMap.put("roles", XMLHandlerState.Unprocessed);
groupsMap.put("senderIPAddress", XMLHandlerState.Unprocessed);
groupsMap.put("status", XMLHandlerState.Unprocessed);
groupsMap.put("roles", XMLHandlerState.Unprocessed);
groupsMap.put("groupRole", XMLHandlerState.Unprocessed);
groupsMap.put("deviceId", XMLHandlerState.DeviceId);
groupsMap.put("role", XMLHandlerState.Unprocessed);
groupsMap.put("ipAddress", XMLHandlerState.DeviceIp);
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal.discovery;
import java.io.IOException;
import org.openhab.core.io.net.http.HttpUtil;
/**
* The {@link DiscoveryUtil} is a static helper class, to get infos from a XML
*
* @author Thomas Traunbauer - Initial contribution
*/
public class DiscoveryUtil {
/**
* Finds the content in an element
*
* This is a quick and dirty method, it always delivers the first appearance of content in an element
*/
public static String getContentOfFirstElement(String content, String element) {
if (content == null) {
return "";
}
String beginTag = "<" + element + ">";
String endTag = "</" + element + ">";
int startIndex = content.indexOf(beginTag) + beginTag.length();
int endIndex = content.indexOf(endTag);
if (startIndex != -1 && endIndex != -1) {
return content.substring(startIndex, endIndex);
} else {
return "";
}
}
/**
* Executes an URL and returns to answer
*/
public static String executeUrl(String url) throws IOException {
return HttpUtil.executeUrl("GET", url, 5000);
}
}

View File

@@ -0,0 +1,213 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal.discovery;
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.*;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchConfiguration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SoundTouchDiscoveryParticipant} is responsible processing the
* results of searches for mDNS services of type _soundtouch._tcp.local.
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
*/
@Component(immediate = true, configurationPid = "discovery.bosesoundtouch")
public class SoundTouchDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(SoundTouchDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public DiscoveryResult createResult(ServiceInfo info) {
DiscoveryResult result = null;
ThingUID uid = getThingUID(info);
if (uid != null) {
// remove the domain from the name
InetAddress[] addrs = info.getInetAddresses();
Map<String, Object> properties = new HashMap<>(2);
String label = null;
if (BST_10_THING_TYPE_UID.equals(getThingTypeUID(info))) {
try {
String group = DiscoveryUtil.executeUrl("http://" + addrs[0].getHostAddress() + ":8090/getGroup");
label = DiscoveryUtil.getContentOfFirstElement(group, "name");
} catch (IOException e) {
logger.debug("Can't obtain label for group. Will use the default one");
}
}
if (label == null || label.isEmpty()) {
label = info.getName();
}
if (label == null || label.isEmpty()) {
label = "Bose SoundTouch";
}
// we expect only one address per device..
if (addrs.length > 1) {
logger.warn("Bose SoundTouch device {} ({}) reports multiple addresses - using the first one: {}",
info.getName(), label, Arrays.toString(addrs));
}
properties.put(BoseSoundTouchConfiguration.HOST, addrs[0].getHostAddress());
if (getMacAddress(info) != null) {
properties.put(BoseSoundTouchConfiguration.MAC_ADDRESS,
new String(getMacAddress(info), StandardCharsets.UTF_8));
}
// Set manufacturer as thing property (if available)
byte[] manufacturer = info.getPropertyBytes("MANUFACTURER");
if (manufacturer != null) {
properties.put(Thing.PROPERTY_VENDOR, new String(manufacturer, StandardCharsets.UTF_8));
}
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).withTTL(600).build();
}
return result;
}
@Override
public ThingUID getThingUID(ServiceInfo info) {
logger.trace("ServiceInfo: {}", info);
ThingTypeUID typeUID = getThingTypeUID(info);
if (typeUID != null) {
if (info.getType() != null) {
if (info.getType().equals(getServiceType())) {
logger.trace("Discovered a Bose SoundTouch thing with name '{}'", info.getName());
byte[] mac = getMacAddress(info);
if (mac != null) {
return new ThingUID(typeUID, new String(mac, StandardCharsets.UTF_8));
} else {
return null;
}
}
}
}
return null;
}
@Override
public String getServiceType() {
return "_soundtouch._tcp.local.";
}
private 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) {
deviceId = new String(mac, StandardCharsets.UTF_8);
}
String deviceType;
try {
String content = DiscoveryUtil.executeUrl("http://" + ip + ":8090/info");
deviceType = DiscoveryUtil.getContentOfFirstElement(content, "type");
} catch (IOException e) {
return null;
}
if (deviceType.toLowerCase().contains("soundtouch 10")) {
// Check if it's a Stereo Pair
try {
String group = DiscoveryUtil.executeUrl("http://" + ip + ":8090/getGroup");
String masterDevice = DiscoveryUtil.getContentOfFirstElement(group, "masterDeviceId");
if (Objects.equals(deviceId, masterDevice)) {
// Stereo Pair - Master Device
return BST_10_THING_TYPE_UID;
} else if (!masterDevice.isEmpty()) {
// Stereo Pair - Secondary Device - should not be paired
return null;
} else {
// Single player
return BST_10_THING_TYPE_UID;
}
} catch (IOException e) {
return null;
}
}
if (deviceType.toLowerCase().contains("soundtouch 20")) {
return BST_20_THING_TYPE_UID;
}
if (deviceType.toLowerCase().contains("soundtouch 300")) {
return BST_300_THING_TYPE_UID;
}
if (deviceType.toLowerCase().contains("soundtouch 30")) {
return BST_30_THING_TYPE_UID;
}
if (deviceType.toLowerCase().contains("soundtouch wireless link adapter")) {
return BST_WLA_THING_TYPE_UID;
}
if (deviceType.toLowerCase().contains("wave")) {
return BST_WSMS_THING_TYPE_UID;
}
if (deviceType.toLowerCase().contains("amplifier")) {
return BST_SA5A_THING_TYPE_UID;
}
return null;
}
return null;
}
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;
}
return null;
}
}

View File

@@ -0,0 +1,569 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bosesoundtouch.internal.handler;
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.*;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketFrameListener;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.Frame.Type;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.bosesoundtouch.internal.APIRequest;
import org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchConfiguration;
import org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchNotificationChannelConfiguration;
import org.openhab.binding.bosesoundtouch.internal.BoseStateDescriptionOptionProvider;
import org.openhab.binding.bosesoundtouch.internal.CommandExecutor;
import org.openhab.binding.bosesoundtouch.internal.OperationModeType;
import org.openhab.binding.bosesoundtouch.internal.PresetContainer;
import org.openhab.binding.bosesoundtouch.internal.RemoteKeyType;
import org.openhab.binding.bosesoundtouch.internal.XMLResponseProcessor;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BoseSoundTouchHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Christian Niessner - Initial contribution
* @author Thomas Traunbauer - Initial contribution
* @author Kai Kreuzer - code clean up
* @author Alexander Kostadinov - Handling of websocket ping-pong mechanism for thing status check
*/
public class BoseSoundTouchHandler extends BaseThingHandler implements WebSocketListener, WebSocketFrameListener {
private static final int MAX_MISSED_PONGS_COUNT = 2;
private static final int RETRY_INTERVAL_IN_SECS = 30;
private final Logger logger = LoggerFactory.getLogger(BoseSoundTouchHandler.class);
private ScheduledFuture<?> connectionChecker;
private WebSocketClient client;
private volatile Session session;
private volatile CommandExecutor commandExecutor;
private volatile int missedPongsCount = 0;
private XMLResponseProcessor xmlResponseProcessor;
private PresetContainer presetContainer;
private BoseStateDescriptionOptionProvider stateOptionProvider;
private Future<?> sessionFuture;
/**
* Creates a new instance of this class for the {@link Thing}.
*
* @param thing the thing that should be handled, not null
* @param presetContainer the preset container instance to use for managing presets
*
* @throws IllegalArgumentException if thing or factory argument is null
*/
public BoseSoundTouchHandler(Thing thing, PresetContainer presetContainer,
BoseStateDescriptionOptionProvider stateOptionProvider) {
super(thing);
this.presetContainer = presetContainer;
this.stateOptionProvider = stateOptionProvider;
xmlResponseProcessor = new XMLResponseProcessor(this);
}
@Override
public void initialize() {
connectionChecker = scheduler.scheduleWithFixedDelay(() -> checkConnection(), 0, RETRY_INTERVAL_IN_SECS,
TimeUnit.SECONDS);
}
@Override
public void dispose() {
if (connectionChecker != null && !connectionChecker.isCancelled()) {
connectionChecker.cancel(true);
connectionChecker = null;
}
closeConnection();
super.dispose();
}
@Override
public void handleRemoval() {
presetContainer.clear();
super.handleRemoval();
}
@Override
public void updateState(String channelID, State state) {
// don't update channel if it's not linked (in case of Stereo Pair slave device)
if (isLinked(channelID)) {
super.updateState(channelID, state);
} else {
logger.debug("{}: Skipping state update because of not linked channel '{}'", getDeviceName(), channelID);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (commandExecutor == null) {
logger.debug("{}: Can't handle command '{}' for channel '{}' because of not initialized connection.",
getDeviceName(), command, channelUID);
return;
} else {
logger.debug("{}: handleCommand({}, {});", getDeviceName(), channelUID, command);
}
if (command.equals(RefreshType.REFRESH)) {
switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_BASS:
commandExecutor.getInformations(APIRequest.BASS);
break;
case CHANNEL_KEY_CODE:
// refresh makes no sense... ?
break;
case CHANNEL_NOWPLAYING_ALBUM:
case CHANNEL_NOWPLAYING_ARTIST:
case CHANNEL_NOWPLAYING_ARTWORK:
case CHANNEL_NOWPLAYING_DESCRIPTION:
case CHANNEL_NOWPLAYING_GENRE:
case CHANNEL_NOWPLAYING_ITEMNAME:
case CHANNEL_NOWPLAYING_STATIONLOCATION:
case CHANNEL_NOWPLAYING_STATIONNAME:
case CHANNEL_NOWPLAYING_TRACK:
case CHANNEL_RATEENABLED:
case CHANNEL_SKIPENABLED:
case CHANNEL_SKIPPREVIOUSENABLED:
commandExecutor.getInformations(APIRequest.NOW_PLAYING);
break;
case CHANNEL_VOLUME:
commandExecutor.getInformations(APIRequest.VOLUME);
break;
default:
logger.debug("{} : Got command '{}' for channel '{}' which is unhandled!", getDeviceName(), command,
channelUID.getId());
}
return;
}
switch (channelUID.getIdWithoutGroup()) {
case CHANNEL_POWER:
if (command instanceof OnOffType) {
commandExecutor.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);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_MUTE:
if (command instanceof OnOffType) {
commandExecutor.postVolumeMuted((OnOffType) command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_OPERATIONMODE:
if (command instanceof StringType) {
String cmd = command.toString().toUpperCase().trim();
try {
OperationModeType mode = OperationModeType.valueOf(cmd);
commandExecutor.postOperationMode(mode);
} catch (IllegalArgumentException iae) {
logger.warn("{}: OperationMode \"{}\" is not valid!", getDeviceName(), cmd);
}
}
break;
case CHANNEL_PLAYER_CONTROL:
if ((command instanceof PlayPauseType) || (command instanceof NextPreviousType)) {
commandExecutor.postPlayerControl(command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_PRESET:
if (command instanceof DecimalType) {
commandExecutor.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);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_SAVE_AS_PRESET:
if (command instanceof DecimalType) {
commandExecutor.addCurrentContentItemToPresetContainer((DecimalType) command);
} else {
logger.debug("{}: Unhandled command type: {}: {}", getDeviceName(), command.getClass(), command);
}
break;
case CHANNEL_KEY_CODE:
if (command instanceof StringType) {
String cmd = command.toString().toUpperCase().trim();
try {
RemoteKeyType keyCommand = RemoteKeyType.valueOf(cmd);
commandExecutor.postRemoteKey(keyCommand);
} catch (IllegalArgumentException e) {
logger.debug("{}: Unhandled remote key: {}", getDeviceName(), cmd);
}
}
break;
default:
Channel channel = getThing().getChannel(channelUID.getId());
if (channel != null) {
ChannelTypeUID chTypeUid = channel.getChannelTypeUID();
if (chTypeUid != null) {
switch (channel.getChannelTypeUID().getId()) {
case CHANNEL_NOTIFICATION_SOUND:
String appKey = Objects.toString(getConfig().get(BoseSoundTouchConfiguration.APP_KEY),
null);
if (appKey != null && !appKey.isEmpty()) {
if (command instanceof StringType) {
String url = command.toString();
BoseSoundTouchNotificationChannelConfiguration notificationConfiguration = channel
.getConfiguration()
.as(BoseSoundTouchNotificationChannelConfiguration.class);
if (!url.isEmpty()) {
commandExecutor.playNotificationSound(appKey, notificationConfiguration,
url);
}
}
} else {
logger.warn("Missing app key - cannot use notification api");
}
return;
}
}
}
logger.warn("{} : Got command '{}' for channel '{}' which is unhandled!", getDeviceName(), command,
channelUID.getId());
break;
}
}
/**
* Returns the CommandExecutor of this handler
*
* @return the CommandExecutor of this handler
*/
public CommandExecutor getCommandExecutor() {
return commandExecutor;
}
/**
* Returns the Session this handler has opened
*
* @return the Session this handler has opened
*/
public Session getSession() {
return session;
}
/**
* Returns the name of the device delivered from itself
*
* @return the name of the device delivered from itself
*/
public String getDeviceName() {
return getThing().getProperties().get(DEVICE_INFO_NAME);
}
/**
* Returns the type of the device delivered from itself
*
* @return the type of the device delivered from itself
*/
public String getDeviceType() {
return getThing().getProperties().get(DEVICE_INFO_TYPE);
}
/**
* Returns the MAC Address of this device
*
* @return the MAC Address of this device (in format "123456789ABC")
*/
public String getMacAddress() {
return ((String) getThing().getConfiguration().get(BoseSoundTouchConfiguration.MAC_ADDRESS)).replaceAll(":",
"");
}
/**
* Returns the IP Address of this device
*
* @return the IP Address of this device
*/
public String getIPAddress() {
return (String) getThing().getConfiguration().getProperties().get(BoseSoundTouchConfiguration.HOST);
}
/**
* Provides the handler internal scheduler instance
*
* @return the {@link ScheduledExecutorService} instance used by this handler
*/
public ScheduledExecutorService getScheduler() {
return scheduler;
}
public PresetContainer getPresetContainer() {
return this.presetContainer;
}
@Override
public void onWebSocketConnect(Session session) {
logger.debug("{}: onWebSocketConnect('{}')", getDeviceName(), session);
this.session = session;
commandExecutor = new CommandExecutor(this);
updateStatus(ThingStatus.ONLINE);
}
@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);
commandExecutor = null;
}
if (session != null) {
session.close(StatusCode.SERVER_ERROR, getDeviceName() + ": Failure: " + e.getMessage());
session = null;
}
}
@Override
public void onWebSocketText(String msg) {
logger.debug("{}: onWebSocketText('{}')", getDeviceName(), msg);
try {
xmlResponseProcessor.handleMessage(msg);
} catch (Exception e) {
logger.warn("{}: Could not parse XML from string '{}'.", getDeviceName(), msg, e);
}
}
@Override
public void onWebSocketBinary(byte[] arr, int pos, int len) {
// we don't expect binary data so just dump if we get some...
logger.debug("{}: onWebSocketBinary({}, {}, '{}')", getDeviceName(), pos, len, Arrays.toString(arr));
}
@Override
public void onWebSocketClose(int code, String reason) {
logger.debug("{}: onClose({}, '{}')", getDeviceName(), code, reason);
missedPongsCount = 0;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
if (commandExecutor != null) {
commandExecutor.postOperationMode(OperationModeType.OFFLINE);
}
}
@Override
public void onWebSocketFrame(Frame frame) {
if (frame.getType() == Type.PONG) {
missedPongsCount = 0;
}
}
private synchronized void openConnection() {
closeConnection();
try {
client = new WebSocketClient();
// we need longer timeouts for web socket.
client.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);
} catch (Exception e) {
onWebSocketError(e);
}
}
private synchronized void closeConnection() {
if (session != null) {
try {
session.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);
}
if (client != null) {
try {
client.stop();
client.destroy();
} catch (Exception e) {
logger.debug("{}: Error while closing websocket communication: {} ({})", getDeviceName(),
e.getClass().getName(), e.getMessage());
}
client = null;
}
commandExecutor = null;
}
private void checkConnection() {
if (getThing().getStatus() != ThingStatus.ONLINE || session == null || client == null
|| commandExecutor == null) {
openConnection(); // try to reconnect....
}
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();
}
}
}
public void refreshPresetChannel() {
List<StateOption> stateOptions = presetContainer.getAllPresets().stream().map(e -> e.toStateOption())
.sorted(Comparator.comparing(StateOption::getValue)).collect(Collectors.toList());
stateOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PRESET), stateOptions);
}
public void handleGroupUpdated(BoseSoundTouchConfiguration masterPlayerConfiguration) {
String deviceId = getMacAddress();
if (masterPlayerConfiguration != null && masterPlayerConfiguration.macAddress != null) {
// Stereo pair
if (Objects.equals(masterPlayerConfiguration.macAddress, deviceId)) {
if (getThing().getThingTypeUID().equals(BST_10_THING_TYPE_UID)) {
logger.debug("{}: Stereo Pair was created and this is the master device.", getDeviceName());
} else {
logger.debug("{}: Unsupported operation for player of type: {}", getDeviceName(),
getThing().getThingTypeUID());
}
} else {
if (getThing().getThingTypeUID().equals(BST_10_THING_TYPE_UID)) {
logger.debug("{}: Stereo Pair was created and this is NOT the master device.", getDeviceName());
updateThing(editThing().withChannels(Collections.emptyList()).build());
} else {
logger.debug("{}: Unsupported operation for player of type: {}", getDeviceName(),
getThing().getThingTypeUID());
}
}
} else {
// NO Stereo Pair
if (getThing().getThingTypeUID().equals(BST_10_THING_TYPE_UID)) {
if (getThing().getChannels().isEmpty()) {
logger.debug("{}: Stereo Pair was disbounded. Restoring channels", getDeviceName());
updateThing(editThing().withChannels(getAllChannels(BST_10_THING_TYPE_UID)).build());
} else {
logger.debug("{}: Stereo Pair was disbounded.", getDeviceName());
}
} else {
logger.debug("{}: Unsupported operation for player of type: {}", getDeviceName(),
getThing().getThingTypeUID());
}
}
}
private List<Channel> getAllChannels(ThingTypeUID thingTypeUID) {
ThingHandlerCallback callback = getCallback();
if (callback == null) {
return Collections.emptyList();
}
return CHANNEL_IDS.stream()
.map(channelId -> callback.createChannelBuilder(new ChannelUID(getThing().getUID(), channelId),
createChannelTypeUID(thingTypeUID, channelId)).build())
.collect(Collectors.toList());
}
private ChannelTypeUID createChannelTypeUID(ThingTypeUID thingTypeUID, String channelId) {
if (CHANNEL_OPERATIONMODE.equals(channelId)) {
return createOperationModeChannelTypeUID(thingTypeUID);
}
return new ChannelTypeUID(BINDING_ID, channelId);
}
private ChannelTypeUID createOperationModeChannelTypeUID(ThingTypeUID thingTypeUID) {
String channelTypeId = CHANNEL_TYPE_OPERATION_MODE_DEFAULT;
if (BST_10_THING_TYPE_UID.equals(thingTypeUID) || BST_20_THING_TYPE_UID.equals(thingTypeUID)
|| BST_30_THING_TYPE_UID.equals(thingTypeUID)) {
channelTypeId = CHANNEL_TYPE_OPERATION_MODE_BST_10_20_30;
} else if (BST_300_THING_TYPE_UID.equals(thingTypeUID)) {
channelTypeId = CHANNEL_TYPE_OPERATION_MODE_BST_300;
} else if (BST_SA5A_THING_TYPE_UID.equals(thingTypeUID)) {
channelTypeId = CHANNEL_TYPE_OPERATION_MODE_BST_SA5A;
} else if (BST_WLA_THING_TYPE_UID.equals(thingTypeUID)) {
channelTypeId = CHANNEL_TYPE_OPERATION_MODE_BST_WLA;
} else if (BST_WSMS_THING_TYPE_UID.equals(thingTypeUID)) {
channelTypeId = CHANNEL_TYPE_OPERATION_MODE_DEFAULT;
}
return new ChannelTypeUID(BINDING_ID, channelTypeId);
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="bosesoundtouch" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Bose SoundTouch Binding</name>
<description>This is the binding for Bose SoundTouch devices.</description>
</binding:binding>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:bosesoundtouch:config">
<parameter name="host" type="text" required="true">
<label>Host Address</label>
<description>The Host / IP Address used for communication to this device.
</description>
</parameter>
<parameter name="macAddress" type="text" required="true">
<label>MAC Address</label>
<description>The MAC Address used for communication to this device.
</description>
</parameter>
<parameter name="appKey" type="text" required="false">
<label>Authorization Key</label>
<description>An authorization key used to identify the client application. Should be requested from the developer
portal.
</description>
</parameter>
</config-description>
<config-description uri="channel-type:bosesoundtouch:notificationSound">
<parameter name="notificationVolume" type="integer" min="10" max="70" step="1" unit="%" required="false">
<label>Notification Sound Volume</label>
<description>This indicates the desired volume level while playing the notification. The value represents a
percentage (0 to 100) of the full audible range of the speaker device. A value less than 10 or greater than 70 will
result in an error and not play the notification. Upon completion of the notification, the speaker volume will
return to its original value. If not present, the notification will play at the existing volume level.</description>
</parameter>
<parameter name="notificationService" type="text" required="true">
<label>Notification Service</label>
<description>This indicates the service providing the notification. This text will appear on the device display (when
available) and the SoundTouch application screen.
</description>
<default>Notification</default>
</parameter>
<parameter name="notificationReason" type="text" required="false">
<label>Notification Reason</label>
<description>This indicates the reason for the notification. This text will appear on the device display (when
available) and the SoundTouch application screen. If a reason string is not provided, the field with be blank.
</description>
</parameter>
<parameter name="notificationMessage" type="text" required="false">
<label>Notification Message</label>
<description>This indicates further details about the notification. This text will appear on the device display (when
available) and the SoundTouch application screen. If a message string is not provided, the field with be blank.
</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="10" extensible="notificationsound">
<label>Bose SoundTouch 10</label>
<description>Bose SoundTouch 10 Speaker</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="bass" typeId="bass"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_BST_10_20_30"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
<!-- Notification channels -->
<channel id="notificationsound" typeId="notificationsound"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="20" extensible="notificationsound">
<label>Bose SoundTouch 20</label>
<description>Bose SoundTouch 20 Speaker</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="bass" typeId="bass"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_BST_10_20_30"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
<!-- Notification channels -->
<channel id="notificationsound" typeId="notificationsound"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="30" extensible="notificationsound">
<label>Bose SoundTouch 30</label>
<description>Bose SoundTouch 30 Speaker</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="bass" typeId="bass"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_BST_10_20_30"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
<!-- Notification channels -->
<channel id="notificationsound" typeId="notificationsound"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="300">
<label>Bose SoundTouch 300</label>
<description>Bose SoundTouch 300 Speaker</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_BST_300"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sa5Amplifier">
<label>Bose SoundTouch SA-5 Amplifier</label>
<description>A Bose SoundTouch SA-5 Amplifier</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="bass" typeId="bass"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_BST_SA5_Amplifier"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="wirelessLinkAdapter">
<label>Bose SoundTouch Wireless Link Adapter</label>
<description>Bose SoundTouch Wireless Link Adapter</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_BST_WLA"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="device">
<label>Bose SoundTouch Device</label>
<description>Aan unknown Bose SoundTouch Device</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="bass" typeId="bass"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_default"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="waveSoundTouchMusicSystemIV">
<label>Bose Wave SoundTouch Music System IV</label>
<description>A Bose Wave SoundTouch Music System IV</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="volume" typeId="volume"/>
<channel id="bass" typeId="bass"/>
<channel id="mute" typeId="mute"/>
<channel id="operationMode" typeId="operationMode_default"/>
<channel id="playerControl" typeId="playerControl"/>
<channel id="preset" typeId="preset"/>
<channel id="saveAsPreset" typeId="saveAsPreset"/>
<channel id="keyCode" typeId="keyCode"/>
<channel id="rateEnabled" typeId="rateEnabled"/>
<channel id="skipEnabled" typeId="skipEnabled"/>
<channel id="skipPreviousEnabled" typeId="skipPreviousEnabled"/>
<channel id="nowPlayingAlbum" typeId="nowPlayingAlbum"/>
<channel id="nowPlayingArtist" typeId="nowPlayingArtist"/>
<channel id="nowPlayingArtwork" typeId="nowPlayingArtwork"/>
<channel id="nowPlayingDescription" typeId="nowPlayingDescription"/>
<channel id="nowPlayingGenre" typeId="nowPlayingGenre"/>
<channel id="nowPlayingItemName" typeId="nowPlayingItemName"/>
<channel id="nowPlayingStationLocation" typeId="nowPlayingStationLocation"/>
<channel id="nowPlayingStationName" typeId="nowPlayingStationName"/>
<channel id="nowPlayingTrack" typeId="nowPlayingTrack"/>
</channels>
<config-description-ref uri="thing-type:bosesoundtouch:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,292 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bosesoundtouch"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Channels common for all BOSE SOUNDTOUCH devices -->
<channel-type id="keyCode" advanced="true">
<item-type>String</item-type>
<label>Remote Key Code</label>
<description>Simulates pushing a remote control button</description>
<state readOnly="false">
<options>
<option value="PLAY">Play</option>
<option value="PAUSE">Pause</option>
<option value="STOP">Stop</option>
<option value="PREV_TRACK">Prev Track</option>
<option value="NEXT_TRACK">Next Track</option>
<option value="THUMBS_UP">Thumbs Up</option>
<option value="THUMBS_DOWN">Thumbs Down</option>
<option value="BOOKMARK">Bookmark</option>
<option value="POWER">Power</option>
<option value="MUTE">Mute</option>
<option value="VOLUME_UP">Volume Up</option>
<option value="VOLUME_DOWN">Volume Down</option>
<option value="PRESET_1">Preset 1</option>
<option value="PRESET_2">Preset 2</option>
<option value="PRESET_3">Preset 3</option>
<option value="PRESET_4">Preset 4</option>
<option value="PRESET_5">Preset 5</option>
<option value="PRESET_6">Preset 6</option>
<option value="AUX_INPUT">AUX Input</option>
<option value="SHUFFLE_OFF">Shuffle Off</option>
<option value="SHUFFLE_ON">Shuffle On</option>
<option value="REPEAT_OFF">Repeat Off</option>
<option value="REPEAT_ONE">Repeat One</option>
<option value="REPEAT_ALL">Repeat All</option>
<option value="PLAY_PAUSE">Play/Pause</option>
<option value="ADD_FAVORITE">Add Favorite</option>
<option value="REMOVE_FAVORITE">Remove Favorite</option>
</options>
</state>
</channel-type>
<channel-type id="mute">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Mutes the sound</description>
</channel-type>
<channel-type id="nowPlayingAlbum" advanced="true">
<item-type>String</item-type>
<label>Album</label>
<description>Current playing album name</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingArtist" advanced="true">
<item-type>String</item-type>
<label>Artist</label>
<description>Current playing artist name</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingArtwork">
<item-type>Image</item-type>
<label>Artwork</label>
<description>Artwork for the current playing song</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingDescription" advanced="true">
<item-type>String</item-type>
<label>Description</label>
<description>Description to current playing song</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingGenre" advanced="true">
<item-type>String</item-type>
<label>Genre</label>
<description>Genre of current playing song</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingItemName">
<item-type>String</item-type>
<label>Now Playing</label>
<description>Visible description shown in display</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingStationLocation" advanced="true">
<item-type>String</item-type>
<label>Station Location</label>
<description>Location of current playing radio station</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingStationName" advanced="true">
<item-type>String</item-type>
<label>Station Name</label>
<description>Name of current playing radio station</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="nowPlayingTrack" advanced="true">
<item-type>String</item-type>
<label>Track</label>
<description>Track currently playing</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="operationMode_default">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>Current Operation Mode</description>
<state readOnly="false">
<options>
<option value="STANDBY">Standby</option>
<option value="INTERNET_RADIO">Internet Radio</option>
<option value="BLUETOOTH">Bluetooth</option>
<option value="STORED_MUSIC">Stored Music</option>
<option value="AUX">AUX</option>
<option value="AUX1">AUX1</option>
<option value="AUX2">AUX2</option>
<option value="AUX3">AUX3</option>
<option value="TV">TV</option>
<option value="HDMI">HDMI</option>
<option value="SPOTIFY">Spotify</option>
<option value="PANDORA">Pandora</option>
<option value="DEEZER">Deezer</option>
<option value="SIRIUSXM">SiriusXM</option>
<option value="AMAZON">Amazon</option>
</options>
</state>
</channel-type>
<channel-type id="playerControl">
<item-type>Player</item-type>
<label>Player Control</label>
<description>Control the Player</description>
<category>Player</category>
</channel-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>Power</label>
<description>SoundTouch power state</description>
</channel-type>
<channel-type id="preset">
<item-type>Number</item-type>
<label>Preset</label>
<description>1-6 Preset of Soundtouch, >7 Binding Presets</description>
</channel-type>
<channel-type id="rateEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Rating Enabled</label>
<description>Current source allows rating</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="saveAsPreset" advanced="true">
<item-type>Number</item-type>
<label>Save as Preset</label>
<description>A selected presetable Contentitem is save as Preset with number >6</description>
</channel-type>
<channel-type id="skipEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Skip Enabled</label>
<description>Current source allows skipping to next track</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="skipPreviousEnabled" advanced="true">
<item-type>Switch</item-type>
<label>Skip/Previous Enabled</label>
<description>Current source allows scrolling through tracks</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="volume">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Set or get the volume</description>
<category>SoundVolume</category>
</channel-type>
<!-- Channels common for BOSE SOUNDTOUCH 10/20/30 devices -->
<channel-type id="bass" advanced="true">
<item-type>Number</item-type>
<label>Bass</label>
<description>Bass (-9 minimum, 0 maximum)</description>
<state min="-9" max="0" step="1" pattern="%d" readOnly="false"/>
</channel-type>
<channel-type id="operationMode_BST_10_20_30">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>Current Operation Mode</description>
<state readOnly="false">
<options>
<option value="STANDBY">Standby</option>
<option value="INTERNET_RADIO">Internet Radio</option>
<option value="BLUETOOTH">Bluetooth</option>
<option value="STORED_MUSIC">Stored Music</option>
<option value="AUX">AUX</option>
<option value="SPOTIFY">Spotify</option>
<option value="PANDORA">Pandora</option>
<option value="DEEZER">Deezer</option>
<option value="SIRIUSXM">SiriusXM</option>
<option value="AMAZON">Amazon</option>
</options>
</state>
</channel-type>
<!-- Channels common for BOSE SOUNDTOUCH 300 devices -->
<channel-type id="operationMode_BST_300">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>Current Operation Mode</description>
<state readOnly="false">
<options>
<option value="STANDBY">Standby</option>
<option value="INTERNET_RADIO">Internet Radio</option>
<option value="BLUETOOTH">Bluetooth</option>
<option value="STORED_MUSIC">Stored Music</option>
<option value="TV">TV</option>
<option value="HDMI">HDMI</option>
<option value="SPOTIFY">Spotify</option>
<option value="PANDORA">Pandora</option>
<option value="DEEZER">Deezer</option>
<option value="SIRIUSXM">SiriusXM</option>
<option value="AMAZON">Amazon</option>
</options>
</state>
</channel-type>
<!-- Channels common for BOSE SOUNDTOUCH Wireless Link Adapter devices -->
<channel-type id="operationMode_BST_WLA">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>Current Operation Mode</description>
<state readOnly="false">
<options>
<option value="STANDBY">Standby</option>
<option value="INTERNET_RADIO">Internet Radio</option>
<option value="BLUETOOTH">Bluetooth</option>
<option value="STORED_MUSIC">Stored Music</option>
<option value="AUX">AUX</option>
<option value="SPOTIFY">Spotify</option>
<option value="PANDORA">Pandora</option>
<option value="DEEZER">Deezer</option>
<option value="SIRIUSXM">SiriusXM</option>
<option value="AMAZON">Amazon</option>
</options>
</state>
</channel-type>
<!-- Channels common for BOSE SOUNDTOUCH SA-5 Amplifier devices -->
<channel-type id="operationMode_BST_SA5_Amplifier">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>Bose SoundTouch current Operation Mode</description>
<state readOnly="false">
<options>
<option value="STANDBY">Standby</option>
<option value="INTERNET_RADIO">Internet Radio</option>
<option value="BLUETOOTH">Bluetooth</option>
<option value="STORED_MUSIC">Stored Music</option>
<option value="AUX1">AUX1</option>
<option value="AUX2">AUX2</option>
<option value="AUX3">AUX3</option>
<option value="SPOTIFY">Spotify</option>
<option value="PANDORA">Pandora</option>
<option value="DEEZER">Deezer</option>
<option value="SIRIUSXM">SiriusXM</option>
<option value="AMAZON">Amazon</option>
</options>
</state>
</channel-type>
<!-- Notification channels -->
<channel-type id="notificationsound" advanced="true">
<item-type>String</item-type>
<label>Notification Sound</label>
<description>Play a notification sound by a given URI</description>
<config-description-ref uri="channel-type:bosesoundtouch:notificationSound"/>
</channel-type>
</thing:thing-descriptions>