]> git.basschouten.com Git - openhab-addons.git/blob
19e075657d9d6eb4923e08f9218480dd7b56ac61
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.onkyo.internal.handler;
14
15 import static org.openhab.binding.onkyo.internal.OnkyoBindingConstants.*;
16
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;
24
25 import javax.xml.parsers.DocumentBuilder;
26 import javax.xml.parsers.DocumentBuilderFactory;
27 import javax.xml.parsers.ParserConfigurationException;
28
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;
68
69 /**
70  * The {@link OnkyoHandler} is responsible for handling commands, which are
71  * sent to one of the channels.
72  *
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
77  */
78 public class OnkyoHandler extends OnkyoUpnpHandler implements OnkyoEventListener {
79
80     private final Logger logger = LoggerFactory.getLogger(OnkyoHandler.class);
81
82     private OnkyoDeviceConfiguration configuration;
83
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;
92
93     private final OnkyoStateDescriptionProvider stateDescriptionProvider;
94
95     private final OnkyoAlbumArt onkyoAlbumArt = new OnkyoAlbumArt();
96
97     private static final int NET_USB_ID = 43;
98
99     public OnkyoHandler(Thing thing, UpnpIOService upnpIOService,
100             OnkyoStateDescriptionProvider stateDescriptionProvider) {
101         super(thing, upnpIOService);
102         this.stateDescriptionProvider = stateDescriptionProvider;
103     }
104
105     /**
106      * Initialize the state of the receiver.
107      */
108     @Override
109     public void initialize() {
110         logger.debug("Initializing handler for Onkyo Receiver");
111         configuration = getConfigAs(OnkyoDeviceConfiguration.class);
112         logger.info("Using configuration: {}", configuration.toString());
113
114         connection = new OnkyoConnection(configuration.ipAddress, configuration.port);
115         connection.addEventListener(this);
116
117         scheduler.execute(() -> {
118             logger.debug("Open connection to Onkyo Receiver @{}", connection.getConnectionName());
119             connection.openConnection();
120             if (connection.isConnected()) {
121                 updateStatus(ThingStatus.ONLINE);
122                 checkStatus();
123             }
124         });
125
126         if (configuration.refreshInterval > 0) {
127             // Start resource refresh updater
128             resourceUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
129                 try {
130                     logger.debug("Send resource update requests to Onkyo Receiver @{}", connection.getConnectionName());
131                     checkStatus();
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());
138                 }
139             }, configuration.refreshInterval, configuration.refreshInterval, TimeUnit.SECONDS);
140         }
141     }
142
143     @Override
144     public void dispose() {
145         super.dispose();
146         if (resourceUpdaterFuture != null) {
147             resourceUpdaterFuture.cancel(true);
148         }
149         if (connection != null) {
150             connection.removeEventListener(this);
151             connection.closeConnection();
152         }
153     }
154
155     @Override
156     public void handleCommand(ChannelUID channelUID, Command command) {
157         logger.debug("handleCommand for channel {}: {}", channelUID.getId(), command.toString());
158         switch (channelUID.getId()) {
159             /*
160              * ZONE 1
161              */
162
163             case CHANNEL_POWER:
164                 if (command instanceof OnOffType) {
165                     sendCommand(EiscpCommand.POWER_SET, command);
166                 } else if (command.equals(RefreshType.REFRESH)) {
167                     sendCommand(EiscpCommand.POWER_QUERY);
168                 }
169                 break;
170             case CHANNEL_MUTE:
171                 if (command instanceof OnOffType) {
172                     sendCommand(EiscpCommand.MUTE_SET, command);
173                 } else if (command.equals(RefreshType.REFRESH)) {
174                     sendCommand(EiscpCommand.MUTE_QUERY);
175                 }
176                 break;
177             case CHANNEL_VOLUME:
178                 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, command);
179                 break;
180             case CHANNEL_INPUT:
181                 if (command instanceof DecimalType decimalCommand) {
182                     selectInput(decimalCommand.intValue());
183                 } else if (command.equals(RefreshType.REFRESH)) {
184                     sendCommand(EiscpCommand.SOURCE_QUERY);
185                 }
186                 break;
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);
192                 }
193                 break;
194
195             /*
196              * ZONE 2
197              */
198
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);
204                 }
205                 break;
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);
211                 }
212                 break;
213             case CHANNEL_VOLUMEZONE2:
214                 handleVolumeSet(EiscpCommand.Zone.ZONE2, volumeLevelZone2, command);
215                 break;
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);
221                 }
222                 break;
223
224             /*
225              * ZONE 3
226              */
227
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);
233                 }
234                 break;
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);
240                 }
241                 break;
242             case CHANNEL_VOLUMEZONE3:
243                 handleVolumeSet(EiscpCommand.Zone.ZONE3, volumeLevelZone3, command);
244                 break;
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);
250                 }
251                 break;
252
253             /*
254              * NET PLAYER
255              */
256
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);
263                     }
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);
269                     }
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);
275                     }
276                 } else if (command.equals(RefreshType.REFRESH)) {
277                     sendCommand(EiscpCommand.NETUSB_PLAY_STATUS_QUERY);
278                 }
279                 break;
280             case CHANNEL_PLAY_URI:
281                 handlePlayUri(command);
282                 break;
283             case CHANNEL_ALBUM_ART:
284             case CHANNEL_ALBUM_ART_URL:
285                 if (command.equals(RefreshType.REFRESH)) {
286                     sendCommand(EiscpCommand.NETUSB_ALBUM_ART_QUERY);
287                 }
288                 break;
289             case CHANNEL_ARTIST:
290                 if (command.equals(RefreshType.REFRESH)) {
291                     sendCommand(EiscpCommand.NETUSB_SONG_ARTIST_QUERY);
292                 }
293                 break;
294             case CHANNEL_ALBUM:
295                 if (command.equals(RefreshType.REFRESH)) {
296                     sendCommand(EiscpCommand.NETUSB_SONG_ALBUM_QUERY);
297                 }
298                 break;
299             case CHANNEL_TITLE:
300                 if (command.equals(RefreshType.REFRESH)) {
301                     sendCommand(EiscpCommand.NETUSB_SONG_TITLE_QUERY);
302                 }
303                 break;
304             case CHANNEL_CURRENTPLAYINGTIME:
305                 if (command.equals(RefreshType.REFRESH)) {
306                     sendCommand(EiscpCommand.NETUSB_SONG_ELAPSEDTIME_QUERY);
307                 }
308                 break;
309
310             /*
311              * NET MENU
312              */
313
314             case CHANNEL_NET_MENU_CONTROL:
315                 if (command instanceof StringType) {
316                     final String cmdName = command.toString();
317                     handleNetMenuCommand(cmdName);
318                 }
319                 break;
320             case CHANNEL_NET_MENU_TITLE:
321                 if (command.equals(RefreshType.REFRESH)) {
322                     sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
323                 }
324                 break;
325             case CHANNEL_AUDIOINFO:
326                 if (command.equals(RefreshType.REFRESH)) {
327                     sendCommand(EiscpCommand.AUDIOINFO_QUERY);
328                 }
329                 break;
330
331             /*
332              * MEDIA INFO
333              */
334             case CHANNEL_AUDIO_IN_INFO:
335             case CHANNEL_AUDIO_OUT_INFO:
336                 if (command.equals(RefreshType.REFRESH)) {
337                     sendCommand(EiscpCommand.AUDIOINFO_QUERY);
338                 }
339                 break;
340             case CHANNEL_VIDEO_IN_INFO:
341             case CHANNEL_VIDEO_OUT_INFO:
342                 if (command.equals(RefreshType.REFRESH)) {
343                     sendCommand(EiscpCommand.VIDEOINFO_QUERY);
344                 }
345                 break;
346             /*
347              * MISC
348              */
349
350             default:
351                 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
352                 break;
353         }
354     }
355
356     private void populateInputs(NodeList selectorlist) {
357         List<StateOption> options = new ArrayList<>();
358
359         for (int i = 0; i < selectorlist.getLength(); i++) {
360             Element selectorItem = (Element) selectorlist.item(i);
361
362             options.add(new StateOption(String.valueOf(Integer.parseInt(selectorItem.getAttribute("id"), 16)),
363                     selectorItem.getAttribute("name")));
364         }
365         logger.debug("Got Input List from Receiver {}", options);
366
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);
370     }
371
372     private void doPowerOnCheck(State state) {
373         if (configuration.refreshInterval == 0 && lastPowerState == OnOffType.OFF && state == OnOffType.ON) {
374             sendCommand(EiscpCommand.INFO_QUERY);
375         }
376         lastPowerState = state;
377     }
378
379     @Override
380     public void statusUpdateReceived(String ip, EiscpMessage data) {
381         logger.debug("Received status update from Onkyo Receiver @{}: data={}", connection.getConnectionName(), data);
382
383         updateStatus(ThingStatus.ONLINE);
384
385         try {
386             EiscpCommand receivedCommand = null;
387
388             try {
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);
393                 return;
394             }
395
396             logger.debug("Received command {}", receivedCommand);
397
398             switch (receivedCommand) {
399                 /*
400                  * ZONE 1
401                  */
402                 case POWER:
403                     State powerState = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
404                     updateState(CHANNEL_POWER, powerState);
405                     doPowerOnCheck(powerState);
406                     break;
407                 case MUTE:
408                     updateState(CHANNEL_MUTE, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
409                     break;
410                 case VOLUME:
411                     volumeLevelZone1 = handleReceivedVolume(
412                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
413                     updateState(CHANNEL_VOLUME, volumeLevelZone1);
414                     break;
415                 case SOURCE:
416                     updateState(CHANNEL_INPUT, convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
417                     break;
418                 case LISTEN_MODE:
419                     updateState(CHANNEL_LISTENMODE,
420                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
421                     break;
422
423                 /*
424                  * ZONE 2
425                  */
426                 case ZONE2_POWER:
427                     State powerZone2State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
428                     updateState(CHANNEL_POWERZONE2, powerZone2State);
429                     doPowerOnCheck(powerZone2State);
430                     break;
431                 case ZONE2_MUTE:
432                     updateState(CHANNEL_MUTEZONE2, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
433                     break;
434                 case ZONE2_VOLUME:
435                     volumeLevelZone2 = handleReceivedVolume(
436                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
437                     updateState(CHANNEL_VOLUMEZONE2, volumeLevelZone2);
438                     break;
439                 case ZONE2_SOURCE:
440                     updateState(CHANNEL_INPUTZONE2,
441                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
442                     break;
443
444                 /*
445                  * ZONE 3
446                  */
447                 case ZONE3_POWER:
448                     State powerZone3State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
449                     updateState(CHANNEL_POWERZONE3, powerZone3State);
450                     doPowerOnCheck(powerZone3State);
451                     break;
452                 case ZONE3_MUTE:
453                     updateState(CHANNEL_MUTEZONE3, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
454                     break;
455                 case ZONE3_VOLUME:
456                     volumeLevelZone3 = handleReceivedVolume(
457                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
458                     updateState(CHANNEL_VOLUMEZONE3, volumeLevelZone3);
459                     break;
460                 case ZONE3_SOURCE:
461                     updateState(CHANNEL_INPUTZONE3,
462                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
463                     break;
464
465                 /*
466                  * NET PLAYER
467                  */
468
469                 case NETUSB_SONG_ARTIST:
470                     updateState(CHANNEL_ARTIST, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
471                     break;
472                 case NETUSB_SONG_ALBUM:
473                     updateState(CHANNEL_ALBUM, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
474                     break;
475                 case NETUSB_SONG_TITLE:
476                     updateState(CHANNEL_TITLE, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
477                     break;
478                 case NETUSB_SONG_ELAPSEDTIME:
479                     updateState(CHANNEL_CURRENTPLAYINGTIME,
480                             convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
481                     break;
482                 case NETUSB_PLAY_STATUS:
483                     updateState(CHANNEL_CONTROL, convertNetUsbPlayStatus(data.getValue()));
484                     break;
485                 case NETUSB_ALBUM_ART:
486                     updateAlbumArt(data.getValue());
487                     break;
488                 case NETUSB_TITLE:
489                     updateNetTitle(data.getValue());
490                     break;
491                 case NETUSB_MENU:
492                     updateNetMenu(data.getValue());
493                     break;
494
495                 /*
496                  * MISC
497                  */
498                 case AUDIOINFO:
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));
503                     break;
504                 case VIDEOINFO:
505                     updateState(CHANNEL_VIDEO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 3));
506                     updateState(CHANNEL_VIDEO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 4, 7));
507                     break;
508                 case INFO:
509                     processInfo(data.getValue());
510                     logger.debug("Info message: '{}'", data.getValue());
511                     break;
512
513                 default:
514                     logger.debug("Received unhandled status update from Onkyo Receiver @{}: data={}",
515                             connection.getConnectionName(), data);
516
517             }
518
519         } catch (Exception ex) {
520             logger.warn("Exception in statusUpdateReceived for Onkyo Receiver @{}. Cause: {}, data received: {}",
521                     connection.getConnectionName(), ex.getMessage(), data);
522         }
523     }
524
525     private void processInfo(String infoXML) {
526         try {
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);
538
539                 NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
540                 populateInputs(selectableInputs);
541             }
542         } catch (ParserConfigurationException | SAXException | IOException e) {
543             logger.debug("Error occured during Info XML parsing.", e);
544         }
545     }
546
547     @Override
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);
551     }
552
553     private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
554         State state = UnDefType.UNDEF;
555
556         try {
557             int index;
558
559             if (data.contentEquals("N/A")) {
560                 state = UnDefType.UNDEF;
561
562             } else if (classToConvert == OnOffType.class) {
563                 index = Integer.parseInt(data, 16);
564                 state = OnOffType.from(index != 0);
565
566             } else if (classToConvert == DecimalType.class) {
567                 index = Integer.parseInt(data, 16);
568                 state = new DecimalType(index);
569
570             } else if (classToConvert == PercentType.class) {
571                 index = Integer.parseInt(data, 16);
572                 state = new PercentType(index);
573
574             } else if (classToConvert == StringType.class) {
575                 state = new StringType(data);
576
577             }
578         } catch (Exception e) {
579             logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
580         }
581
582         logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
583         return state;
584     }
585
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));
602         } else {
603             logger.debug("Received unknown menucommand {}", cmdName);
604         }
605     }
606
607     private void selectInput(int inputId) {
608         sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
609         currentInput = inputId;
610     }
611
612     @SuppressWarnings("unused")
613     private void onInputChanged(int newInput) {
614         currentInput = newInput;
615
616         if (newInput != NET_USB_ID) {
617             resetNetMenu();
618
619             updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
620             updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
621             updateState(CHANNEL_TITLE, UnDefType.UNDEF);
622             updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
623         }
624     }
625
626     private void updateAlbumArt(String data) {
627         onkyoAlbumArt.addFrame(data);
628
629         if (onkyoAlbumArt.isAlbumCoverReady()) {
630             try {
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);
636                     }
637                     updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
638                 } else {
639                     updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
640                 }
641             } catch (IllegalArgumentException e) {
642                 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
643             }
644             onkyoAlbumArt.clearAlbumArt();
645         }
646
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);
651         } else {
652             logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
653             updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
654         }
655     }
656
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);
661
662         String title = "";
663         if (data.length() > 21) {
664             title = data.substring(22, data.length());
665         }
666
667         updateState(CHANNEL_NET_MENU_TITLE,
668                 new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
669     }
670
671     private void updateNetMenu(String data) {
672         switch (data.charAt(0)) {
673             case 'U':
674                 String itemData = data.substring(3, data.length());
675                 switch (data.charAt(1)) {
676                     case '0':
677                         updateState(CHANNEL_NET_MENU0, new StringType(itemData));
678                         break;
679                     case '1':
680                         updateState(CHANNEL_NET_MENU1, new StringType(itemData));
681                         break;
682                     case '2':
683                         updateState(CHANNEL_NET_MENU2, new StringType(itemData));
684                         break;
685                     case '3':
686                         updateState(CHANNEL_NET_MENU3, new StringType(itemData));
687                         break;
688                     case '4':
689                         updateState(CHANNEL_NET_MENU4, new StringType(itemData));
690                         break;
691                     case '5':
692                         updateState(CHANNEL_NET_MENU5, new StringType(itemData));
693                         break;
694                     case '6':
695                         updateState(CHANNEL_NET_MENU6, new StringType(itemData));
696                         break;
697                     case '7':
698                         updateState(CHANNEL_NET_MENU7, new StringType(itemData));
699                         break;
700                     case '8':
701                         updateState(CHANNEL_NET_MENU8, new StringType(itemData));
702                         break;
703                     case '9':
704                         updateState(CHANNEL_NET_MENU9, new StringType(itemData));
705                         break;
706                 }
707                 break;
708
709             case 'C':
710                 updateMenuPosition(data);
711                 break;
712         }
713     }
714
715     private void updateMenuPosition(String data) {
716         char position = data.charAt(1);
717         int pos = Character.getNumericValue(position);
718
719         logger.debug("Updating menu position to {}", pos);
720
721         if (pos == -1) {
722             updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
723         } else {
724             updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
725         }
726
727         if (data.endsWith("P")) {
728             resetNetMenu();
729         }
730     }
731
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("-"));
744     }
745
746     private State convertNetUsbPlayStatus(String data) {
747         State state = UnDefType.UNDEF;
748         switch (data.charAt(0)) {
749             case 'P':
750                 state = PlayPauseType.PLAY;
751                 break;
752             case 'p':
753             case 'S':
754                 state = PlayPauseType.PAUSE;
755                 break;
756             case 'F':
757                 state = RewindFastforwardType.FASTFORWARD;
758                 break;
759             case 'R':
760                 state = RewindFastforwardType.REWIND;
761                 break;
762
763         }
764         return state;
765     }
766
767     public void sendRawCommand(String command, String value) {
768         if (connection != null) {
769             connection.send(command, value);
770         } else {
771             logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
772         }
773     }
774
775     private void sendCommand(EiscpCommand deviceCommand) {
776         if (connection != null) {
777             connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
778         } else {
779             logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
780         }
781     }
782
783     private void sendCommand(EiscpCommand deviceCommand, Command command) {
784         if (connection != null) {
785             final String cmd = deviceCommand.getCommand();
786             String valTemplate = deviceCommand.getValue();
787             String val;
788
789             if (command instanceof OnOffType) {
790                 val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
791
792             } else if (command instanceof StringType) {
793                 val = String.format(valTemplate, command);
794
795             } else if (command instanceof DecimalType decimalCommand) {
796                 val = String.format(valTemplate, decimalCommand.intValue());
797
798             } else if (command instanceof PercentType percentCommand) {
799                 val = String.format(valTemplate, percentCommand.intValue());
800             } else {
801                 val = valTemplate;
802             }
803
804             logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
805                     connection.getConnectionName());
806             connection.send(cmd, val);
807         } else {
808             logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
809         }
810     }
811
812     /**
813      * Check the status of the AVR.
814      *
815      * @return
816      */
817     private void checkStatus() {
818         sendCommand(EiscpCommand.POWER_QUERY);
819
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);
829
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);
835             }
836
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);
842             }
843         } else {
844             updateStatus(ThingStatus.OFFLINE);
845         }
846     }
847
848     private boolean isChannelAvailable(String channel) {
849         List<Channel> channels = getThing().getChannels();
850         for (Channel c : channels) {
851             if (c.getUID().getId().equals(channel)) {
852                 return true;
853             }
854         }
855         return false;
856     }
857
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));
865                 } else {
866                     logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
867                 }
868             }
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));
878         }
879     }
880
881     private State handleReceivedVolume(State volume) {
882         if (volume instanceof DecimalType decimalCommand) {
883             return upScaleVolume(decimalCommand);
884         }
885         return volume;
886     }
887
888     private PercentType upScaleVolume(DecimalType volume) {
889         PercentType newVolume = scaleVolumeFromReceiver(volume);
890
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);
896         }
897
898         return newVolume;
899     }
900
901     private DecimalType downScaleVolume(PercentType volume) {
902         PercentType limitedVolume = volume;
903
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);
908         }
909
910         return scaleVolumeForReceiver(limitedVolume);
911     }
912
913     private DecimalType scaleVolumeForReceiver(PercentType volume) {
914         return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
915     }
916
917     private PercentType scaleVolumeFromReceiver(DecimalType volume) {
918         return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
919     }
920
921     public PercentType getVolume() throws IOException {
922         if (volumeLevelZone1 instanceof PercentType percentCommand) {
923             return percentCommand;
924         }
925
926         throw new IOException();
927     }
928
929     public void setVolume(PercentType volume) throws IOException {
930         handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
931     }
932
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;
938         }
939         logger.debug("Mime type: {}", mimeType);
940         return mimeType;
941     }
942
943     @Override
944     public Collection<Class<? extends ThingHandlerService>> getServices() {
945         return List.of(OnkyoThingActions.class);
946     }
947 }