2 * Copyright (c) 2010-2022 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;
160 updateOperatingValues();
164 * Sets the device is currently muted
168 public void setCurrentMuted(boolean muted) {
169 currentMuted = muted;
173 * Post Bass on the device
175 * @param command the command is Type of DecimalType
177 public void postBass(DecimalType command) {
178 if (isBassAvailable()) {
179 sendPostRequestInWebSocket("bass",
180 "<bass deviceID=\"" + handler.getMacAddress() + "\"" + ">" + command.intValue() + "</bass>");
182 logger.warn("{}: Bass modification not supported for this device", handler.getDeviceName());
187 * Post OperationMode on the device
189 * @param command the command is Type of OperationModeType
191 public void postOperationMode(OperationModeType command) {
192 if (command == OperationModeType.STANDBY) {
193 postPower(OnOffType.OFF);
196 ContentItemMaker contentItemMaker = new ContentItemMaker(this, handler.getPresetContainer());
197 ContentItem contentItem = contentItemMaker.getContentItem(command);
198 postContentItem(contentItem);
199 } catch (OperationModeNotAvailableException e) {
200 logger.warn("{}: OperationMode \"{}\" is not supported yet", handler.getDeviceName(),
202 } catch (NoInternetRadioPresetFoundException e) {
203 logger.warn("{}: Unable to switch to mode \"INTERNET_RADIO\". No PRESET defined",
204 handler.getDeviceName());
205 } catch (NoStoredMusicPresetFoundException e) {
206 logger.warn("{}: Unable to switch to mode: \"STORED_MUSIC\". No PRESET defined",
207 handler.getDeviceName());
209 updateOperatingValues();
214 * Post PlayerControl on the device
216 * @param command the command is Type of Command
218 public void postPlayerControl(Command command) {
219 if (command.equals(PlayPauseType.PLAY)) {
220 if (currentOperationMode == OperationModeType.STANDBY) {
221 postRemoteKey(RemoteKeyType.POWER);
223 postRemoteKey(RemoteKeyType.PLAY);
225 } else if (command.equals(PlayPauseType.PAUSE)) {
226 postRemoteKey(RemoteKeyType.PAUSE);
227 } else if (command.equals(NextPreviousType.NEXT)) {
228 postRemoteKey(RemoteKeyType.NEXT_TRACK);
229 } else if (command.equals(NextPreviousType.PREVIOUS)) {
230 postRemoteKey(RemoteKeyType.PREV_TRACK);
235 * Post Power on the device
237 * @param command the command is Type of OnOffType
239 public void postPower(OnOffType command) {
240 if (command.equals(OnOffType.ON)) {
241 if (currentOperationMode == OperationModeType.STANDBY) {
242 postRemoteKey(RemoteKeyType.POWER);
244 } else if (command.equals(OnOffType.OFF)) {
245 if (currentOperationMode != OperationModeType.STANDBY) {
246 postRemoteKey(RemoteKeyType.POWER);
249 updateOperatingValues();
253 * Post Preset on the device
255 * @param command the command is Type of DecimalType
257 public void postPreset(DecimalType command) {
258 ContentItem item = null;
260 item = handler.getPresetContainer().get(command.intValue());
261 postContentItem(item);
262 } catch (NoPresetFoundException e) {
263 logger.warn("{}: No preset found at id: {}", handler.getDeviceName(), command.intValue());
268 * Post RemoteKey on the device
270 * @param command the command is Type of RemoteKeyType
272 public void postRemoteKey(RemoteKeyType key) {
273 sendPostRequestInWebSocket("key", "mainNode=\"keyPress\"",
274 "<key state=\"press\" sender=\"Gabbo\">" + key.name() + "</key>");
275 sendPostRequestInWebSocket("key", "mainNode=\"keyRelease\"",
276 "<key state=\"release\" sender=\"Gabbo\">" + key.name() + "</key>");
280 * Post Volume on the device
282 * @param command the command is Type of PercentType
284 public void postVolume(PercentType command) {
285 sendPostRequestInWebSocket("volume",
286 "<volume deviceID=\"" + handler.getMacAddress() + "\"" + ">" + command.intValue() + "</volume>");
290 * Post VolumeMute on the device
292 * @param command the command is Type of OnOffType
294 public void postVolumeMuted(OnOffType command) {
295 if (command.equals(OnOffType.ON)) {
298 postRemoteKey(RemoteKeyType.MUTE);
300 } else if (command.equals(OnOffType.OFF)) {
302 currentMuted = false;
303 postRemoteKey(RemoteKeyType.MUTE);
309 * Update GUI for Basslevel
311 * @param state the state is Type of DecimalType
313 public void updateBassLevelGUIState(DecimalType state) {
314 handler.updateState(CHANNEL_BASS, state);
318 * Update GUI for Volume
320 * @param state the state is Type of PercentType
322 public void updateVolumeGUIState(PercentType state) {
323 handler.updateState(CHANNEL_VOLUME, state);
327 * Update GUI for OperationMode
329 * @param state the state is Type of StringType
331 public void updateOperationModeGUIState(StringType state) {
332 handler.updateState(CHANNEL_OPERATIONMODE, state);
336 * Update GUI for PlayerControl
338 * @param state the state is Type of State
340 public void updatePlayerControlGUIState(State state) {
341 handler.updateState(CHANNEL_PLAYER_CONTROL, state);
345 * Update GUI for Power
347 * @param state the state is Type of OnOffType
349 public void updatePowerStateGUIState(OnOffType state) {
350 handler.updateState(CHANNEL_POWER, state);
354 * Update GUI for Preset
356 * @param state the state is Type of DecimalType
358 public void updatePresetGUIState(DecimalType state) {
359 handler.updateState(CHANNEL_PRESET, state);
362 private void postContentItem(ContentItem contentItem) {
363 setCurrentContentItem(contentItem);
364 sendPostRequestInWebSocket("select", "", contentItem.generateXML());
367 private void sendPostRequestInWebSocket(String url, String postData) {
368 sendPostRequestInWebSocket(url, "", postData);
371 private void sendPostRequestInWebSocket(String url, String infoAddon, String postData) {
373 String msg = "<msg><header " + "deviceID=\"" + handler.getMacAddress() + "\"" + " url=\"" + url
374 + "\" method=\"POST\"><request requestID=\"" + id + "\"><info " + infoAddon
375 + " type=\"new\"/></request></header><body>" + postData + "</body></msg>";
376 Session localSession = handler.getSession();
377 if (localSession != null) {
378 localSession.getRemote().sendStringByFuture(msg);
379 logger.debug("{}: sending request: {}", handler.getDeviceName(), msg);
381 handler.onWebSocketError(new NullPointerException("NPE: Session is unexpected null"));
385 private void updateOperatingValues() {
386 OperationModeType operationMode;
387 ContentItem localContentItem = currentContentItem;
388 if (localContentItem != null) {
389 updatePresetGUIState(new DecimalType(localContentItem.getPresetID()));
390 operationMode = localContentItem.getOperationMode();
392 operationMode = OperationModeType.STANDBY;
395 updateOperationModeGUIState(new StringType(operationMode.toString()));
396 currentOperationMode = operationMode;
397 if (currentOperationMode == OperationModeType.STANDBY) {
398 updatePowerStateGUIState(OnOffType.OFF);
399 updatePlayerControlGUIState(PlayPauseType.PAUSE);
401 updatePowerStateGUIState(OnOffType.ON);
406 public boolean isBluetoothAvailable() {
407 return isSourceAvailable("bluetooth");
411 public boolean isAUXAvailable() {
412 return isSourceAvailable("aux");
416 public boolean isAUX1Available() {
417 return isSourceAvailable("aux1");
421 public boolean isAUX2Available() {
422 return isSourceAvailable("aux2");
426 public boolean isAUX3Available() {
427 return isSourceAvailable("aux3");
431 public boolean isTVAvailable() {
432 return isSourceAvailable("tv");
436 public boolean isHDMI1Available() {
437 return isSourceAvailable("hdmi1");
441 public boolean isInternetRadioAvailable() {
442 return isSourceAvailable("internetRadio");
446 public boolean isStoredMusicAvailable() {
447 return isSourceAvailable("storedMusic");
451 public boolean isBassAvailable() {
452 return isSourceAvailable("bass");
456 public void setBluetoothAvailable(boolean bluetooth) {
457 mapOfAvailableFunctions.put("bluetooth", bluetooth);
461 public void setAUXAvailable(boolean aux) {
462 mapOfAvailableFunctions.put("aux", aux);
466 public void setAUX1Available(boolean aux1) {
467 mapOfAvailableFunctions.put("aux1", aux1);
471 public void setAUX2Available(boolean aux2) {
472 mapOfAvailableFunctions.put("aux2", aux2);
476 public void setAUX3Available(boolean aux3) {
477 mapOfAvailableFunctions.put("aux3", aux3);
481 public void setStoredMusicAvailable(boolean storedMusic) {
482 mapOfAvailableFunctions.put("storedMusic", storedMusic);
486 public void setInternetRadioAvailable(boolean internetRadio) {
487 mapOfAvailableFunctions.put("internetRadio", internetRadio);
491 public void setTVAvailable(boolean tv) {
492 mapOfAvailableFunctions.put("tv", tv);
496 public void setHDMI1Available(boolean hdmi1) {
497 mapOfAvailableFunctions.put("hdmi1", hdmi1);
501 public void setBassAvailable(boolean bass) {
502 mapOfAvailableFunctions.put("bass", bass);
505 private boolean isSourceAvailable(String source) {
506 Boolean isAvailable = mapOfAvailableFunctions.get(source);
507 if (isAvailable == null) {
514 public void playNotificationSound(String appKey, BoseSoundTouchNotificationChannelConfiguration notificationConfig,
516 String msg = "<play_info>" + "<app_key>" + appKey + "</app_key>" + "<url>" + fileUrl + "</url>" + "<service>"
517 + notificationConfig.notificationService + "</service>"
518 + (notificationConfig.notificationReason != null
519 ? "<reason>" + notificationConfig.notificationReason + "</reason>"
521 + (notificationConfig.notificationMessage != null
522 ? "<message>" + notificationConfig.notificationMessage + "</message>"
524 + (notificationConfig.notificationVolume != null
525 ? "<volume>" + notificationConfig.notificationVolume + "</volume>"
529 sendPostRequestInWebSocket("speaker", msg);