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