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