2 * Copyright (c) 2010-2021 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.onkyo.internal.handler;
15 import static org.openhab.binding.onkyo.internal.OnkyoBindingConstants.*;
17 import java.io.IOException;
18 import java.io.StringReader;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.xml.parsers.DocumentBuilder;
27 import javax.xml.parsers.DocumentBuilderFactory;
28 import javax.xml.parsers.ParserConfigurationException;
30 import org.openhab.binding.onkyo.internal.OnkyoAlbumArt;
31 import org.openhab.binding.onkyo.internal.OnkyoConnection;
32 import org.openhab.binding.onkyo.internal.OnkyoEventListener;
33 import org.openhab.binding.onkyo.internal.OnkyoStateDescriptionProvider;
34 import org.openhab.binding.onkyo.internal.ServiceType;
35 import org.openhab.binding.onkyo.internal.automation.modules.OnkyoThingActions;
36 import org.openhab.binding.onkyo.internal.config.OnkyoDeviceConfiguration;
37 import org.openhab.binding.onkyo.internal.eiscp.EiscpCommand;
38 import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
39 import org.openhab.core.audio.AudioHTTPServer;
40 import org.openhab.core.io.net.http.HttpUtil;
41 import org.openhab.core.io.transport.upnp.UpnpIOService;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.IncreaseDecreaseType;
44 import org.openhab.core.library.types.NextPreviousType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.library.types.PlayPauseType;
48 import org.openhab.core.library.types.RawType;
49 import org.openhab.core.library.types.RewindFastforwardType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.openhab.core.types.StateOption;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64 import org.w3c.dom.Document;
65 import org.w3c.dom.Element;
66 import org.w3c.dom.NodeList;
67 import org.xml.sax.InputSource;
68 import org.xml.sax.SAXException;
71 * The {@link OnkyoHandler} is responsible for handling commands, which are
72 * sent to one of the channels.
74 * @author Paul Frank - Initial contribution
75 * @author Marcel Verpaalen - parsing additional commands
76 * @author Pauli Anttila - lot of refactoring
77 * @author Stewart Cossey - add dynamic state description provider
79 public class OnkyoHandler extends UpnpAudioSinkHandler implements OnkyoEventListener {
81 private final Logger logger = LoggerFactory.getLogger(OnkyoHandler.class);
83 private OnkyoDeviceConfiguration configuration;
85 private OnkyoConnection connection;
86 private ScheduledFuture<?> resourceUpdaterFuture;
87 @SuppressWarnings("unused")
88 private int currentInput = -1;
89 private State volumeLevelZone1 = UnDefType.UNDEF;
90 private State volumeLevelZone2 = UnDefType.UNDEF;
91 private State volumeLevelZone3 = UnDefType.UNDEF;
92 private State lastPowerState = OnOffType.OFF;
94 private final OnkyoStateDescriptionProvider stateDescriptionProvider;
96 private final OnkyoAlbumArt onkyoAlbumArt = new OnkyoAlbumArt();
98 private static final int NET_USB_ID = 43;
100 public OnkyoHandler(Thing thing, UpnpIOService upnpIOService, AudioHTTPServer audioHTTPServer, String callbackUrl,
101 OnkyoStateDescriptionProvider stateDescriptionProvider) {
102 super(thing, upnpIOService, audioHTTPServer, callbackUrl);
103 this.stateDescriptionProvider = stateDescriptionProvider;
107 * Initialize the state of the receiver.
110 public void initialize() {
111 logger.debug("Initializing handler for Onkyo Receiver");
112 configuration = getConfigAs(OnkyoDeviceConfiguration.class);
113 logger.info("Using configuration: {}", configuration.toString());
115 connection = new OnkyoConnection(configuration.ipAddress, configuration.port);
116 connection.addEventListener(this);
118 scheduler.execute(() -> {
119 logger.debug("Open connection to Onkyo Receiver @{}", connection.getConnectionName());
120 connection.openConnection();
121 if (connection.isConnected()) {
122 updateStatus(ThingStatus.ONLINE);
124 sendCommand(EiscpCommand.INFO_QUERY);
125 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
129 if (configuration.refreshInterval > 0) {
130 // Start resource refresh updater
131 resourceUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
133 logger.debug("Send resource update requests to Onkyo Receiver @{}", connection.getConnectionName());
135 } catch (LinkageError e) {
136 logger.warn("Failed to send resource update requests to Onkyo Receiver @{}. Cause: {}",
137 connection.getConnectionName(), e.getMessage());
138 } catch (Exception ex) {
139 logger.warn("Exception in resource refresh Thread Onkyo Receiver @{}. Cause: {}",
140 connection.getConnectionName(), ex.getMessage());
142 }, configuration.refreshInterval, configuration.refreshInterval, TimeUnit.SECONDS);
147 public void dispose() {
149 if (resourceUpdaterFuture != null) {
150 resourceUpdaterFuture.cancel(true);
152 if (connection != null) {
153 connection.removeEventListener(this);
154 connection.closeConnection();
159 public void handleCommand(ChannelUID channelUID, Command command) {
160 logger.debug("handleCommand for channel {}: {}", channelUID.getId(), command.toString());
161 switch (channelUID.getId()) {
167 if (command instanceof OnOffType) {
168 sendCommand(EiscpCommand.POWER_SET, command);
169 } else if (command.equals(RefreshType.REFRESH)) {
170 sendCommand(EiscpCommand.POWER_QUERY);
174 if (command instanceof OnOffType) {
175 sendCommand(EiscpCommand.MUTE_SET, command);
176 } else if (command.equals(RefreshType.REFRESH)) {
177 sendCommand(EiscpCommand.MUTE_QUERY);
181 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, command);
184 if (command instanceof DecimalType) {
185 selectInput(((DecimalType) command).intValue());
186 } else if (command.equals(RefreshType.REFRESH)) {
187 sendCommand(EiscpCommand.SOURCE_QUERY);
190 case CHANNEL_LISTENMODE:
191 if (command instanceof DecimalType) {
192 sendCommand(EiscpCommand.LISTEN_MODE_SET, command);
193 } else if (command.equals(RefreshType.REFRESH)) {
194 sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
202 case CHANNEL_POWERZONE2:
203 if (command instanceof OnOffType) {
204 sendCommand(EiscpCommand.ZONE2_POWER_SET, command);
205 } else if (command.equals(RefreshType.REFRESH)) {
206 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
209 case CHANNEL_MUTEZONE2:
210 if (command instanceof OnOffType) {
211 sendCommand(EiscpCommand.ZONE2_MUTE_SET, command);
212 } else if (command.equals(RefreshType.REFRESH)) {
213 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
216 case CHANNEL_VOLUMEZONE2:
217 handleVolumeSet(EiscpCommand.Zone.ZONE2, volumeLevelZone2, command);
219 case CHANNEL_INPUTZONE2:
220 if (command instanceof DecimalType) {
221 sendCommand(EiscpCommand.ZONE2_SOURCE_SET, command);
222 } else if (command.equals(RefreshType.REFRESH)) {
223 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
231 case CHANNEL_POWERZONE3:
232 if (command instanceof OnOffType) {
233 sendCommand(EiscpCommand.ZONE3_POWER_SET, command);
234 } else if (command.equals(RefreshType.REFRESH)) {
235 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
238 case CHANNEL_MUTEZONE3:
239 if (command instanceof OnOffType) {
240 sendCommand(EiscpCommand.ZONE3_MUTE_SET, command);
241 } else if (command.equals(RefreshType.REFRESH)) {
242 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
245 case CHANNEL_VOLUMEZONE3:
246 handleVolumeSet(EiscpCommand.Zone.ZONE3, volumeLevelZone3, command);
248 case CHANNEL_INPUTZONE3:
249 if (command instanceof DecimalType) {
250 sendCommand(EiscpCommand.ZONE3_SOURCE_SET, command);
251 } else if (command.equals(RefreshType.REFRESH)) {
252 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
260 case CHANNEL_CONTROL:
261 if (command instanceof PlayPauseType) {
262 if (command.equals(PlayPauseType.PLAY)) {
263 sendCommand(EiscpCommand.NETUSB_OP_PLAY);
264 } else if (command.equals(PlayPauseType.PAUSE)) {
265 sendCommand(EiscpCommand.NETUSB_OP_PAUSE);
267 } else if (command instanceof NextPreviousType) {
268 if (command.equals(NextPreviousType.NEXT)) {
269 sendCommand(EiscpCommand.NETUSB_OP_TRACKUP);
270 } else if (command.equals(NextPreviousType.PREVIOUS)) {
271 sendCommand(EiscpCommand.NETUSB_OP_TRACKDWN);
273 } else if (command instanceof RewindFastforwardType) {
274 if (command.equals(RewindFastforwardType.REWIND)) {
275 sendCommand(EiscpCommand.NETUSB_OP_REW);
276 } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
277 sendCommand(EiscpCommand.NETUSB_OP_FF);
279 } else if (command.equals(RefreshType.REFRESH)) {
280 sendCommand(EiscpCommand.NETUSB_PLAY_STATUS_QUERY);
283 case CHANNEL_PLAY_URI:
284 handlePlayUri(command);
286 case CHANNEL_ALBUM_ART:
287 case CHANNEL_ALBUM_ART_URL:
288 if (command.equals(RefreshType.REFRESH)) {
289 sendCommand(EiscpCommand.NETUSB_ALBUM_ART_QUERY);
293 if (command.equals(RefreshType.REFRESH)) {
294 sendCommand(EiscpCommand.NETUSB_SONG_ARTIST_QUERY);
298 if (command.equals(RefreshType.REFRESH)) {
299 sendCommand(EiscpCommand.NETUSB_SONG_ALBUM_QUERY);
303 if (command.equals(RefreshType.REFRESH)) {
304 sendCommand(EiscpCommand.NETUSB_SONG_TITLE_QUERY);
307 case CHANNEL_CURRENTPLAYINGTIME:
308 if (command.equals(RefreshType.REFRESH)) {
309 sendCommand(EiscpCommand.NETUSB_SONG_ELAPSEDTIME_QUERY);
317 case CHANNEL_NET_MENU_CONTROL:
318 if (command instanceof StringType) {
319 final String cmdName = command.toString();
320 handleNetMenuCommand(cmdName);
323 case CHANNEL_NET_MENU_TITLE:
324 if (command.equals(RefreshType.REFRESH)) {
325 sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
328 case CHANNEL_AUDIOINFO:
329 if (command.equals(RefreshType.REFRESH)) {
330 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
338 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
343 private void populateInputs(NodeList selectorlist) {
344 List<StateOption> options = new ArrayList<>();
346 for (int i = 0; i < selectorlist.getLength(); i++) {
347 Element selectorItem = (Element) selectorlist.item(i);
349 options.add(new StateOption(String.valueOf(Integer.parseInt(selectorItem.getAttribute("id"), 16)),
350 selectorItem.getAttribute("name")));
352 logger.debug("Got Input List from Receiver {}", options);
354 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUT), options);
355 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE2), options);
356 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE3), options);
359 private void doPowerOnCheck(State state) {
360 if (configuration.refreshInterval == 0 && lastPowerState == OnOffType.OFF && state == OnOffType.ON) {
361 sendCommand(EiscpCommand.INFO_QUERY);
363 lastPowerState = state;
367 public void statusUpdateReceived(String ip, EiscpMessage data) {
368 logger.debug("Received status update from Onkyo Receiver @{}: data={}", connection.getConnectionName(), data);
370 updateStatus(ThingStatus.ONLINE);
373 EiscpCommand receivedCommand = null;
376 receivedCommand = EiscpCommand.getCommandByCommandAndValueStr(data.getCommand(), "");
377 } catch (IllegalArgumentException ex) {
378 logger.debug("Received unknown status update from Onkyo Receiver @{}: data={}",
379 connection.getConnectionName(), data);
383 logger.debug("Received command {}", receivedCommand);
385 switch (receivedCommand) {
390 State powerState = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
391 updateState(CHANNEL_POWER, powerState);
392 doPowerOnCheck(powerState);
395 updateState(CHANNEL_MUTE, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
398 volumeLevelZone1 = handleReceivedVolume(
399 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
400 updateState(CHANNEL_VOLUME, volumeLevelZone1);
403 updateState(CHANNEL_INPUT, convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
406 updateState(CHANNEL_LISTENMODE,
407 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
414 State powerZone2State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
415 updateState(CHANNEL_POWERZONE2, powerZone2State);
416 doPowerOnCheck(powerZone2State);
419 updateState(CHANNEL_MUTEZONE2, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
422 volumeLevelZone2 = handleReceivedVolume(
423 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
424 updateState(CHANNEL_VOLUMEZONE2, volumeLevelZone2);
427 updateState(CHANNEL_INPUTZONE2,
428 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
435 State powerZone3State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
436 updateState(CHANNEL_POWERZONE3, powerZone3State);
437 doPowerOnCheck(powerZone3State);
440 updateState(CHANNEL_MUTEZONE3, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
443 volumeLevelZone3 = handleReceivedVolume(
444 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
445 updateState(CHANNEL_VOLUMEZONE3, volumeLevelZone3);
448 updateState(CHANNEL_INPUTZONE3,
449 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
456 case NETUSB_SONG_ARTIST:
457 updateState(CHANNEL_ARTIST, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
459 case NETUSB_SONG_ALBUM:
460 updateState(CHANNEL_ALBUM, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
462 case NETUSB_SONG_TITLE:
463 updateState(CHANNEL_TITLE, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
465 case NETUSB_SONG_ELAPSEDTIME:
466 updateState(CHANNEL_CURRENTPLAYINGTIME,
467 convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
469 case NETUSB_PLAY_STATUS:
470 updateState(CHANNEL_CONTROL, convertNetUsbPlayStatus(data.getValue()));
472 case NETUSB_ALBUM_ART:
473 updateAlbumArt(data.getValue());
476 updateNetTitle(data.getValue());
479 updateNetMenu(data.getValue());
486 updateState(CHANNEL_AUDIOINFO, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
487 logger.debug("audioinfo message: '{}'", data.getValue());
490 processInfo(data.getValue());
491 logger.debug("Info message: '{}'", data.getValue());
495 logger.debug("Received unhandled status update from Onkyo Receiver @{}: data={}",
496 connection.getConnectionName(), data);
500 } catch (Exception ex) {
501 logger.warn("Exception in statusUpdateReceived for Onkyo Receiver @{}. Cause: {}, data received: {}",
502 connection.getConnectionName(), ex.getMessage(), data);
506 private void processInfo(String infoXML) {
508 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
509 // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
510 factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
511 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
512 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
513 factory.setXIncludeAware(false);
514 factory.setExpandEntityReferences(false);
515 DocumentBuilder builder = factory.newDocumentBuilder();
516 try (StringReader sr = new StringReader(infoXML)) {
517 InputSource is = new InputSource(sr);
518 Document doc = builder.parse(is);
520 NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
521 populateInputs(selectableInputs);
523 } catch (ParserConfigurationException | SAXException | IOException e) {
524 logger.debug("Error occured during Info XML parsing.", e);
529 public void connectionError(String ip, String errorMsg) {
530 logger.debug("Connection error occurred to Onkyo Receiver @{}", ip);
531 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
534 private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
535 State state = UnDefType.UNDEF;
540 if (data.contentEquals("N/A")) {
541 state = UnDefType.UNDEF;
543 } else if (classToConvert == OnOffType.class) {
544 index = Integer.parseInt(data, 16);
545 state = index == 0 ? OnOffType.OFF : OnOffType.ON;
547 } else if (classToConvert == DecimalType.class) {
548 index = Integer.parseInt(data, 16);
549 state = new DecimalType(index);
551 } else if (classToConvert == PercentType.class) {
552 index = Integer.parseInt(data, 16);
553 state = new PercentType(index);
555 } else if (classToConvert == StringType.class) {
556 state = new StringType(data);
559 } catch (Exception e) {
560 logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
563 logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
567 private void handleNetMenuCommand(String cmdName) {
568 if ("Up".equals(cmdName)) {
569 sendCommand(EiscpCommand.NETUSB_OP_UP);
570 } else if ("Down".equals(cmdName)) {
571 sendCommand(EiscpCommand.NETUSB_OP_DOWN);
572 } else if ("Select".equals(cmdName)) {
573 sendCommand(EiscpCommand.NETUSB_OP_SELECT);
574 } else if ("PageUp".equals(cmdName)) {
575 sendCommand(EiscpCommand.NETUSB_OP_LEFT);
576 } else if ("PageDown".equals(cmdName)) {
577 sendCommand(EiscpCommand.NETUSB_OP_RIGHT);
578 } else if ("Back".equals(cmdName)) {
579 sendCommand(EiscpCommand.NETUSB_OP_RETURN);
580 } else if (cmdName.matches("Select[0-9]")) {
581 int pos = Integer.parseInt(cmdName.substring(6));
582 sendCommand(EiscpCommand.NETUSB_MENU_SELECT, new DecimalType(pos));
584 logger.debug("Received unknown menucommand {}", cmdName);
588 private void selectInput(int inputId) {
589 sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
590 currentInput = inputId;
593 @SuppressWarnings("unused")
594 private void onInputChanged(int newInput) {
595 currentInput = newInput;
597 if (newInput != NET_USB_ID) {
600 updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
601 updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
602 updateState(CHANNEL_TITLE, UnDefType.UNDEF);
603 updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
607 private void updateAlbumArt(String data) {
608 onkyoAlbumArt.addFrame(data);
610 if (onkyoAlbumArt.isAlbumCoverReady()) {
612 byte[] imgData = onkyoAlbumArt.getAlbumArt();
613 if (imgData != null && imgData.length > 0) {
614 String mimeType = onkyoAlbumArt.getAlbumArtMimeType();
615 if (mimeType.isEmpty()) {
616 mimeType = guessMimeTypeFromData(imgData);
618 updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
620 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
622 } catch (IllegalArgumentException e) {
623 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
625 onkyoAlbumArt.clearAlbumArt();
628 if (data.startsWith("2-")) {
629 updateState(CHANNEL_ALBUM_ART_URL, new StringType(data.substring(2, data.length())));
630 } else if (data.startsWith("n-")) {
631 updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
633 logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
634 updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
638 private void updateNetTitle(String data) {
639 // first 2 characters is service type
640 int type = Integer.parseInt(data.substring(0, 2), 16);
641 ServiceType service = ServiceType.getType(type);
644 if (data.length() > 21) {
645 title = data.substring(22, data.length());
648 updateState(CHANNEL_NET_MENU_TITLE,
649 new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
652 private void updateNetMenu(String data) {
653 switch (data.charAt(0)) {
655 String itemData = data.substring(3, data.length());
656 switch (data.charAt(1)) {
658 updateState(CHANNEL_NET_MENU0, new StringType(itemData));
661 updateState(CHANNEL_NET_MENU1, new StringType(itemData));
664 updateState(CHANNEL_NET_MENU2, new StringType(itemData));
667 updateState(CHANNEL_NET_MENU3, new StringType(itemData));
670 updateState(CHANNEL_NET_MENU4, new StringType(itemData));
673 updateState(CHANNEL_NET_MENU5, new StringType(itemData));
676 updateState(CHANNEL_NET_MENU6, new StringType(itemData));
679 updateState(CHANNEL_NET_MENU7, new StringType(itemData));
682 updateState(CHANNEL_NET_MENU8, new StringType(itemData));
685 updateState(CHANNEL_NET_MENU9, new StringType(itemData));
691 updateMenuPosition(data);
696 private void updateMenuPosition(String data) {
697 char position = data.charAt(1);
698 int pos = Character.getNumericValue(position);
700 logger.debug("Updating menu position to {}", pos);
703 updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
705 updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
708 if (data.endsWith("P")) {
713 private void resetNetMenu() {
714 logger.debug("Reset net menu");
715 updateState(CHANNEL_NET_MENU0, new StringType("-"));
716 updateState(CHANNEL_NET_MENU1, new StringType("-"));
717 updateState(CHANNEL_NET_MENU2, new StringType("-"));
718 updateState(CHANNEL_NET_MENU3, new StringType("-"));
719 updateState(CHANNEL_NET_MENU4, new StringType("-"));
720 updateState(CHANNEL_NET_MENU5, new StringType("-"));
721 updateState(CHANNEL_NET_MENU6, new StringType("-"));
722 updateState(CHANNEL_NET_MENU7, new StringType("-"));
723 updateState(CHANNEL_NET_MENU8, new StringType("-"));
724 updateState(CHANNEL_NET_MENU9, new StringType("-"));
727 private State convertNetUsbPlayStatus(String data) {
728 State state = UnDefType.UNDEF;
729 switch (data.charAt(0)) {
731 state = PlayPauseType.PLAY;
735 state = PlayPauseType.PAUSE;
738 state = RewindFastforwardType.FASTFORWARD;
741 state = RewindFastforwardType.REWIND;
748 public void sendRawCommand(String command, String value) {
749 if (connection != null) {
750 connection.send(command, value);
752 logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
756 private void sendCommand(EiscpCommand deviceCommand) {
757 if (connection != null) {
758 connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
760 logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
764 private void sendCommand(EiscpCommand deviceCommand, Command command) {
765 if (connection != null) {
766 final String cmd = deviceCommand.getCommand();
767 String valTemplate = deviceCommand.getValue();
770 if (command instanceof OnOffType) {
771 val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
773 } else if (command instanceof StringType) {
774 val = String.format(valTemplate, command);
776 } else if (command instanceof DecimalType) {
777 val = String.format(valTemplate, ((DecimalType) command).intValue());
779 } else if (command instanceof PercentType) {
780 val = String.format(valTemplate, ((DecimalType) command).intValue());
785 logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
786 connection.getConnectionName());
787 connection.send(cmd, val);
789 logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
794 * Check the status of the AVR.
798 private void checkStatus() {
799 sendCommand(EiscpCommand.POWER_QUERY);
801 if (connection != null && connection.isConnected()) {
802 sendCommand(EiscpCommand.VOLUME_QUERY);
803 sendCommand(EiscpCommand.SOURCE_QUERY);
804 sendCommand(EiscpCommand.MUTE_QUERY);
805 sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
806 sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
807 sendCommand(EiscpCommand.INFO_QUERY);
808 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
810 if (isChannelAvailable(CHANNEL_POWERZONE2)) {
811 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
812 sendCommand(EiscpCommand.ZONE2_VOLUME_QUERY);
813 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
814 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
817 if (isChannelAvailable(CHANNEL_POWERZONE3)) {
818 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
819 sendCommand(EiscpCommand.ZONE3_VOLUME_QUERY);
820 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
821 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
824 updateStatus(ThingStatus.OFFLINE);
828 private boolean isChannelAvailable(String channel) {
829 List<Channel> channels = getThing().getChannels();
830 for (Channel c : channels) {
831 if (c.getUID().getId().equals(channel)) {
838 private void handleVolumeSet(EiscpCommand.Zone zone, final State currentValue, final Command command) {
839 if (command instanceof PercentType) {
840 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_SET),
841 downScaleVolume((PercentType) command));
842 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
843 if (currentValue instanceof PercentType) {
844 if (((DecimalType) currentValue).intValue() < configuration.volumeLimit) {
845 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_UP));
847 logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
850 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
851 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_DOWN));
852 } else if (command.equals(OnOffType.OFF)) {
853 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
854 } else if (command.equals(OnOffType.ON)) {
855 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
856 } else if (command.equals(RefreshType.REFRESH)) {
857 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_QUERY));
858 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_QUERY));
862 private State handleReceivedVolume(State volume) {
863 if (volume instanceof DecimalType) {
864 return upScaleVolume(((DecimalType) volume));
869 private PercentType upScaleVolume(DecimalType volume) {
870 PercentType newVolume = scaleVolumeFromReceiver(volume);
872 if (configuration.volumeLimit < 100) {
873 double scaleCoefficient = 100d / configuration.volumeLimit;
874 PercentType unLimitedVolume = newVolume;
875 newVolume = new PercentType(((Double) (newVolume.doubleValue() * scaleCoefficient)).intValue());
876 logger.debug("Up scaled volume level '{}' to '{}'", unLimitedVolume, newVolume);
882 private DecimalType downScaleVolume(PercentType volume) {
883 PercentType limitedVolume = volume;
885 if (configuration.volumeLimit < 100) {
886 double scaleCoefficient = configuration.volumeLimit / 100d;
887 limitedVolume = new PercentType(((Double) (volume.doubleValue() * scaleCoefficient)).intValue());
888 logger.debug("Limited volume level '{}' to '{}'", volume, limitedVolume);
891 return scaleVolumeForReceiver(limitedVolume);
894 private DecimalType scaleVolumeForReceiver(PercentType volume) {
895 return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
898 private PercentType scaleVolumeFromReceiver(DecimalType volume) {
899 return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
903 public PercentType getVolume() throws IOException {
904 if (volumeLevelZone1 instanceof PercentType) {
905 return (PercentType) volumeLevelZone1;
908 throw new IOException();
912 public void setVolume(PercentType volume) throws IOException {
913 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
916 private String guessMimeTypeFromData(byte[] data) {
917 String mimeType = HttpUtil.guessContentTypeFromData(data);
918 logger.debug("Mime type guess from content: {}", mimeType);
919 if (mimeType == null) {
920 mimeType = RawType.DEFAULT_MIME_TYPE;
922 logger.debug("Mime type: {}", mimeType);
927 public Collection<Class<? extends ThingHandlerService>> getServices() {
928 return Collections.singletonList(OnkyoThingActions.class);