2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bosesoundtouch.internal;
15 import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.*;
17 import java.util.Collection;
18 import java.util.HashMap;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.websocket.api.Session;
24 import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
25 import org.openhab.core.library.types.DecimalType;
26 import org.openhab.core.library.types.NextPreviousType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.PercentType;
29 import org.openhab.core.library.types.PlayPauseType;
30 import org.openhab.core.library.types.StringType;
31 import org.openhab.core.types.Command;
32 import org.openhab.core.types.State;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * The {@link CommandExecutor} class executes commands on the websocket
39 * @author Thomas Traunbauer - Initial contribution
40 * @author Kai Kreuzer - code clean up
43 public class CommandExecutor implements AvailableSources {
44 private final Logger logger = LoggerFactory.getLogger(CommandExecutor.class);
46 private final BoseSoundTouchHandler handler;
48 private boolean currentMuted;
49 private @Nullable ContentItem currentContentItem = null;
50 private @Nullable OperationModeType currentOperationMode;
52 private final Map<String, Boolean> mapOfAvailableFunctions = new HashMap<>();
55 * Creates a new instance of this class
57 * @param handler the handler that created this CommandExecutor
59 public CommandExecutor(BoseSoundTouchHandler handler) {
60 this.handler = handler;
61 getInformations(APIRequest.INFO);
62 currentOperationMode = OperationModeType.OFFLINE;
66 * Synchronizes the underlying storage container with the current value for the presets stored on the player
67 * by updating the available ones and deleting the cleared ones
69 * @param playerPresets a Map<Integer, ContentItems> containing the items currently stored on the player
71 public void updatePresetContainerFromPlayer(Map<Integer, ContentItem> playerPresets) {
72 playerPresets.forEach((k, v) -> {
74 handler.getPresetContainer().put(k, v);
75 } catch (ContentItemNotPresetableException e) {
76 logger.debug("{}: ContentItem is not presetable", handler.getDeviceName());
80 handler.refreshPresetChannel();
84 * Adds a ContentItem to the PresetContainer
86 * @param id the id the ContentItem should be reached
87 * @param contentItem the contentItem that should be saved as PRESET. Note that an eventually set presetID of the
88 * ContentItem will be overwritten with id
90 public void addContentItemToPresetContainer(int id, ContentItem contentItem) {
91 contentItem.setPresetID(id);
93 handler.getPresetContainer().put(id, contentItem);
94 } catch (ContentItemNotPresetableException e) {
95 logger.debug("{}: ContentItem is not presetable", handler.getDeviceName());
97 handler.refreshPresetChannel();
101 * Adds the current selected ContentItem to the PresetContainer
103 * @param command the command is a DecimalType, thats intValue will be used as id. The id the ContentItem should be
106 public void addCurrentContentItemToPresetContainer(DecimalType command) {
107 if (command.intValue() > 6) {
108 ContentItem localContentItem = currentContentItem;
109 if (localContentItem != null) {
110 addContentItemToPresetContainer(command.intValue(), localContentItem);
113 logger.warn("{}: Only PresetID >6 is allowed", handler.getDeviceName());
118 * Initializes an API Request on this device
120 * @param apiRequest the apiRequest thats informations should be collected
122 public void getInformations(APIRequest apiRequest) {
123 String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + apiRequest
124 + "\" method=\"GET\"><request requestID=\"0\"><info type=\"new\"/></request></header></msg>";
125 Session localSession = handler.getSession();
126 if (localSession != null) {
127 localSession.getRemote().sendStringByFuture(msg);
128 logger.debug("{}: sending request: {}", handler.getDeviceName(), msg);
133 * Sets the current ContentItem if it is valid, and inits an update of the operating values
137 public void setCurrentContentItem(ContentItem contentItem) {
138 if (contentItem.isValid()) {
139 ContentItem psFound = null;
140 Collection<ContentItem> listOfPresets = handler.getPresetContainer().getAllPresets();
141 for (ContentItem ps : listOfPresets) {
142 if (ps.isPresetable()) {
143 String localLocation = ps.getLocation();
144 if (localLocation != null) {
145 if (localLocation.equals(contentItem.getLocation())) {
152 if (psFound != null) {
153 presetID = psFound.getPresetID();
155 contentItem.setPresetID(presetID);
157 currentContentItem = contentItem;
159 updateOperatingValues();
163 * Sets the device is currently muted
167 public void setCurrentMuted(boolean muted) {
168 currentMuted = muted;
172 * Post Bass on the device
174 * @param command the command is Type of DecimalType
176 public void postBass(DecimalType command) {
177 if (isBassAvailable()) {
178 sendPostRequestInWebSocket("bass",
179 "<bass deviceID=\"" + handler.getMacAddress() + "\"" + ">" + command.intValue() + "</bass>");
181 logger.warn("{}: Bass modification not supported for this device", handler.getDeviceName());
186 * Post OperationMode on the device
188 * @param command the command is Type of OperationModeType
190 public void postOperationMode(OperationModeType command) {
191 if (command == OperationModeType.STANDBY) {
192 postPower(OnOffType.OFF);
195 ContentItemMaker contentItemMaker = new ContentItemMaker(this, handler.getPresetContainer());
196 ContentItem contentItem = contentItemMaker.getContentItem(command);
197 postContentItem(contentItem);
198 } catch (OperationModeNotAvailableException e) {
199 logger.warn("{}: OperationMode \"{}\" is not supported yet", handler.getDeviceName(),
201 } catch (NoInternetRadioPresetFoundException e) {
202 logger.warn("{}: Unable to switch to mode \"INTERNET_RADIO\". No PRESET defined",
203 handler.getDeviceName());
204 } catch (NoStoredMusicPresetFoundException e) {
205 logger.warn("{}: Unable to switch to mode: \"STORED_MUSIC\". No PRESET defined",
206 handler.getDeviceName());
208 updateOperatingValues();
213 * Post PlayerControl on the device
215 * @param command the command is Type of Command
217 public void postPlayerControl(Command command) {
218 if (command.equals(PlayPauseType.PLAY)) {
219 if (currentOperationMode == OperationModeType.STANDBY) {
220 postRemoteKey(RemoteKeyType.POWER);
222 postRemoteKey(RemoteKeyType.PLAY);
224 } else if (command.equals(PlayPauseType.PAUSE)) {
225 postRemoteKey(RemoteKeyType.PAUSE);
226 } else if (command.equals(NextPreviousType.NEXT)) {
227 postRemoteKey(RemoteKeyType.NEXT_TRACK);
228 } else if (command.equals(NextPreviousType.PREVIOUS)) {
229 postRemoteKey(RemoteKeyType.PREV_TRACK);
234 * Post Power on the device
236 * @param command the command is Type of OnOffType
238 public void postPower(OnOffType command) {
239 if (command.equals(OnOffType.ON)) {
240 if (currentOperationMode == OperationModeType.STANDBY) {
241 postRemoteKey(RemoteKeyType.POWER);
243 } else if (command.equals(OnOffType.OFF)) {
244 if (currentOperationMode != OperationModeType.STANDBY) {
245 postRemoteKey(RemoteKeyType.POWER);
248 updateOperatingValues();
252 * Post Preset on the device
254 * @param command the command is Type of DecimalType
256 public void postPreset(DecimalType command) {
257 ContentItem item = null;
259 item = handler.getPresetContainer().get(command.intValue());
260 postContentItem(item);
261 } catch (NoPresetFoundException e) {
262 logger.warn("{}: No preset found at id: {}", handler.getDeviceName(), command.intValue());
267 * Post RemoteKey on the device
269 * @param key the key is Type of RemoteKeyType
271 public void postRemoteKey(RemoteKeyType key) {
272 sendPostRequestInWebSocket("key", "mainNode=\"keyPress\"",
273 "<key state=\"press\" sender=\"Gabbo\">" + key.name() + "</key>");
274 sendPostRequestInWebSocket("key", "mainNode=\"keyRelease\"",
275 "<key state=\"release\" sender=\"Gabbo\">" + key.name() + "</key>");
279 * Post Volume on the device
281 * @param command the command is Type of PercentType
283 public void postVolume(PercentType command) {
284 sendPostRequestInWebSocket("volume",
285 "<volume deviceID=\"" + handler.getMacAddress() + "\"" + ">" + command.intValue() + "</volume>");
289 * Post VolumeMute on the device
291 * @param command the command is Type of OnOffType
293 public void postVolumeMuted(OnOffType command) {
294 if (command.equals(OnOffType.ON)) {
297 postRemoteKey(RemoteKeyType.MUTE);
299 } else if (command.equals(OnOffType.OFF)) {
301 currentMuted = false;
302 postRemoteKey(RemoteKeyType.MUTE);
308 * Update GUI for Basslevel
310 * @param state the state is Type of DecimalType
312 public void updateBassLevelGUIState(DecimalType state) {
313 handler.updateState(CHANNEL_BASS, state);
317 * Update GUI for Volume
319 * @param state the state is Type of PercentType
321 public void updateVolumeGUIState(PercentType state) {
322 handler.updateState(CHANNEL_VOLUME, state);
326 * Update GUI for OperationMode
328 * @param state the state is Type of StringType
330 public void updateOperationModeGUIState(StringType state) {
331 handler.updateState(CHANNEL_OPERATIONMODE, state);
335 * Update GUI for PlayerControl
337 * @param state the state is Type of State
339 public void updatePlayerControlGUIState(State state) {
340 handler.updateState(CHANNEL_PLAYER_CONTROL, state);
344 * Update GUI for Power
346 * @param state the state is Type of OnOffType
348 public void updatePowerStateGUIState(OnOffType state) {
349 handler.updateState(CHANNEL_POWER, state);
353 * Update GUI for Preset
355 * @param state the state is Type of DecimalType
357 public void updatePresetGUIState(DecimalType state) {
358 handler.updateState(CHANNEL_PRESET, state);
361 private void postContentItem(ContentItem contentItem) {
362 setCurrentContentItem(contentItem);
363 sendPostRequestInWebSocket("select", "", contentItem.generateXML());
366 private void sendPostRequestInWebSocket(String url, String postData) {
367 sendPostRequestInWebSocket(url, "", postData);
370 private void sendPostRequestInWebSocket(String url, String infoAddon, String postData) {
372 String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + url
373 + "\" method=\"POST\"><request requestID=\"" + id + "\"><info " + infoAddon
374 + " type=\"new\"/></request></header><body>" + postData + "</body></msg>";
375 Session localSession = handler.getSession();
376 if (localSession != null) {
377 localSession.getRemote().sendStringByFuture(msg);
378 logger.debug("{}: sending request: {}", handler.getDeviceName(), msg);
380 handler.onWebSocketError(new NullPointerException("NPE: Session is unexpected null"));
384 private void updateOperatingValues() {
385 OperationModeType operationMode;
386 ContentItem localContentItem = currentContentItem;
387 if (localContentItem != null) {
388 updatePresetGUIState(new DecimalType(localContentItem.getPresetID()));
389 operationMode = localContentItem.getOperationMode();
391 operationMode = OperationModeType.STANDBY;
394 updateOperationModeGUIState(new StringType(operationMode.toString()));
395 currentOperationMode = operationMode;
396 if (currentOperationMode == OperationModeType.STANDBY) {
397 updatePowerStateGUIState(OnOffType.OFF);
398 updatePlayerControlGUIState(PlayPauseType.PAUSE);
400 updatePowerStateGUIState(OnOffType.ON);
405 public boolean isBluetoothAvailable() {
406 return isSourceAvailable("bluetooth");
410 public boolean isAUXAvailable() {
411 return isSourceAvailable("aux");
415 public boolean isAUX1Available() {
416 return isSourceAvailable("aux1");
420 public boolean isAUX2Available() {
421 return isSourceAvailable("aux2");
425 public boolean isAUX3Available() {
426 return isSourceAvailable("aux3");
430 public boolean isTVAvailable() {
431 return isSourceAvailable("tv");
435 public boolean isHDMI1Available() {
436 return isSourceAvailable("hdmi1");
440 public boolean isInternetRadioAvailable() {
441 return isSourceAvailable("internetRadio");
445 public boolean isStoredMusicAvailable() {
446 return isSourceAvailable("storedMusic");
450 public boolean isBassAvailable() {
451 return isSourceAvailable("bass");
455 public void setBluetoothAvailable(boolean bluetooth) {
456 mapOfAvailableFunctions.put("bluetooth", bluetooth);
460 public void setAUXAvailable(boolean aux) {
461 mapOfAvailableFunctions.put("aux", aux);
465 public void setAUX1Available(boolean aux1) {
466 mapOfAvailableFunctions.put("aux1", aux1);
470 public void setAUX2Available(boolean aux2) {
471 mapOfAvailableFunctions.put("aux2", aux2);
475 public void setAUX3Available(boolean aux3) {
476 mapOfAvailableFunctions.put("aux3", aux3);
480 public void setStoredMusicAvailable(boolean storedMusic) {
481 mapOfAvailableFunctions.put("storedMusic", storedMusic);
485 public void setInternetRadioAvailable(boolean internetRadio) {
486 mapOfAvailableFunctions.put("internetRadio", internetRadio);
490 public void setTVAvailable(boolean tv) {
491 mapOfAvailableFunctions.put("tv", tv);
495 public void setHDMI1Available(boolean hdmi1) {
496 mapOfAvailableFunctions.put("hdmi1", hdmi1);
500 public void setBassAvailable(boolean bass) {
501 mapOfAvailableFunctions.put("bass", bass);
504 private boolean isSourceAvailable(String source) {
505 Boolean isAvailable = mapOfAvailableFunctions.get(source);
506 if (isAvailable == null) {
513 public void playNotificationSound(String appKey, BoseSoundTouchNotificationChannelConfiguration notificationConfig,
515 String msg = "<play_info>" + "<app_key>" + appKey + "</app_key>" + "<url>" + fileUrl + "</url>" + "<service>"
516 + notificationConfig.notificationService + "</service>"
517 + (notificationConfig.notificationReason != null
518 ? "<reason>" + notificationConfig.notificationReason + "</reason>"
520 + (notificationConfig.notificationMessage != null
521 ? "<message>" + notificationConfig.notificationMessage + "</message>"
523 + (notificationConfig.notificationVolume != null
524 ? "<volume>" + notificationConfig.notificationVolume + "</volume>"
528 sendPostRequestInWebSocket("speaker", msg);