]> git.basschouten.com Git - openhab-addons.git/blob
e0dff912c92a14805e4c9704ac7ef58ab2d04f55
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.Collections;
22 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.xml.parsers.DocumentBuilder;
27 import javax.xml.parsers.DocumentBuilderFactory;
28 import javax.xml.parsers.ParserConfigurationException;
29
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.io.net.http.HttpUtil;
41 import org.openhab.core.io.transport.upnp.UpnpIOService;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.IncreaseDecreaseType;
44 import org.openhab.core.library.types.NextPreviousType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.library.types.PlayPauseType;
48 import org.openhab.core.library.types.RawType;
49 import org.openhab.core.library.types.RewindFastforwardType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.openhab.core.types.StateOption;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64 import org.w3c.dom.Document;
65 import org.w3c.dom.Element;
66 import org.w3c.dom.NodeList;
67 import org.xml.sax.InputSource;
68 import org.xml.sax.SAXException;
69
70 /**
71  * The {@link OnkyoHandler} is responsible for handling commands, which are
72  * sent to one of the channels.
73  *
74  * @author Paul Frank - Initial contribution
75  * @author Marcel Verpaalen - parsing additional commands
76  * @author Pauli Anttila - lot of refactoring
77  * @author Stewart Cossey - add dynamic state description provider
78  */
79 public class OnkyoHandler extends OnkyoUpnpHandler implements OnkyoEventListener {
80
81     private final Logger logger = LoggerFactory.getLogger(OnkyoHandler.class);
82
83     private OnkyoDeviceConfiguration configuration;
84
85     private OnkyoConnection connection;
86     private ScheduledFuture<?> resourceUpdaterFuture;
87     @SuppressWarnings("unused")
88     private int currentInput = -1;
89     private State volumeLevelZone1 = UnDefType.UNDEF;
90     private State volumeLevelZone2 = UnDefType.UNDEF;
91     private State volumeLevelZone3 = UnDefType.UNDEF;
92     private State lastPowerState = OnOffType.OFF;
93
94     private final OnkyoStateDescriptionProvider stateDescriptionProvider;
95
96     private final OnkyoAlbumArt onkyoAlbumArt = new OnkyoAlbumArt();
97
98     private static final int NET_USB_ID = 43;
99
100     public OnkyoHandler(Thing thing, UpnpIOService upnpIOService,
101             OnkyoStateDescriptionProvider stateDescriptionProvider) {
102         super(thing, upnpIOService);
103         this.stateDescriptionProvider = stateDescriptionProvider;
104     }
105
106     /**
107      * Initialize the state of the receiver.
108      */
109     @Override
110     public void initialize() {
111         logger.debug("Initializing handler for Onkyo Receiver");
112         configuration = getConfigAs(OnkyoDeviceConfiguration.class);
113         logger.info("Using configuration: {}", configuration.toString());
114
115         connection = new OnkyoConnection(configuration.ipAddress, configuration.port);
116         connection.addEventListener(this);
117
118         scheduler.execute(() -> {
119             logger.debug("Open connection to Onkyo Receiver @{}", connection.getConnectionName());
120             connection.openConnection();
121             if (connection.isConnected()) {
122                 updateStatus(ThingStatus.ONLINE);
123                 checkStatus();
124             }
125         });
126
127         if (configuration.refreshInterval > 0) {
128             // Start resource refresh updater
129             resourceUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
130                 try {
131                     logger.debug("Send resource update requests to Onkyo Receiver @{}", connection.getConnectionName());
132                     checkStatus();
133                 } catch (LinkageError e) {
134                     logger.warn("Failed to send resource update requests to Onkyo Receiver @{}. Cause: {}",
135                             connection.getConnectionName(), e.getMessage());
136                 } catch (Exception ex) {
137                     logger.warn("Exception in resource refresh Thread Onkyo Receiver @{}. Cause: {}",
138                             connection.getConnectionName(), ex.getMessage());
139                 }
140             }, configuration.refreshInterval, configuration.refreshInterval, TimeUnit.SECONDS);
141         }
142     }
143
144     @Override
145     public void dispose() {
146         super.dispose();
147         if (resourceUpdaterFuture != null) {
148             resourceUpdaterFuture.cancel(true);
149         }
150         if (connection != null) {
151             connection.removeEventListener(this);
152             connection.closeConnection();
153         }
154     }
155
156     @Override
157     public void handleCommand(ChannelUID channelUID, Command command) {
158         logger.debug("handleCommand for channel {}: {}", channelUID.getId(), command.toString());
159         switch (channelUID.getId()) {
160             /*
161              * ZONE 1
162              */
163
164             case CHANNEL_POWER:
165                 if (command instanceof OnOffType) {
166                     sendCommand(EiscpCommand.POWER_SET, command);
167                 } else if (command.equals(RefreshType.REFRESH)) {
168                     sendCommand(EiscpCommand.POWER_QUERY);
169                 }
170                 break;
171             case CHANNEL_MUTE:
172                 if (command instanceof OnOffType) {
173                     sendCommand(EiscpCommand.MUTE_SET, command);
174                 } else if (command.equals(RefreshType.REFRESH)) {
175                     sendCommand(EiscpCommand.MUTE_QUERY);
176                 }
177                 break;
178             case CHANNEL_VOLUME:
179                 handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, command);
180                 break;
181             case CHANNEL_INPUT:
182                 if (command instanceof DecimalType) {
183                     selectInput(((DecimalType) command).intValue());
184                 } else if (command.equals(RefreshType.REFRESH)) {
185                     sendCommand(EiscpCommand.SOURCE_QUERY);
186                 }
187                 break;
188             case CHANNEL_LISTENMODE:
189                 if (command instanceof DecimalType) {
190                     sendCommand(EiscpCommand.LISTEN_MODE_SET, command);
191                 } else if (command.equals(RefreshType.REFRESH)) {
192                     sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
193                 }
194                 break;
195
196             /*
197              * ZONE 2
198              */
199
200             case CHANNEL_POWERZONE2:
201                 if (command instanceof OnOffType) {
202                     sendCommand(EiscpCommand.ZONE2_POWER_SET, command);
203                 } else if (command.equals(RefreshType.REFRESH)) {
204                     sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
205                 }
206                 break;
207             case CHANNEL_MUTEZONE2:
208                 if (command instanceof OnOffType) {
209                     sendCommand(EiscpCommand.ZONE2_MUTE_SET, command);
210                 } else if (command.equals(RefreshType.REFRESH)) {
211                     sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
212                 }
213                 break;
214             case CHANNEL_VOLUMEZONE2:
215                 handleVolumeSet(EiscpCommand.Zone.ZONE2, volumeLevelZone2, command);
216                 break;
217             case CHANNEL_INPUTZONE2:
218                 if (command instanceof DecimalType) {
219                     sendCommand(EiscpCommand.ZONE2_SOURCE_SET, command);
220                 } else if (command.equals(RefreshType.REFRESH)) {
221                     sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
222                 }
223                 break;
224
225             /*
226              * ZONE 3
227              */
228
229             case CHANNEL_POWERZONE3:
230                 if (command instanceof OnOffType) {
231                     sendCommand(EiscpCommand.ZONE3_POWER_SET, command);
232                 } else if (command.equals(RefreshType.REFRESH)) {
233                     sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
234                 }
235                 break;
236             case CHANNEL_MUTEZONE3:
237                 if (command instanceof OnOffType) {
238                     sendCommand(EiscpCommand.ZONE3_MUTE_SET, command);
239                 } else if (command.equals(RefreshType.REFRESH)) {
240                     sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
241                 }
242                 break;
243             case CHANNEL_VOLUMEZONE3:
244                 handleVolumeSet(EiscpCommand.Zone.ZONE3, volumeLevelZone3, command);
245                 break;
246             case CHANNEL_INPUTZONE3:
247                 if (command instanceof DecimalType) {
248                     sendCommand(EiscpCommand.ZONE3_SOURCE_SET, command);
249                 } else if (command.equals(RefreshType.REFRESH)) {
250                     sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
251                 }
252                 break;
253
254             /*
255              * NET PLAYER
256              */
257
258             case CHANNEL_CONTROL:
259                 if (command instanceof PlayPauseType) {
260                     if (command.equals(PlayPauseType.PLAY)) {
261                         sendCommand(EiscpCommand.NETUSB_OP_PLAY);
262                     } else if (command.equals(PlayPauseType.PAUSE)) {
263                         sendCommand(EiscpCommand.NETUSB_OP_PAUSE);
264                     }
265                 } else if (command instanceof NextPreviousType) {
266                     if (command.equals(NextPreviousType.NEXT)) {
267                         sendCommand(EiscpCommand.NETUSB_OP_TRACKUP);
268                     } else if (command.equals(NextPreviousType.PREVIOUS)) {
269                         sendCommand(EiscpCommand.NETUSB_OP_TRACKDWN);
270                     }
271                 } else if (command instanceof RewindFastforwardType) {
272                     if (command.equals(RewindFastforwardType.REWIND)) {
273                         sendCommand(EiscpCommand.NETUSB_OP_REW);
274                     } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
275                         sendCommand(EiscpCommand.NETUSB_OP_FF);
276                     }
277                 } else if (command.equals(RefreshType.REFRESH)) {
278                     sendCommand(EiscpCommand.NETUSB_PLAY_STATUS_QUERY);
279                 }
280                 break;
281             case CHANNEL_PLAY_URI:
282                 handlePlayUri(command);
283                 break;
284             case CHANNEL_ALBUM_ART:
285             case CHANNEL_ALBUM_ART_URL:
286                 if (command.equals(RefreshType.REFRESH)) {
287                     sendCommand(EiscpCommand.NETUSB_ALBUM_ART_QUERY);
288                 }
289                 break;
290             case CHANNEL_ARTIST:
291                 if (command.equals(RefreshType.REFRESH)) {
292                     sendCommand(EiscpCommand.NETUSB_SONG_ARTIST_QUERY);
293                 }
294                 break;
295             case CHANNEL_ALBUM:
296                 if (command.equals(RefreshType.REFRESH)) {
297                     sendCommand(EiscpCommand.NETUSB_SONG_ALBUM_QUERY);
298                 }
299                 break;
300             case CHANNEL_TITLE:
301                 if (command.equals(RefreshType.REFRESH)) {
302                     sendCommand(EiscpCommand.NETUSB_SONG_TITLE_QUERY);
303                 }
304                 break;
305             case CHANNEL_CURRENTPLAYINGTIME:
306                 if (command.equals(RefreshType.REFRESH)) {
307                     sendCommand(EiscpCommand.NETUSB_SONG_ELAPSEDTIME_QUERY);
308                 }
309                 break;
310
311             /*
312              * NET MENU
313              */
314
315             case CHANNEL_NET_MENU_CONTROL:
316                 if (command instanceof StringType) {
317                     final String cmdName = command.toString();
318                     handleNetMenuCommand(cmdName);
319                 }
320                 break;
321             case CHANNEL_NET_MENU_TITLE:
322                 if (command.equals(RefreshType.REFRESH)) {
323                     sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
324                 }
325                 break;
326             case CHANNEL_AUDIOINFO:
327                 if (command.equals(RefreshType.REFRESH)) {
328                     sendCommand(EiscpCommand.AUDIOINFO_QUERY);
329                 }
330                 break;
331
332             /*
333              * MEDIA INFO
334              */
335             case CHANNEL_AUDIO_IN_INFO:
336             case CHANNEL_AUDIO_OUT_INFO:
337                 if (command.equals(RefreshType.REFRESH)) {
338                     sendCommand(EiscpCommand.AUDIOINFO_QUERY);
339                 }
340                 break;
341             case CHANNEL_VIDEO_IN_INFO:
342             case CHANNEL_VIDEO_OUT_INFO:
343                 if (command.equals(RefreshType.REFRESH)) {
344                     sendCommand(EiscpCommand.VIDEOINFO_QUERY);
345                 }
346                 break;
347             /*
348              * MISC
349              */
350
351             default:
352                 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
353                 break;
354         }
355     }
356
357     private void populateInputs(NodeList selectorlist) {
358         List<StateOption> options = new ArrayList<>();
359
360         for (int i = 0; i < selectorlist.getLength(); i++) {
361             Element selectorItem = (Element) selectorlist.item(i);
362
363             options.add(new StateOption(String.valueOf(Integer.parseInt(selectorItem.getAttribute("id"), 16)),
364                     selectorItem.getAttribute("name")));
365         }
366         logger.debug("Got Input List from Receiver {}", options);
367
368         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUT), options);
369         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE2), options);
370         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE3), options);
371     }
372
373     private void doPowerOnCheck(State state) {
374         if (configuration.refreshInterval == 0 && lastPowerState == OnOffType.OFF && state == OnOffType.ON) {
375             sendCommand(EiscpCommand.INFO_QUERY);
376         }
377         lastPowerState = state;
378     }
379
380     @Override
381     public void statusUpdateReceived(String ip, EiscpMessage data) {
382         logger.debug("Received status update from Onkyo Receiver @{}: data={}", connection.getConnectionName(), data);
383
384         updateStatus(ThingStatus.ONLINE);
385
386         try {
387             EiscpCommand receivedCommand = null;
388
389             try {
390                 receivedCommand = EiscpCommand.getCommandByCommandAndValueStr(data.getCommand(), "");
391             } catch (IllegalArgumentException ex) {
392                 logger.debug("Received unknown status update from Onkyo Receiver @{}: data={}",
393                         connection.getConnectionName(), data);
394                 return;
395             }
396
397             logger.debug("Received command {}", receivedCommand);
398
399             switch (receivedCommand) {
400                 /*
401                  * ZONE 1
402                  */
403                 case POWER:
404                     State powerState = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
405                     updateState(CHANNEL_POWER, powerState);
406                     doPowerOnCheck(powerState);
407                     break;
408                 case MUTE:
409                     updateState(CHANNEL_MUTE, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
410                     break;
411                 case VOLUME:
412                     volumeLevelZone1 = handleReceivedVolume(
413                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
414                     updateState(CHANNEL_VOLUME, volumeLevelZone1);
415                     break;
416                 case SOURCE:
417                     updateState(CHANNEL_INPUT, convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
418                     break;
419                 case LISTEN_MODE:
420                     updateState(CHANNEL_LISTENMODE,
421                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
422                     break;
423
424                 /*
425                  * ZONE 2
426                  */
427                 case ZONE2_POWER:
428                     State powerZone2State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
429                     updateState(CHANNEL_POWERZONE2, powerZone2State);
430                     doPowerOnCheck(powerZone2State);
431                     break;
432                 case ZONE2_MUTE:
433                     updateState(CHANNEL_MUTEZONE2, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
434                     break;
435                 case ZONE2_VOLUME:
436                     volumeLevelZone2 = handleReceivedVolume(
437                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
438                     updateState(CHANNEL_VOLUMEZONE2, volumeLevelZone2);
439                     break;
440                 case ZONE2_SOURCE:
441                     updateState(CHANNEL_INPUTZONE2,
442                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
443                     break;
444
445                 /*
446                  * ZONE 3
447                  */
448                 case ZONE3_POWER:
449                     State powerZone3State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
450                     updateState(CHANNEL_POWERZONE3, powerZone3State);
451                     doPowerOnCheck(powerZone3State);
452                     break;
453                 case ZONE3_MUTE:
454                     updateState(CHANNEL_MUTEZONE3, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
455                     break;
456                 case ZONE3_VOLUME:
457                     volumeLevelZone3 = handleReceivedVolume(
458                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
459                     updateState(CHANNEL_VOLUMEZONE3, volumeLevelZone3);
460                     break;
461                 case ZONE3_SOURCE:
462                     updateState(CHANNEL_INPUTZONE3,
463                             convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
464                     break;
465
466                 /*
467                  * NET PLAYER
468                  */
469
470                 case NETUSB_SONG_ARTIST:
471                     updateState(CHANNEL_ARTIST, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
472                     break;
473                 case NETUSB_SONG_ALBUM:
474                     updateState(CHANNEL_ALBUM, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
475                     break;
476                 case NETUSB_SONG_TITLE:
477                     updateState(CHANNEL_TITLE, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
478                     break;
479                 case NETUSB_SONG_ELAPSEDTIME:
480                     updateState(CHANNEL_CURRENTPLAYINGTIME,
481                             convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
482                     break;
483                 case NETUSB_PLAY_STATUS:
484                     updateState(CHANNEL_CONTROL, convertNetUsbPlayStatus(data.getValue()));
485                     break;
486                 case NETUSB_ALBUM_ART:
487                     updateAlbumArt(data.getValue());
488                     break;
489                 case NETUSB_TITLE:
490                     updateNetTitle(data.getValue());
491                     break;
492                 case NETUSB_MENU:
493                     updateNetMenu(data.getValue());
494                     break;
495
496                 /*
497                  * MISC
498                  */
499                 case AUDIOINFO:
500                     updateState(CHANNEL_AUDIOINFO, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
501                     logger.debug("audioinfo message: '{}'", data.getValue());
502                     updateState(CHANNEL_AUDIO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 2));
503                     updateState(CHANNEL_AUDIO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 3, 5));
504                     break;
505                 case VIDEOINFO:
506                     updateState(CHANNEL_VIDEO_IN_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 0, 3));
507                     updateState(CHANNEL_VIDEO_OUT_INFO, OnkyoParserHelper.infoBuilder(data.getValue(), 4, 7));
508                     break;
509                 case INFO:
510                     processInfo(data.getValue());
511                     logger.debug("Info message: '{}'", data.getValue());
512                     break;
513
514                 default:
515                     logger.debug("Received unhandled status update from Onkyo Receiver @{}: data={}",
516                             connection.getConnectionName(), data);
517
518             }
519
520         } catch (Exception ex) {
521             logger.warn("Exception in statusUpdateReceived for Onkyo Receiver @{}. Cause: {}, data received: {}",
522                     connection.getConnectionName(), ex.getMessage(), data);
523         }
524     }
525
526     private void processInfo(String infoXML) {
527         try {
528             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
529             // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
530             factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
531             factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
532             factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
533             factory.setXIncludeAware(false);
534             factory.setExpandEntityReferences(false);
535             DocumentBuilder builder = factory.newDocumentBuilder();
536             try (StringReader sr = new StringReader(infoXML)) {
537                 InputSource is = new InputSource(sr);
538                 Document doc = builder.parse(is);
539
540                 NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
541                 populateInputs(selectableInputs);
542             }
543         } catch (ParserConfigurationException | SAXException | IOException e) {
544             logger.debug("Error occured during Info XML parsing.", e);
545         }
546     }
547
548     @Override
549     public void connectionError(String ip, String errorMsg) {
550         logger.debug("Connection error occurred to Onkyo Receiver @{}", ip);
551         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
552     }
553
554     private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
555         State state = UnDefType.UNDEF;
556
557         try {
558             int index;
559
560             if (data.contentEquals("N/A")) {
561                 state = UnDefType.UNDEF;
562
563             } else if (classToConvert == OnOffType.class) {
564                 index = Integer.parseInt(data, 16);
565                 state = index == 0 ? OnOffType.OFF : OnOffType.ON;
566
567             } else if (classToConvert == DecimalType.class) {
568                 index = Integer.parseInt(data, 16);
569                 state = new DecimalType(index);
570
571             } else if (classToConvert == PercentType.class) {
572                 index = Integer.parseInt(data, 16);
573                 state = new PercentType(index);
574
575             } else if (classToConvert == StringType.class) {
576                 state = new StringType(data);
577
578             }
579         } catch (Exception e) {
580             logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
581         }
582
583         logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
584         return state;
585     }
586
587     private void handleNetMenuCommand(String cmdName) {
588         if ("Up".equals(cmdName)) {
589             sendCommand(EiscpCommand.NETUSB_OP_UP);
590         } else if ("Down".equals(cmdName)) {
591             sendCommand(EiscpCommand.NETUSB_OP_DOWN);
592         } else if ("Select".equals(cmdName)) {
593             sendCommand(EiscpCommand.NETUSB_OP_SELECT);
594         } else if ("PageUp".equals(cmdName)) {
595             sendCommand(EiscpCommand.NETUSB_OP_LEFT);
596         } else if ("PageDown".equals(cmdName)) {
597             sendCommand(EiscpCommand.NETUSB_OP_RIGHT);
598         } else if ("Back".equals(cmdName)) {
599             sendCommand(EiscpCommand.NETUSB_OP_RETURN);
600         } else if (cmdName.matches("Select[0-9]")) {
601             int pos = Integer.parseInt(cmdName.substring(6));
602             sendCommand(EiscpCommand.NETUSB_MENU_SELECT, new DecimalType(pos));
603         } else {
604             logger.debug("Received unknown menucommand {}", cmdName);
605         }
606     }
607
608     private void selectInput(int inputId) {
609         sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
610         currentInput = inputId;
611     }
612
613     @SuppressWarnings("unused")
614     private void onInputChanged(int newInput) {
615         currentInput = newInput;
616
617         if (newInput != NET_USB_ID) {
618             resetNetMenu();
619
620             updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
621             updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
622             updateState(CHANNEL_TITLE, UnDefType.UNDEF);
623             updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
624         }
625     }
626
627     private void updateAlbumArt(String data) {
628         onkyoAlbumArt.addFrame(data);
629
630         if (onkyoAlbumArt.isAlbumCoverReady()) {
631             try {
632                 byte[] imgData = onkyoAlbumArt.getAlbumArt();
633                 if (imgData != null && imgData.length > 0) {
634                     String mimeType = onkyoAlbumArt.getAlbumArtMimeType();
635                     if (mimeType.isEmpty()) {
636                         mimeType = guessMimeTypeFromData(imgData);
637                     }
638                     updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
639                 } else {
640                     updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
641                 }
642             } catch (IllegalArgumentException e) {
643                 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
644             }
645             onkyoAlbumArt.clearAlbumArt();
646         }
647
648         if (data.startsWith("2-")) {
649             updateState(CHANNEL_ALBUM_ART_URL, new StringType(data.substring(2, data.length())));
650         } else if (data.startsWith("n-")) {
651             updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
652         } else {
653             logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
654             updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
655         }
656     }
657
658     private void updateNetTitle(String data) {
659         // first 2 characters is service type
660         int type = Integer.parseInt(data.substring(0, 2), 16);
661         ServiceType service = ServiceType.getType(type);
662
663         String title = "";
664         if (data.length() > 21) {
665             title = data.substring(22, data.length());
666         }
667
668         updateState(CHANNEL_NET_MENU_TITLE,
669                 new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
670     }
671
672     private void updateNetMenu(String data) {
673         switch (data.charAt(0)) {
674             case 'U':
675                 String itemData = data.substring(3, data.length());
676                 switch (data.charAt(1)) {
677                     case '0':
678                         updateState(CHANNEL_NET_MENU0, new StringType(itemData));
679                         break;
680                     case '1':
681                         updateState(CHANNEL_NET_MENU1, new StringType(itemData));
682                         break;
683                     case '2':
684                         updateState(CHANNEL_NET_MENU2, new StringType(itemData));
685                         break;
686                     case '3':
687                         updateState(CHANNEL_NET_MENU3, new StringType(itemData));
688                         break;
689                     case '4':
690                         updateState(CHANNEL_NET_MENU4, new StringType(itemData));
691                         break;
692                     case '5':
693                         updateState(CHANNEL_NET_MENU5, new StringType(itemData));
694                         break;
695                     case '6':
696                         updateState(CHANNEL_NET_MENU6, new StringType(itemData));
697                         break;
698                     case '7':
699                         updateState(CHANNEL_NET_MENU7, new StringType(itemData));
700                         break;
701                     case '8':
702                         updateState(CHANNEL_NET_MENU8, new StringType(itemData));
703                         break;
704                     case '9':
705                         updateState(CHANNEL_NET_MENU9, new StringType(itemData));
706                         break;
707                 }
708                 break;
709
710             case 'C':
711                 updateMenuPosition(data);
712                 break;
713         }
714     }
715
716     private void updateMenuPosition(String data) {
717         char position = data.charAt(1);
718         int pos = Character.getNumericValue(position);
719
720         logger.debug("Updating menu position to {}", pos);
721
722         if (pos == -1) {
723             updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
724         } else {
725             updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
726         }
727
728         if (data.endsWith("P")) {
729             resetNetMenu();
730         }
731     }
732
733     private void resetNetMenu() {
734         logger.debug("Reset net menu");
735         updateState(CHANNEL_NET_MENU0, new StringType("-"));
736         updateState(CHANNEL_NET_MENU1, new StringType("-"));
737         updateState(CHANNEL_NET_MENU2, new StringType("-"));
738         updateState(CHANNEL_NET_MENU3, new StringType("-"));
739         updateState(CHANNEL_NET_MENU4, new StringType("-"));
740         updateState(CHANNEL_NET_MENU5, new StringType("-"));
741         updateState(CHANNEL_NET_MENU6, new StringType("-"));
742         updateState(CHANNEL_NET_MENU7, new StringType("-"));
743         updateState(CHANNEL_NET_MENU8, new StringType("-"));
744         updateState(CHANNEL_NET_MENU9, new StringType("-"));
745     }
746
747     private State convertNetUsbPlayStatus(String data) {
748         State state = UnDefType.UNDEF;
749         switch (data.charAt(0)) {
750             case 'P':
751                 state = PlayPauseType.PLAY;
752                 break;
753             case 'p':
754             case 'S':
755                 state = PlayPauseType.PAUSE;
756                 break;
757             case 'F':
758                 state = RewindFastforwardType.FASTFORWARD;
759                 break;
760             case 'R':
761                 state = RewindFastforwardType.REWIND;
762                 break;
763
764         }
765         return state;
766     }
767
768     public void sendRawCommand(String command, String value) {
769         if (connection != null) {
770             connection.send(command, value);
771         } else {
772             logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
773         }
774     }
775
776     private void sendCommand(EiscpCommand deviceCommand) {
777         if (connection != null) {
778             connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
779         } else {
780             logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
781         }
782     }
783
784     private void sendCommand(EiscpCommand deviceCommand, Command command) {
785         if (connection != null) {
786             final String cmd = deviceCommand.getCommand();
787             String valTemplate = deviceCommand.getValue();
788             String val;
789
790             if (command instanceof OnOffType) {
791                 val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
792
793             } else if (command instanceof StringType) {
794                 val = String.format(valTemplate, command);
795
796             } else if (command instanceof DecimalType) {
797                 val = String.format(valTemplate, ((DecimalType) command).intValue());
798
799             } else if (command instanceof PercentType) {
800                 val = String.format(valTemplate, ((DecimalType) command).intValue());
801             } else {
802                 val = valTemplate;
803             }
804
805             logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
806                     connection.getConnectionName());
807             connection.send(cmd, val);
808         } else {
809             logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
810         }
811     }
812
813     /**
814      * Check the status of the AVR.
815      *
816      * @return
817      */
818     private void checkStatus() {
819         sendCommand(EiscpCommand.POWER_QUERY);
820
821         if (connection != null && connection.isConnected()) {
822             sendCommand(EiscpCommand.VOLUME_QUERY);
823             sendCommand(EiscpCommand.SOURCE_QUERY);
824             sendCommand(EiscpCommand.MUTE_QUERY);
825             sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
826             sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
827             sendCommand(EiscpCommand.INFO_QUERY);
828             sendCommand(EiscpCommand.AUDIOINFO_QUERY);
829             sendCommand(EiscpCommand.VIDEOINFO_QUERY);
830
831             if (isChannelAvailable(CHANNEL_POWERZONE2)) {
832                 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
833                 sendCommand(EiscpCommand.ZONE2_VOLUME_QUERY);
834                 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
835                 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
836             }
837
838             if (isChannelAvailable(CHANNEL_POWERZONE3)) {
839                 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
840                 sendCommand(EiscpCommand.ZONE3_VOLUME_QUERY);
841                 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
842                 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
843             }
844         } else {
845             updateStatus(ThingStatus.OFFLINE);
846         }
847     }
848
849     private boolean isChannelAvailable(String channel) {
850         List<Channel> channels = getThing().getChannels();
851         for (Channel c : channels) {
852             if (c.getUID().getId().equals(channel)) {
853                 return true;
854             }
855         }
856         return false;
857     }
858
859     private void handleVolumeSet(EiscpCommand.Zone zone, final State currentValue, final Command command) {
860         if (command instanceof PercentType) {
861             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_SET),
862                     downScaleVolume((PercentType) command));
863         } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
864             if (currentValue instanceof PercentType) {
865                 if (((DecimalType) currentValue).intValue() < configuration.volumeLimit) {
866                     sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_UP));
867                 } else {
868                     logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
869                 }
870             }
871         } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
872             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_DOWN));
873         } else if (command.equals(OnOffType.OFF)) {
874             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
875         } else if (command.equals(OnOffType.ON)) {
876             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
877         } else if (command.equals(RefreshType.REFRESH)) {
878             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_QUERY));
879             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_QUERY));
880         }
881     }
882
883     private State handleReceivedVolume(State volume) {
884         if (volume instanceof DecimalType) {
885             return upScaleVolume(((DecimalType) volume));
886         }
887         return volume;
888     }
889
890     private PercentType upScaleVolume(DecimalType volume) {
891         PercentType newVolume = scaleVolumeFromReceiver(volume);
892
893         if (configuration.volumeLimit < 100) {
894             double scaleCoefficient = 100d / configuration.volumeLimit;
895             PercentType unLimitedVolume = newVolume;
896             newVolume = new PercentType(((Double) (newVolume.doubleValue() * scaleCoefficient)).intValue());
897             logger.debug("Up scaled volume level '{}' to '{}'", unLimitedVolume, newVolume);
898         }
899
900         return newVolume;
901     }
902
903     private DecimalType downScaleVolume(PercentType volume) {
904         PercentType limitedVolume = volume;
905
906         if (configuration.volumeLimit < 100) {
907             double scaleCoefficient = configuration.volumeLimit / 100d;
908             limitedVolume = new PercentType(((Double) (volume.doubleValue() * scaleCoefficient)).intValue());
909             logger.debug("Limited volume level '{}' to '{}'", volume, limitedVolume);
910         }
911
912         return scaleVolumeForReceiver(limitedVolume);
913     }
914
915     private DecimalType scaleVolumeForReceiver(PercentType volume) {
916         return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
917     }
918
919     private PercentType scaleVolumeFromReceiver(DecimalType volume) {
920         return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
921     }
922
923     public PercentType getVolume() throws IOException {
924         if (volumeLevelZone1 instanceof PercentType) {
925             return (PercentType) volumeLevelZone1;
926         }
927
928         throw new IOException();
929     }
930
931     public void setVolume(PercentType volume) throws IOException {
932         handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
933     }
934
935     private String guessMimeTypeFromData(byte[] data) {
936         String mimeType = HttpUtil.guessContentTypeFromData(data);
937         logger.debug("Mime type guess from content: {}", mimeType);
938         if (mimeType == null) {
939             mimeType = RawType.DEFAULT_MIME_TYPE;
940         }
941         logger.debug("Mime type: {}", mimeType);
942         return mimeType;
943     }
944
945     @Override
946     public Collection<Class<? extends ThingHandlerService>> getServices() {
947         return Collections.singletonList(OnkyoThingActions.class);
948     }
949 }