2 * Copyright (c) 2010-2024 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.List;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import javax.xml.parsers.DocumentBuilder;
26 import javax.xml.parsers.DocumentBuilderFactory;
27 import javax.xml.parsers.ParserConfigurationException;
29 import org.openhab.binding.onkyo.internal.OnkyoAlbumArt;
30 import org.openhab.binding.onkyo.internal.OnkyoConnection;
31 import org.openhab.binding.onkyo.internal.OnkyoEventListener;
32 import org.openhab.binding.onkyo.internal.OnkyoParserHelper;
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.io.net.http.HttpUtil;
40 import org.openhab.core.io.transport.upnp.UpnpIOService;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.IncreaseDecreaseType;
43 import org.openhab.core.library.types.NextPreviousType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.PercentType;
46 import org.openhab.core.library.types.PlayPauseType;
47 import org.openhab.core.library.types.RawType;
48 import org.openhab.core.library.types.RewindFastforwardType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.ThingHandlerService;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.StateOption;
60 import org.openhab.core.types.UnDefType;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63 import org.w3c.dom.Document;
64 import org.w3c.dom.Element;
65 import org.w3c.dom.NodeList;
66 import org.xml.sax.InputSource;
67 import org.xml.sax.SAXException;
70 * The {@link OnkyoHandler} is responsible for handling commands, which are
71 * sent to one of the channels.
73 * @author Paul Frank - Initial contribution
74 * @author Marcel Verpaalen - parsing additional commands
75 * @author Pauli Anttila - lot of refactoring
76 * @author Stewart Cossey - add dynamic state description provider
78 public class OnkyoHandler extends OnkyoUpnpHandler implements OnkyoEventListener {
80 private final Logger logger = LoggerFactory.getLogger(OnkyoHandler.class);
82 private OnkyoDeviceConfiguration configuration;
84 private OnkyoConnection connection;
85 private ScheduledFuture<?> resourceUpdaterFuture;
86 @SuppressWarnings("unused")
87 private int currentInput = -1;
88 private State volumeLevelZone1 = UnDefType.UNDEF;
89 private State volumeLevelZone2 = UnDefType.UNDEF;
90 private State volumeLevelZone3 = UnDefType.UNDEF;
91 private State lastPowerState = OnOffType.OFF;
93 private final OnkyoStateDescriptionProvider stateDescriptionProvider;
95 private final OnkyoAlbumArt onkyoAlbumArt = new OnkyoAlbumArt();
97 private static final int NET_USB_ID = 43;
99 public OnkyoHandler(Thing thing, UpnpIOService upnpIOService,
100 OnkyoStateDescriptionProvider stateDescriptionProvider) {
101 super(thing, upnpIOService);
102 this.stateDescriptionProvider = stateDescriptionProvider;
106 * Initialize the state of the receiver.
109 public void initialize() {
110 logger.debug("Initializing handler for Onkyo Receiver");
111 configuration = getConfigAs(OnkyoDeviceConfiguration.class);
112 logger.info("Using configuration: {}", configuration.toString());
114 connection = new OnkyoConnection(configuration.ipAddress, configuration.port);
115 connection.addEventListener(this);
117 scheduler.execute(() -> {
118 logger.debug("Open connection to Onkyo Receiver @{}", connection.getConnectionName());
119 connection.openConnection();
120 if (connection.isConnected()) {
121 updateStatus(ThingStatus.ONLINE);
126 if (configuration.refreshInterval > 0) {
127 // Start resource refresh updater
128 resourceUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
130 logger.debug("Send resource update requests to Onkyo Receiver @{}", connection.getConnectionName());
132 } catch (LinkageError e) {
133 logger.warn("Failed to send resource update requests to Onkyo Receiver @{}. Cause: {}",
134 connection.getConnectionName(), e.getMessage());
135 } catch (Exception ex) {
136 logger.warn("Exception in resource refresh Thread Onkyo Receiver @{}. Cause: {}",
137 connection.getConnectionName(), ex.getMessage());
139 }, configuration.refreshInterval, configuration.refreshInterval, TimeUnit.SECONDS);
144 public void dispose() {
146 if (resourceUpdaterFuture != null) {
147 resourceUpdaterFuture.cancel(true);
149 if (connection != null) {
150 connection.removeEventListener(this);
151 connection.closeConnection();
156 public void handleCommand(ChannelUID channelUID, Command command) {
157 logger.debug("handleCommand for channel {}: {}", channelUID.getId(), command.toString());
158 switch (channelUID.getId()) {
164 if (command instanceof OnOffType) {
165 sendCommand(EiscpCommand.POWER_SET, command);
166 } else if (command.equals(RefreshType.REFRESH)) {
167 sendCommand(EiscpCommand.POWER_QUERY);
171 if (command instanceof OnOffType) {
172 sendCommand(EiscpCommand.MUTE_SET, command);
173 } else if (command.equals(RefreshType.REFRESH)) {
174 sendCommand(EiscpCommand.MUTE_QUERY);
178 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, command);
181 if (command instanceof DecimalType decimalCommand) {
182 selectInput(decimalCommand.intValue());
183 } else if (command.equals(RefreshType.REFRESH)) {
184 sendCommand(EiscpCommand.SOURCE_QUERY);
187 case CHANNEL_LISTENMODE:
188 if (command instanceof DecimalType) {
189 sendCommand(EiscpCommand.LISTEN_MODE_SET, command);
190 } else if (command.equals(RefreshType.REFRESH)) {
191 sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
199 case CHANNEL_POWERZONE2:
200 if (command instanceof OnOffType) {
201 sendCommand(EiscpCommand.ZONE2_POWER_SET, command);
202 } else if (command.equals(RefreshType.REFRESH)) {
203 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
206 case CHANNEL_MUTEZONE2:
207 if (command instanceof OnOffType) {
208 sendCommand(EiscpCommand.ZONE2_MUTE_SET, command);
209 } else if (command.equals(RefreshType.REFRESH)) {
210 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
213 case CHANNEL_VOLUMEZONE2:
214 handleVolumeSet(EiscpCommand.Zone.ZONE2, volumeLevelZone2, command);
216 case CHANNEL_INPUTZONE2:
217 if (command instanceof DecimalType) {
218 sendCommand(EiscpCommand.ZONE2_SOURCE_SET, command);
219 } else if (command.equals(RefreshType.REFRESH)) {
220 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
228 case CHANNEL_POWERZONE3:
229 if (command instanceof OnOffType) {
230 sendCommand(EiscpCommand.ZONE3_POWER_SET, command);
231 } else if (command.equals(RefreshType.REFRESH)) {
232 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
235 case CHANNEL_MUTEZONE3:
236 if (command instanceof OnOffType) {
237 sendCommand(EiscpCommand.ZONE3_MUTE_SET, command);
238 } else if (command.equals(RefreshType.REFRESH)) {
239 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
242 case CHANNEL_VOLUMEZONE3:
243 handleVolumeSet(EiscpCommand.Zone.ZONE3, volumeLevelZone3, command);
245 case CHANNEL_INPUTZONE3:
246 if (command instanceof DecimalType) {
247 sendCommand(EiscpCommand.ZONE3_SOURCE_SET, command);
248 } else if (command.equals(RefreshType.REFRESH)) {
249 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
257 case CHANNEL_CONTROL:
258 if (command instanceof PlayPauseType) {
259 if (command.equals(PlayPauseType.PLAY)) {
260 sendCommand(EiscpCommand.NETUSB_OP_PLAY);
261 } else if (command.equals(PlayPauseType.PAUSE)) {
262 sendCommand(EiscpCommand.NETUSB_OP_PAUSE);
264 } else if (command instanceof NextPreviousType) {
265 if (command.equals(NextPreviousType.NEXT)) {
266 sendCommand(EiscpCommand.NETUSB_OP_TRACKUP);
267 } else if (command.equals(NextPreviousType.PREVIOUS)) {
268 sendCommand(EiscpCommand.NETUSB_OP_TRACKDWN);
270 } else if (command instanceof RewindFastforwardType) {
271 if (command.equals(RewindFastforwardType.REWIND)) {
272 sendCommand(EiscpCommand.NETUSB_OP_REW);
273 } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
274 sendCommand(EiscpCommand.NETUSB_OP_FF);
276 } else if (command.equals(RefreshType.REFRESH)) {
277 sendCommand(EiscpCommand.NETUSB_PLAY_STATUS_QUERY);
280 case CHANNEL_PLAY_URI:
281 handlePlayUri(command);
283 case CHANNEL_ALBUM_ART:
284 case CHANNEL_ALBUM_ART_URL:
285 if (command.equals(RefreshType.REFRESH)) {
286 sendCommand(EiscpCommand.NETUSB_ALBUM_ART_QUERY);
290 if (command.equals(RefreshType.REFRESH)) {
291 sendCommand(EiscpCommand.NETUSB_SONG_ARTIST_QUERY);
295 if (command.equals(RefreshType.REFRESH)) {
296 sendCommand(EiscpCommand.NETUSB_SONG_ALBUM_QUERY);
300 if (command.equals(RefreshType.REFRESH)) {
301 sendCommand(EiscpCommand.NETUSB_SONG_TITLE_QUERY);
304 case CHANNEL_CURRENTPLAYINGTIME:
305 if (command.equals(RefreshType.REFRESH)) {
306 sendCommand(EiscpCommand.NETUSB_SONG_ELAPSEDTIME_QUERY);
314 case CHANNEL_NET_MENU_CONTROL:
315 if (command instanceof StringType) {
316 final String cmdName = command.toString();
317 handleNetMenuCommand(cmdName);
320 case CHANNEL_NET_MENU_TITLE:
321 if (command.equals(RefreshType.REFRESH)) {
322 sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
325 case CHANNEL_AUDIOINFO:
326 if (command.equals(RefreshType.REFRESH)) {
327 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
334 case CHANNEL_AUDIO_IN_INFO:
335 case CHANNEL_AUDIO_OUT_INFO:
336 if (command.equals(RefreshType.REFRESH)) {
337 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
340 case CHANNEL_VIDEO_IN_INFO:
341 case CHANNEL_VIDEO_OUT_INFO:
342 if (command.equals(RefreshType.REFRESH)) {
343 sendCommand(EiscpCommand.VIDEOINFO_QUERY);
351 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
356 private void populateInputs(NodeList selectorlist) {
357 List<StateOption> options = new ArrayList<>();
359 for (int i = 0; i < selectorlist.getLength(); i++) {
360 Element selectorItem = (Element) selectorlist.item(i);
362 options.add(new StateOption(String.valueOf(Integer.parseInt(selectorItem.getAttribute("id"), 16)),
363 selectorItem.getAttribute("name")));
365 logger.debug("Got Input List from Receiver {}", options);
367 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUT), options);
368 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE2), options);
369 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE3), options);
372 private void doPowerOnCheck(State state) {
373 if (configuration.refreshInterval == 0 && lastPowerState == OnOffType.OFF && state == OnOffType.ON) {
374 sendCommand(EiscpCommand.INFO_QUERY);
376 lastPowerState = state;
380 public void statusUpdateReceived(String ip, EiscpMessage data) {
381 logger.debug("Received status update from Onkyo Receiver @{}: data={}", connection.getConnectionName(), data);
383 updateStatus(ThingStatus.ONLINE);
386 EiscpCommand receivedCommand = null;
389 receivedCommand = EiscpCommand.getCommandByCommandAndValueStr(data.getCommand(), "");
390 } catch (IllegalArgumentException ex) {
391 logger.debug("Received unknown status update from Onkyo Receiver @{}: data={}",
392 connection.getConnectionName(), data);
396 logger.debug("Received command {}", receivedCommand);
398 switch (receivedCommand) {
403 State powerState = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
404 updateState(CHANNEL_POWER, powerState);
405 doPowerOnCheck(powerState);
408 updateState(CHANNEL_MUTE, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
411 volumeLevelZone1 = handleReceivedVolume(
412 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
413 updateState(CHANNEL_VOLUME, volumeLevelZone1);
416 updateState(CHANNEL_INPUT, convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
419 updateState(CHANNEL_LISTENMODE,
420 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
427 State powerZone2State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
428 updateState(CHANNEL_POWERZONE2, powerZone2State);
429 doPowerOnCheck(powerZone2State);
432 updateState(CHANNEL_MUTEZONE2, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
435 volumeLevelZone2 = handleReceivedVolume(
436 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
437 updateState(CHANNEL_VOLUMEZONE2, volumeLevelZone2);
440 updateState(CHANNEL_INPUTZONE2,
441 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
448 State powerZone3State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
449 updateState(CHANNEL_POWERZONE3, powerZone3State);
450 doPowerOnCheck(powerZone3State);
453 updateState(CHANNEL_MUTEZONE3, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
456 volumeLevelZone3 = handleReceivedVolume(
457 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
458 updateState(CHANNEL_VOLUMEZONE3, volumeLevelZone3);
461 updateState(CHANNEL_INPUTZONE3,
462 convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
469 case NETUSB_SONG_ARTIST:
470 updateState(CHANNEL_ARTIST, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
472 case NETUSB_SONG_ALBUM:
473 updateState(CHANNEL_ALBUM, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
475 case NETUSB_SONG_TITLE:
476 updateState(CHANNEL_TITLE, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
478 case NETUSB_SONG_ELAPSEDTIME:
479 updateState(CHANNEL_CURRENTPLAYINGTIME,
480 convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
482 case NETUSB_PLAY_STATUS:
483 updateState(CHANNEL_CONTROL, convertNetUsbPlayStatus(data.getValue()));
485 case NETUSB_ALBUM_ART:
486 updateAlbumArt(data.getValue());
489 updateNetTitle(data.getValue());
492 updateNetMenu(data.getValue());
499 updateState(CHANNEL_AUDIOINFO, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
500 logger.debug("audioinfo message: '{}'", data.getValue());
501 updateState(CHANNEL_AUDIO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 2));
502 updateState(CHANNEL_AUDIO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 3, 5));
505 updateState(CHANNEL_VIDEO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 3));
506 updateState(CHANNEL_VIDEO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 4, 7));
509 processInfo(data.getValue());
510 logger.debug("Info message: '{}'", data.getValue());
514 logger.debug("Received unhandled status update from Onkyo Receiver @{}: data={}",
515 connection.getConnectionName(), data);
519 } catch (Exception ex) {
520 logger.warn("Exception in statusUpdateReceived for Onkyo Receiver @{}. Cause: {}, data received: {}",
521 connection.getConnectionName(), ex.getMessage(), data);
525 private void processInfo(String infoXML) {
527 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
528 // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
529 factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
530 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
531 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
532 factory.setXIncludeAware(false);
533 factory.setExpandEntityReferences(false);
534 DocumentBuilder builder = factory.newDocumentBuilder();
535 try (StringReader sr = new StringReader(infoXML)) {
536 InputSource is = new InputSource(sr);
537 Document doc = builder.parse(is);
539 NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
540 populateInputs(selectableInputs);
542 } catch (ParserConfigurationException | SAXException | IOException e) {
543 logger.debug("Error occured during Info XML parsing.", e);
548 public void connectionError(String ip, String errorMsg) {
549 logger.debug("Connection error occurred to Onkyo Receiver @{}", ip);
550 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
553 private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
554 State state = UnDefType.UNDEF;
559 if (data.contentEquals("N/A")) {
560 state = UnDefType.UNDEF;
562 } else if (classToConvert == OnOffType.class) {
563 index = Integer.parseInt(data, 16);
564 state = OnOffType.from(index != 0);
566 } else if (classToConvert == DecimalType.class) {
567 index = Integer.parseInt(data, 16);
568 state = new DecimalType(index);
570 } else if (classToConvert == PercentType.class) {
571 index = Integer.parseInt(data, 16);
572 state = new PercentType(index);
574 } else if (classToConvert == StringType.class) {
575 state = new StringType(data);
578 } catch (Exception e) {
579 logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
582 logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
586 private void handleNetMenuCommand(String cmdName) {
587 if ("Up".equals(cmdName)) {
588 sendCommand(EiscpCommand.NETUSB_OP_UP);
589 } else if ("Down".equals(cmdName)) {
590 sendCommand(EiscpCommand.NETUSB_OP_DOWN);
591 } else if ("Select".equals(cmdName)) {
592 sendCommand(EiscpCommand.NETUSB_OP_SELECT);
593 } else if ("PageUp".equals(cmdName)) {
594 sendCommand(EiscpCommand.NETUSB_OP_LEFT);
595 } else if ("PageDown".equals(cmdName)) {
596 sendCommand(EiscpCommand.NETUSB_OP_RIGHT);
597 } else if ("Back".equals(cmdName)) {
598 sendCommand(EiscpCommand.NETUSB_OP_RETURN);
599 } else if (cmdName.matches("Select[0-9]")) {
600 int pos = Integer.parseInt(cmdName.substring(6));
601 sendCommand(EiscpCommand.NETUSB_MENU_SELECT, new DecimalType(pos));
603 logger.debug("Received unknown menucommand {}", cmdName);
607 private void selectInput(int inputId) {
608 sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
609 currentInput = inputId;
612 @SuppressWarnings("unused")
613 private void onInputChanged(int newInput) {
614 currentInput = newInput;
616 if (newInput != NET_USB_ID) {
619 updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
620 updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
621 updateState(CHANNEL_TITLE, UnDefType.UNDEF);
622 updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
626 private void updateAlbumArt(String data) {
627 onkyoAlbumArt.addFrame(data);
629 if (onkyoAlbumArt.isAlbumCoverReady()) {
631 byte[] imgData = onkyoAlbumArt.getAlbumArt();
632 if (imgData != null && imgData.length > 0) {
633 String mimeType = onkyoAlbumArt.getAlbumArtMimeType();
634 if (mimeType.isEmpty()) {
635 mimeType = guessMimeTypeFromData(imgData);
637 updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
639 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
641 } catch (IllegalArgumentException e) {
642 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
644 onkyoAlbumArt.clearAlbumArt();
647 if (data.startsWith("2-")) {
648 updateState(CHANNEL_ALBUM_ART_URL, new StringType(data.substring(2, data.length())));
649 } else if (data.startsWith("n-")) {
650 updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
652 logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
653 updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
657 private void updateNetTitle(String data) {
658 // first 2 characters is service type
659 int type = Integer.parseInt(data.substring(0, 2), 16);
660 ServiceType service = ServiceType.getType(type);
663 if (data.length() > 21) {
664 title = data.substring(22, data.length());
667 updateState(CHANNEL_NET_MENU_TITLE,
668 new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
671 private void updateNetMenu(String data) {
672 switch (data.charAt(0)) {
674 String itemData = data.substring(3, data.length());
675 switch (data.charAt(1)) {
677 updateState(CHANNEL_NET_MENU0, new StringType(itemData));
680 updateState(CHANNEL_NET_MENU1, new StringType(itemData));
683 updateState(CHANNEL_NET_MENU2, new StringType(itemData));
686 updateState(CHANNEL_NET_MENU3, new StringType(itemData));
689 updateState(CHANNEL_NET_MENU4, new StringType(itemData));
692 updateState(CHANNEL_NET_MENU5, new StringType(itemData));
695 updateState(CHANNEL_NET_MENU6, new StringType(itemData));
698 updateState(CHANNEL_NET_MENU7, new StringType(itemData));
701 updateState(CHANNEL_NET_MENU8, new StringType(itemData));
704 updateState(CHANNEL_NET_MENU9, new StringType(itemData));
710 updateMenuPosition(data);
715 private void updateMenuPosition(String data) {
716 char position = data.charAt(1);
717 int pos = Character.getNumericValue(position);
719 logger.debug("Updating menu position to {}", pos);
722 updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
724 updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
727 if (data.endsWith("P")) {
732 private void resetNetMenu() {
733 logger.debug("Reset net menu");
734 updateState(CHANNEL_NET_MENU0, new StringType("-"));
735 updateState(CHANNEL_NET_MENU1, new StringType("-"));
736 updateState(CHANNEL_NET_MENU2, new StringType("-"));
737 updateState(CHANNEL_NET_MENU3, new StringType("-"));
738 updateState(CHANNEL_NET_MENU4, new StringType("-"));
739 updateState(CHANNEL_NET_MENU5, new StringType("-"));
740 updateState(CHANNEL_NET_MENU6, new StringType("-"));
741 updateState(CHANNEL_NET_MENU7, new StringType("-"));
742 updateState(CHANNEL_NET_MENU8, new StringType("-"));
743 updateState(CHANNEL_NET_MENU9, new StringType("-"));
746 private State convertNetUsbPlayStatus(String data) {
747 State state = UnDefType.UNDEF;
748 switch (data.charAt(0)) {
750 state = PlayPauseType.PLAY;
754 state = PlayPauseType.PAUSE;
757 state = RewindFastforwardType.FASTFORWARD;
760 state = RewindFastforwardType.REWIND;
767 public void sendRawCommand(String command, String value) {
768 if (connection != null) {
769 connection.send(command, value);
771 logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
775 private void sendCommand(EiscpCommand deviceCommand) {
776 if (connection != null) {
777 connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
779 logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
783 private void sendCommand(EiscpCommand deviceCommand, Command command) {
784 if (connection != null) {
785 final String cmd = deviceCommand.getCommand();
786 String valTemplate = deviceCommand.getValue();
789 if (command instanceof OnOffType) {
790 val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
792 } else if (command instanceof StringType) {
793 val = String.format(valTemplate, command);
795 } else if (command instanceof DecimalType decimalCommand) {
796 val = String.format(valTemplate, decimalCommand.intValue());
798 } else if (command instanceof PercentType percentCommand) {
799 val = String.format(valTemplate, percentCommand.intValue());
804 logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
805 connection.getConnectionName());
806 connection.send(cmd, val);
808 logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
813 * Check the status of the AVR.
817 private void checkStatus() {
818 sendCommand(EiscpCommand.POWER_QUERY);
820 if (connection != null && connection.isConnected()) {
821 sendCommand(EiscpCommand.VOLUME_QUERY);
822 sendCommand(EiscpCommand.SOURCE_QUERY);
823 sendCommand(EiscpCommand.MUTE_QUERY);
824 sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
825 sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
826 sendCommand(EiscpCommand.INFO_QUERY);
827 sendCommand(EiscpCommand.AUDIOINFO_QUERY);
828 sendCommand(EiscpCommand.VIDEOINFO_QUERY);
830 if (isChannelAvailable(CHANNEL_POWERZONE2)) {
831 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
832 sendCommand(EiscpCommand.ZONE2_VOLUME_QUERY);
833 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
834 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
837 if (isChannelAvailable(CHANNEL_POWERZONE3)) {
838 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
839 sendCommand(EiscpCommand.ZONE3_VOLUME_QUERY);
840 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
841 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
844 updateStatus(ThingStatus.OFFLINE);
848 private boolean isChannelAvailable(String channel) {
849 List<Channel> channels = getThing().getChannels();
850 for (Channel c : channels) {
851 if (c.getUID().getId().equals(channel)) {
858 private void handleVolumeSet(EiscpCommand.Zone zone, final State currentValue, final Command command) {
859 if (command instanceof PercentType percentCommand) {
860 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_SET), downScaleVolume(percentCommand));
861 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
862 if (currentValue instanceof PercentType percentCommand) {
863 if (percentCommand.intValue() < configuration.volumeLimit) {
864 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_UP));
866 logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
869 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
870 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_DOWN));
871 } else if (command.equals(OnOffType.OFF)) {
872 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
873 } else if (command.equals(OnOffType.ON)) {
874 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
875 } else if (command.equals(RefreshType.REFRESH)) {
876 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_QUERY));
877 sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_QUERY));
881 private State handleReceivedVolume(State volume) {
882 if (volume instanceof DecimalType decimalCommand) {
883 return upScaleVolume(decimalCommand);
888 private PercentType upScaleVolume(DecimalType volume) {
889 PercentType newVolume = scaleVolumeFromReceiver(volume);
891 if (configuration.volumeLimit < 100) {
892 double scaleCoefficient = 100d / configuration.volumeLimit;
893 PercentType unLimitedVolume = newVolume;
894 newVolume = new PercentType(((Double) (newVolume.doubleValue() * scaleCoefficient)).intValue());
895 logger.debug("Up scaled volume level '{}' to '{}'", unLimitedVolume, newVolume);
901 private DecimalType downScaleVolume(PercentType volume) {
902 PercentType limitedVolume = volume;
904 if (configuration.volumeLimit < 100) {
905 double scaleCoefficient = configuration.volumeLimit / 100d;
906 limitedVolume = new PercentType(((Double) (volume.doubleValue() * scaleCoefficient)).intValue());
907 logger.debug("Limited volume level '{}' to '{}'", volume, limitedVolume);
910 return scaleVolumeForReceiver(limitedVolume);
913 private DecimalType scaleVolumeForReceiver(PercentType volume) {
914 return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
917 private PercentType scaleVolumeFromReceiver(DecimalType volume) {
918 return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
921 public PercentType getVolume() throws IOException {
922 if (volumeLevelZone1 instanceof PercentType percentCommand) {
923 return percentCommand;
926 throw new IOException();
929 public void setVolume(PercentType volume) throws IOException {
930 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
933 private String guessMimeTypeFromData(byte[] data) {
934 String mimeType = HttpUtil.guessContentTypeFromData(data);
935 logger.debug("Mime type guess from content: {}", mimeType);
936 if (mimeType == null) {
937 mimeType = RawType.DEFAULT_MIME_TYPE;
939 logger.debug("Mime type: {}", mimeType);
944 public Collection<Class<? extends ThingHandlerService>> getServices() {
945 return List.of(OnkyoThingActions.class);