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.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.OnkyoParserHelper;
34 import org.openhab.binding.onkyo.internal.OnkyoStateDescriptionProvider;
35 import org.openhab.binding.onkyo.internal.ServiceType;
36 import org.openhab.binding.onkyo.internal.automation.modules.OnkyoThingActions;
37 import org.openhab.binding.onkyo.internal.config.OnkyoDeviceConfiguration;
38 import org.openhab.binding.onkyo.internal.eiscp.EiscpCommand;
39 import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
40 import org.openhab.core.audio.AudioHTTPServer;
41 import org.openhab.core.io.net.http.HttpUtil;
42 import org.openhab.core.io.transport.upnp.UpnpIOService;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.IncreaseDecreaseType;
45 import org.openhab.core.library.types.NextPreviousType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.library.types.PlayPauseType;
49 import org.openhab.core.library.types.RawType;
50 import org.openhab.core.library.types.RewindFastforwardType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.ThingHandlerService;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.StateOption;
62 import org.openhab.core.types.UnDefType;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65 import org.w3c.dom.Document;
66 import org.w3c.dom.Element;
67 import org.w3c.dom.NodeList;
68 import org.xml.sax.InputSource;
69 import org.xml.sax.SAXException;
72 * The {@link OnkyoHandler} is responsible for handling commands, which are
73 * sent to one of the channels.
75 * @author Paul Frank - Initial contribution
76 * @author Marcel Verpaalen - parsing additional commands
77 * @author Pauli Anttila - lot of refactoring
78 * @author Stewart Cossey - add dynamic state description provider
80 public class OnkyoHandler extends UpnpAudioSinkHandler implements OnkyoEventListener {
82 private final Logger logger = LoggerFactory.getLogger(OnkyoHandler.class);
84 private OnkyoDeviceConfiguration configuration;
86 private OnkyoConnection connection;
87 private ScheduledFuture<?> resourceUpdaterFuture;
88 @SuppressWarnings("unused")
89 private int currentInput = -1;
90 private State volumeLevelZone1 = UnDefType.UNDEF;
91 private State volumeLevelZone2 = UnDefType.UNDEF;
92 private State volumeLevelZone3 = UnDefType.UNDEF;
93 private State lastPowerState = OnOffType.OFF;
95 private final OnkyoStateDescriptionProvider stateDescriptionProvider;
97 private final OnkyoAlbumArt onkyoAlbumArt = new OnkyoAlbumArt();
99 private static final int NET_USB_ID = 43;
101 public OnkyoHandler(Thing thing, UpnpIOService upnpIOService, AudioHTTPServer audioHTTPServer, String callbackUrl,
102 OnkyoStateDescriptionProvider stateDescriptionProvider) {
103 super(thing, upnpIOService, audioHTTPServer, callbackUrl);
104 this.stateDescriptionProvider = stateDescriptionProvider;
108 * Initialize the state of the receiver.
111 public void initialize() {
112 logger.debug("Initializing handler for Onkyo Receiver");
113 configuration = getConfigAs(OnkyoDeviceConfiguration.class);
114 logger.info("Using configuration: {}", configuration.toString());
116 connection = new OnkyoConnection(configuration.ipAddress, configuration.port);
117 connection.addEventListener(this);
119 scheduler.execute(() -> {
120 logger.debug("Open connection to Onkyo Receiver @{}", connection.getConnectionName());
121 connection.openConnection();
122 if (connection.isConnected()) {
123 updateStatus(ThingStatus.ONLINE);
128 if (configuration.refreshInterval > 0) {
129 // Start resource refresh updater
130 resourceUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
132 logger.debug("Send resource update requests to Onkyo Receiver @{}", connection.getConnectionName());
134 } catch (LinkageError e) {
135 logger.warn("Failed to send resource update requests to Onkyo Receiver @{}. Cause: {}",
136 connection.getConnectionName(), e.getMessage());
137 } catch (Exception ex) {
138 logger.warn("Exception in resource refresh Thread Onkyo Receiver @{}. Cause: {}",
139 connection.getConnectionName(), ex.getMessage());
141 }, configuration.refreshInterval, configuration.refreshInterval, TimeUnit.SECONDS);
146 public void dispose() {
148 if (resourceUpdaterFuture != null) {
149 resourceUpdaterFuture.cancel(true);
151 if (connection != null) {
152 connection.removeEventListener(this);
153 connection.closeConnection();
158 public void handleCommand(ChannelUID channelUID, Command command) {
159 logger.debug("handleCommand for channel {}: {}", channelUID.getId(), command.toString());
160 switch (channelUID.getId()) {
166 if (command instanceof OnOffType) {
167 sendCommand(EiscpCommand.POWER_SET, command);
168 } else if (command.equals(RefreshType.REFRESH)) {
169 sendCommand(EiscpCommand.POWER_QUERY);
173 if (command instanceof OnOffType) {
174 sendCommand(EiscpCommand.MUTE_SET, command);
175 } else if (command.equals(RefreshType.REFRESH)) {
176 sendCommand(EiscpCommand.MUTE_QUERY);
180 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, command);
183 if (command instanceof DecimalType) {
184 selectInput(((DecimalType) command).intValue());
185 } else if (command.equals(RefreshType.REFRESH)) {
186 sendCommand(EiscpCommand.SOURCE_QUERY);
189 case CHANNEL_LISTENMODE:
190 if (command instanceof DecimalType) {
191 sendCommand(EiscpCommand.LISTEN_MODE_SET, command);
192 } else if (command.equals(RefreshType.REFRESH)) {
193 sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
201 case CHANNEL_POWERZONE2:
202 if (command instanceof OnOffType) {
203 sendCommand(EiscpCommand.ZONE2_POWER_SET, command);
204 } else if (command.equals(RefreshType.REFRESH)) {
205 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
208 case CHANNEL_MUTEZONE2:
209 if (command instanceof OnOffType) {
210 sendCommand(EiscpCommand.ZONE2_MUTE_SET, command);
211 } else if (command.equals(RefreshType.REFRESH)) {
212 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
215 case CHANNEL_VOLUMEZONE2:
216 handleVolumeSet(EiscpCommand.Zone.ZONE2, volumeLevelZone2, command);
218 case CHANNEL_INPUTZONE2:
219 if (command instanceof DecimalType) {
220 sendCommand(EiscpCommand.ZONE2_SOURCE_SET, command);
221 } else if (command.equals(RefreshType.REFRESH)) {
222 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
230 case CHANNEL_POWERZONE3:
231 if (command instanceof OnOffType) {
232 sendCommand(EiscpCommand.ZONE3_POWER_SET, command);
233 } else if (command.equals(RefreshType.REFRESH)) {
234 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
237 case CHANNEL_MUTEZONE3:
238 if (command instanceof OnOffType) {
239 sendCommand(EiscpCommand.ZONE3_MUTE_SET, command);
240 } else if (command.equals(RefreshType.REFRESH)) {
241 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
244 case CHANNEL_VOLUMEZONE3:
245 handleVolumeSet(EiscpCommand.Zone.ZONE3, volumeLevelZone3, command);
247 case CHANNEL_INPUTZONE3:
248 if (command instanceof DecimalType) {
249 sendCommand(EiscpCommand.ZONE3_SOURCE_SET, command);
250 } else if (command.equals(RefreshType.REFRESH)) {
251 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
259 case CHANNEL_CONTROL:
260 if (command instanceof PlayPauseType) {
261 if (command.equals(PlayPauseType.PLAY)) {
262 sendCommand(EiscpCommand.NETUSB_OP_PLAY);
263 } else if (command.equals(PlayPauseType.PAUSE)) {
264 sendCommand(EiscpCommand.NETUSB_OP_PAUSE);
266 } else if (command instanceof NextPreviousType) {
267 if (command.equals(NextPreviousType.NEXT)) {
268 sendCommand(EiscpCommand.NETUSB_OP_TRACKUP);
269 } else if (command.equals(NextPreviousType.PREVIOUS)) {
270 sendCommand(EiscpCommand.NETUSB_OP_TRACKDWN);
272 } else if (command instanceof RewindFastforwardType) {
273 if (command.equals(RewindFastforwardType.REWIND)) {
274 sendCommand(EiscpCommand.NETUSB_OP_REW);
275 } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
276 sendCommand(EiscpCommand.NETUSB_OP_FF);
278 } else if (command.equals(RefreshType.REFRESH)) {
279 sendCommand(EiscpCommand.NETUSB_PLAY_STATUS_QUERY);
282 case CHANNEL_PLAY_URI:
283 handlePlayUri(command);
285 case CHANNEL_ALBUM_ART:
286 case CHANNEL_ALBUM_ART_URL:
287 if (command.equals(RefreshType.REFRESH)) {
288 sendCommand(EiscpCommand.NETUSB_ALBUM_ART_QUERY);
292 if (command.equals(RefreshType.REFRESH)) {
293 sendCommand(EiscpCommand.NETUSB_SONG_ARTIST_QUERY);
297 if (command.equals(RefreshType.REFRESH)) {
298 sendCommand(EiscpCommand.NETUSB_SONG_ALBUM_QUERY);
302 if (command.equals(RefreshType.REFRESH)) {
303 sendCommand(EiscpCommand.NETUSB_SONG_TITLE_QUERY);
306 case CHANNEL_CURRENTPLAYINGTIME:
307 if (command.equals(RefreshType.REFRESH)) {
308 sendCommand(EiscpCommand.NETUSB_SONG_ELAPSEDTIME_QUERY);
316 case CHANNEL_NET_MENU_CONTROL:
317 if (command instanceof StringType) {
318 final String cmdName = command.toString();
319 handleNetMenuCommand(cmdName);
322 case CHANNEL_NET_MENU_TITLE:
323 if (command.equals(RefreshType.REFRESH)) {
324 sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
327 case CHANNEL_AUDIOINFO:
328 if (command.equals(RefreshType.REFRESH)) {
329 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
336 case CHANNEL_AUDIO_IN_INFO:
337 case CHANNEL_AUDIO_OUT_INFO:
338 if (command.equals(RefreshType.REFRESH)) {
339 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
342 case CHANNEL_VIDEO_IN_INFO:
343 case CHANNEL_VIDEO_OUT_INFO:
344 if (command.equals(RefreshType.REFRESH)) {
345 sendCommand(EiscpCommand.VIDEOINFO_QUERY);
353 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
358 private void populateInputs(NodeList selectorlist) {
359 List<StateOption> options = new ArrayList<>();
361 for (int i = 0; i < selectorlist.getLength(); i++) {
362 Element selectorItem = (Element) selectorlist.item(i);
364 options.add(new StateOption(String.valueOf(Integer.parseInt(selectorItem.getAttribute("id"), 16)),
365 selectorItem.getAttribute("name")));
367 logger.debug("Got Input List from Receiver {}", options);
369 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUT), options);
370 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE2), options);
371 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE3), options);
374 private void doPowerOnCheck(State state) {
375 if (configuration.refreshInterval == 0 && lastPowerState == OnOffType.OFF && state == OnOffType.ON) {
376 sendCommand(EiscpCommand.INFO_QUERY);
378 lastPowerState = state;
382 public void statusUpdateReceived(String ip, EiscpMessage data) {
383 logger.debug("Received status update from Onkyo Receiver @{}: data={}", connection.getConnectionName(), data);
385 updateStatus(ThingStatus.ONLINE);
388 EiscpCommand receivedCommand = null;
391 receivedCommand = EiscpCommand.getCommandByCommandAndValueStr(data.getCommand(), "");
392 } catch (IllegalArgumentException ex) {
393 logger.debug("Received unknown status update from Onkyo Receiver @{}: data={}",
394 connection.getConnectionName(), data);
398 logger.debug("Received command {}", receivedCommand);
400 switch (receivedCommand) {
405 State powerState = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
406 updateState(CHANNEL_POWER, powerState);
407 doPowerOnCheck(powerState);
410 updateState(CHANNEL_MUTE, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
413 volumeLevelZone1 = handleReceivedVolume(
414 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
415 updateState(CHANNEL_VOLUME, volumeLevelZone1);
418 updateState(CHANNEL_INPUT, convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
421 updateState(CHANNEL_LISTENMODE,
422 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
429 State powerZone2State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
430 updateState(CHANNEL_POWERZONE2, powerZone2State);
431 doPowerOnCheck(powerZone2State);
434 updateState(CHANNEL_MUTEZONE2, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
437 volumeLevelZone2 = handleReceivedVolume(
438 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
439 updateState(CHANNEL_VOLUMEZONE2, volumeLevelZone2);
442 updateState(CHANNEL_INPUTZONE2,
443 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
450 State powerZone3State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
451 updateState(CHANNEL_POWERZONE3, powerZone3State);
452 doPowerOnCheck(powerZone3State);
455 updateState(CHANNEL_MUTEZONE3, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
458 volumeLevelZone3 = handleReceivedVolume(
459 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
460 updateState(CHANNEL_VOLUMEZONE3, volumeLevelZone3);
463 updateState(CHANNEL_INPUTZONE3,
464 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
471 case NETUSB_SONG_ARTIST:
472 updateState(CHANNEL_ARTIST, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
474 case NETUSB_SONG_ALBUM:
475 updateState(CHANNEL_ALBUM, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
477 case NETUSB_SONG_TITLE:
478 updateState(CHANNEL_TITLE, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
480 case NETUSB_SONG_ELAPSEDTIME:
481 updateState(CHANNEL_CURRENTPLAYINGTIME,
482 convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
484 case NETUSB_PLAY_STATUS:
485 updateState(CHANNEL_CONTROL, convertNetUsbPlayStatus(data.getValue()));
487 case NETUSB_ALBUM_ART:
488 updateAlbumArt(data.getValue());
491 updateNetTitle(data.getValue());
494 updateNetMenu(data.getValue());
501 updateState(CHANNEL_AUDIOINFO, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
502 logger.debug("audioinfo message: '{}'", data.getValue());
503 updateState(CHANNEL_AUDIO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 2));
504 updateState(CHANNEL_AUDIO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 3, 5));
507 updateState(CHANNEL_VIDEO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 3));
508 updateState(CHANNEL_VIDEO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 4, 7));
511 processInfo(data.getValue());
512 logger.debug("Info message: '{}'", data.getValue());
516 logger.debug("Received unhandled status update from Onkyo Receiver @{}: data={}",
517 connection.getConnectionName(), data);
521 } catch (Exception ex) {
522 logger.warn("Exception in statusUpdateReceived for Onkyo Receiver @{}. Cause: {}, data received: {}",
523 connection.getConnectionName(), ex.getMessage(), data);
527 private void processInfo(String infoXML) {
529 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
530 // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
531 factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
532 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
533 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
534 factory.setXIncludeAware(false);
535 factory.setExpandEntityReferences(false);
536 DocumentBuilder builder = factory.newDocumentBuilder();
537 try (StringReader sr = new StringReader(infoXML)) {
538 InputSource is = new InputSource(sr);
539 Document doc = builder.parse(is);
541 NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
542 populateInputs(selectableInputs);
544 } catch (ParserConfigurationException | SAXException | IOException e) {
545 logger.debug("Error occured during Info XML parsing.", e);
550 public void connectionError(String ip, String errorMsg) {
551 logger.debug("Connection error occurred to Onkyo Receiver @{}", ip);
552 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
555 private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
556 State state = UnDefType.UNDEF;
561 if (data.contentEquals("N/A")) {
562 state = UnDefType.UNDEF;
564 } else if (classToConvert == OnOffType.class) {
565 index = Integer.parseInt(data, 16);
566 state = index == 0 ? OnOffType.OFF : OnOffType.ON;
568 } else if (classToConvert == DecimalType.class) {
569 index = Integer.parseInt(data, 16);
570 state = new DecimalType(index);
572 } else if (classToConvert == PercentType.class) {
573 index = Integer.parseInt(data, 16);
574 state = new PercentType(index);
576 } else if (classToConvert == StringType.class) {
577 state = new StringType(data);
580 } catch (Exception e) {
581 logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
584 logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
588 private void handleNetMenuCommand(String cmdName) {
589 if ("Up".equals(cmdName)) {
590 sendCommand(EiscpCommand.NETUSB_OP_UP);
591 } else if ("Down".equals(cmdName)) {
592 sendCommand(EiscpCommand.NETUSB_OP_DOWN);
593 } else if ("Select".equals(cmdName)) {
594 sendCommand(EiscpCommand.NETUSB_OP_SELECT);
595 } else if ("PageUp".equals(cmdName)) {
596 sendCommand(EiscpCommand.NETUSB_OP_LEFT);
597 } else if ("PageDown".equals(cmdName)) {
598 sendCommand(EiscpCommand.NETUSB_OP_RIGHT);
599 } else if ("Back".equals(cmdName)) {
600 sendCommand(EiscpCommand.NETUSB_OP_RETURN);
601 } else if (cmdName.matches("Select[0-9]")) {
602 int pos = Integer.parseInt(cmdName.substring(6));
603 sendCommand(EiscpCommand.NETUSB_MENU_SELECT, new DecimalType(pos));
605 logger.debug("Received unknown menucommand {}", cmdName);
609 private void selectInput(int inputId) {
610 sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
611 currentInput = inputId;
614 @SuppressWarnings("unused")
615 private void onInputChanged(int newInput) {
616 currentInput = newInput;
618 if (newInput != NET_USB_ID) {
621 updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
622 updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
623 updateState(CHANNEL_TITLE, UnDefType.UNDEF);
624 updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
628 private void updateAlbumArt(String data) {
629 onkyoAlbumArt.addFrame(data);
631 if (onkyoAlbumArt.isAlbumCoverReady()) {
633 byte[] imgData = onkyoAlbumArt.getAlbumArt();
634 if (imgData != null && imgData.length > 0) {
635 String mimeType = onkyoAlbumArt.getAlbumArtMimeType();
636 if (mimeType.isEmpty()) {
637 mimeType = guessMimeTypeFromData(imgData);
639 updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
641 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
643 } catch (IllegalArgumentException e) {
644 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
646 onkyoAlbumArt.clearAlbumArt();
649 if (data.startsWith("2-")) {
650 updateState(CHANNEL_ALBUM_ART_URL, new StringType(data.substring(2, data.length())));
651 } else if (data.startsWith("n-")) {
652 updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
654 logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
655 updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
659 private void updateNetTitle(String data) {
660 // first 2 characters is service type
661 int type = Integer.parseInt(data.substring(0, 2), 16);
662 ServiceType service = ServiceType.getType(type);
665 if (data.length() > 21) {
666 title = data.substring(22, data.length());
669 updateState(CHANNEL_NET_MENU_TITLE,
670 new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
673 private void updateNetMenu(String data) {
674 switch (data.charAt(0)) {
676 String itemData = data.substring(3, data.length());
677 switch (data.charAt(1)) {
679 updateState(CHANNEL_NET_MENU0, new StringType(itemData));
682 updateState(CHANNEL_NET_MENU1, new StringType(itemData));
685 updateState(CHANNEL_NET_MENU2, new StringType(itemData));
688 updateState(CHANNEL_NET_MENU3, new StringType(itemData));
691 updateState(CHANNEL_NET_MENU4, new StringType(itemData));
694 updateState(CHANNEL_NET_MENU5, new StringType(itemData));
697 updateState(CHANNEL_NET_MENU6, new StringType(itemData));
700 updateState(CHANNEL_NET_MENU7, new StringType(itemData));
703 updateState(CHANNEL_NET_MENU8, new StringType(itemData));
706 updateState(CHANNEL_NET_MENU9, new StringType(itemData));
712 updateMenuPosition(data);
717 private void updateMenuPosition(String data) {
718 char position = data.charAt(1);
719 int pos = Character.getNumericValue(position);
721 logger.debug("Updating menu position to {}", pos);
724 updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
726 updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
729 if (data.endsWith("P")) {
734 private void resetNetMenu() {
735 logger.debug("Reset net menu");
736 updateState(CHANNEL_NET_MENU0, new StringType("-"));
737 updateState(CHANNEL_NET_MENU1, new StringType("-"));
738 updateState(CHANNEL_NET_MENU2, new StringType("-"));
739 updateState(CHANNEL_NET_MENU3, new StringType("-"));
740 updateState(CHANNEL_NET_MENU4, new StringType("-"));
741 updateState(CHANNEL_NET_MENU5, new StringType("-"));
742 updateState(CHANNEL_NET_MENU6, new StringType("-"));
743 updateState(CHANNEL_NET_MENU7, new StringType("-"));
744 updateState(CHANNEL_NET_MENU8, new StringType("-"));
745 updateState(CHANNEL_NET_MENU9, new StringType("-"));
748 private State convertNetUsbPlayStatus(String data) {
749 State state = UnDefType.UNDEF;
750 switch (data.charAt(0)) {
752 state = PlayPauseType.PLAY;
756 state = PlayPauseType.PAUSE;
759 state = RewindFastforwardType.FASTFORWARD;
762 state = RewindFastforwardType.REWIND;
769 public void sendRawCommand(String command, String value) {
770 if (connection != null) {
771 connection.send(command, value);
773 logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
777 private void sendCommand(EiscpCommand deviceCommand) {
778 if (connection != null) {
779 connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
781 logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
785 private void sendCommand(EiscpCommand deviceCommand, Command command) {
786 if (connection != null) {
787 final String cmd = deviceCommand.getCommand();
788 String valTemplate = deviceCommand.getValue();
791 if (command instanceof OnOffType) {
792 val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
794 } else if (command instanceof StringType) {
795 val = String.format(valTemplate, command);
797 } else if (command instanceof DecimalType) {
798 val = String.format(valTemplate, ((DecimalType) command).intValue());
800 } else if (command instanceof PercentType) {
801 val = String.format(valTemplate, ((DecimalType) command).intValue());
806 logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
807 connection.getConnectionName());
808 connection.send(cmd, val);
810 logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
815 * Check the status of the AVR.
819 private void checkStatus() {
820 sendCommand(EiscpCommand.POWER_QUERY);
822 if (connection != null && connection.isConnected()) {
823 sendCommand(EiscpCommand.VOLUME_QUERY);
824 sendCommand(EiscpCommand.SOURCE_QUERY);
825 sendCommand(EiscpCommand.MUTE_QUERY);
826 sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
827 sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
828 sendCommand(EiscpCommand.INFO_QUERY);
829 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
830 sendCommand(EiscpCommand.VIDEOINFO_QUERY);
832 if (isChannelAvailable(CHANNEL_POWERZONE2)) {
833 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
834 sendCommand(EiscpCommand.ZONE2_VOLUME_QUERY);
835 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
836 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
839 if (isChannelAvailable(CHANNEL_POWERZONE3)) {
840 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
841 sendCommand(EiscpCommand.ZONE3_VOLUME_QUERY);
842 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
843 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
846 updateStatus(ThingStatus.OFFLINE);
850 private boolean isChannelAvailable(String channel) {
851 List<Channel> channels = getThing().getChannels();
852 for (Channel c : channels) {
853 if (c.getUID().getId().equals(channel)) {
860 private void handleVolumeSet(EiscpCommand.Zone zone, final State currentValue, final Command command) {
861 if (command instanceof PercentType) {
862 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_SET),
863 downScaleVolume((PercentType) command));
864 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
865 if (currentValue instanceof PercentType) {
866 if (((DecimalType) currentValue).intValue() < configuration.volumeLimit) {
867 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_UP));
869 logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
872 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
873 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_DOWN));
874 } else if (command.equals(OnOffType.OFF)) {
875 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
876 } else if (command.equals(OnOffType.ON)) {
877 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
878 } else if (command.equals(RefreshType.REFRESH)) {
879 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_QUERY));
880 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_QUERY));
884 private State handleReceivedVolume(State volume) {
885 if (volume instanceof DecimalType) {
886 return upScaleVolume(((DecimalType) volume));
891 private PercentType upScaleVolume(DecimalType volume) {
892 PercentType newVolume = scaleVolumeFromReceiver(volume);
894 if (configuration.volumeLimit < 100) {
895 double scaleCoefficient = 100d / configuration.volumeLimit;
896 PercentType unLimitedVolume = newVolume;
897 newVolume = new PercentType(((Double) (newVolume.doubleValue() * scaleCoefficient)).intValue());
898 logger.debug("Up scaled volume level '{}' to '{}'", unLimitedVolume, newVolume);
904 private DecimalType downScaleVolume(PercentType volume) {
905 PercentType limitedVolume = volume;
907 if (configuration.volumeLimit < 100) {
908 double scaleCoefficient = configuration.volumeLimit / 100d;
909 limitedVolume = new PercentType(((Double) (volume.doubleValue() * scaleCoefficient)).intValue());
910 logger.debug("Limited volume level '{}' to '{}'", volume, limitedVolume);
913 return scaleVolumeForReceiver(limitedVolume);
916 private DecimalType scaleVolumeForReceiver(PercentType volume) {
917 return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
920 private PercentType scaleVolumeFromReceiver(DecimalType volume) {
921 return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
925 public PercentType getVolume() throws IOException {
926 if (volumeLevelZone1 instanceof PercentType) {
927 return (PercentType) volumeLevelZone1;
930 throw new IOException();
934 public void setVolume(PercentType volume) throws IOException {
935 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
938 private String guessMimeTypeFromData(byte[] data) {
939 String mimeType = HttpUtil.guessContentTypeFromData(data);
940 logger.debug("Mime type guess from content: {}", mimeType);
941 if (mimeType == null) {
942 mimeType = RawType.DEFAULT_MIME_TYPE;
944 logger.debug("Mime type: {}", mimeType);
949 public Collection<Class<? extends ThingHandlerService>> getServices() {
950 return Collections.singletonList(OnkyoThingActions.class);