]> git.basschouten.com Git - openhab-addons.git/blob
274a4c7e9d3baa5d5d2fa794815d56480a86443b
[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             DocumentBuilder builder = factory.newDocumentBuilder();
502             try (StringReader sr = new StringReader(infoXML)) {
503                 InputSource is = new InputSource(sr);
504                 Document doc = builder.parse(is);
505
506                 NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
507                 populateInputs(selectableInputs);
508             }
509         } catch (ParserConfigurationException | SAXException | IOException e) {
510             logger.debug("Error occured during Info XML parsing.", e);
511         }
512     }
513
514     @Override
515     public void connectionError(String ip, String errorMsg) {
516         logger.debug("Connection error occurred to Onkyo Receiver @{}", ip);
517         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
518     }
519
520     private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
521         State state = UnDefType.UNDEF;
522
523         try {
524             int index;
525
526             if (data.contentEquals("N/A")) {
527                 state = UnDefType.UNDEF;
528
529             } else if (classToConvert == OnOffType.class) {
530                 index = Integer.parseInt(data, 16);
531                 state = index == 0 ? OnOffType.OFF : OnOffType.ON;
532
533             } else if (classToConvert == DecimalType.class) {
534                 index = Integer.parseInt(data, 16);
535                 state = new DecimalType(index);
536
537             } else if (classToConvert == PercentType.class) {
538                 index = Integer.parseInt(data, 16);
539                 state = new PercentType(index);
540
541             } else if (classToConvert == StringType.class) {
542                 state = new StringType(data);
543
544             }
545         } catch (Exception e) {
546             logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
547         }
548
549         logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
550         return state;
551     }
552
553     private void handleNetMenuCommand(String cmdName) {
554         if ("Up".equals(cmdName)) {
555             sendCommand(EiscpCommand.NETUSB_OP_UP);
556         } else if ("Down".equals(cmdName)) {
557             sendCommand(EiscpCommand.NETUSB_OP_DOWN);
558         } else if ("Select".equals(cmdName)) {
559             sendCommand(EiscpCommand.NETUSB_OP_SELECT);
560         } else if ("PageUp".equals(cmdName)) {
561             sendCommand(EiscpCommand.NETUSB_OP_LEFT);
562         } else if ("PageDown".equals(cmdName)) {
563             sendCommand(EiscpCommand.NETUSB_OP_RIGHT);
564         } else if ("Back".equals(cmdName)) {
565             sendCommand(EiscpCommand.NETUSB_OP_RETURN);
566         } else if (cmdName.matches("Select[0-9]")) {
567             int pos = Integer.parseInt(cmdName.substring(6));
568             sendCommand(EiscpCommand.NETUSB_MENU_SELECT, new DecimalType(pos));
569         } else {
570             logger.debug("Received unknown menucommand {}", cmdName);
571         }
572     }
573
574     private void selectInput(int inputId) {
575         sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
576         currentInput = inputId;
577     }
578
579     @SuppressWarnings("unused")
580     private void onInputChanged(int newInput) {
581         currentInput = newInput;
582
583         if (newInput != NET_USB_ID) {
584             resetNetMenu();
585
586             updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
587             updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
588             updateState(CHANNEL_TITLE, UnDefType.UNDEF);
589             updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
590         }
591     }
592
593     private void updateAlbumArt(String data) {
594         onkyoAlbumArt.addFrame(data);
595
596         if (onkyoAlbumArt.isAlbumCoverReady()) {
597             try {
598                 byte[] imgData = onkyoAlbumArt.getAlbumArt();
599                 if (imgData != null && imgData.length > 0) {
600                     String mimeType = onkyoAlbumArt.getAlbumArtMimeType();
601                     if (mimeType.isEmpty()) {
602                         mimeType = guessMimeTypeFromData(imgData);
603                     }
604                     updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
605                 } else {
606                     updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
607                 }
608             } catch (IllegalArgumentException e) {
609                 updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
610             }
611             onkyoAlbumArt.clearAlbumArt();
612         }
613
614         if (data.startsWith("2-")) {
615             updateState(CHANNEL_ALBUM_ART_URL, new StringType(data.substring(2, data.length())));
616         } else if (data.startsWith("n-")) {
617             updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
618         } else {
619             logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
620             updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
621         }
622     }
623
624     private void updateNetTitle(String data) {
625         // first 2 characters is service type
626         int type = Integer.parseInt(data.substring(0, 2), 16);
627         ServiceType service = ServiceType.getType(type);
628
629         String title = "";
630         if (data.length() > 21) {
631             title = data.substring(22, data.length());
632         }
633
634         updateState(CHANNEL_NET_MENU_TITLE,
635                 new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
636     }
637
638     private void updateNetMenu(String data) {
639         switch (data.charAt(0)) {
640             case 'U':
641                 String itemData = data.substring(3, data.length());
642                 switch (data.charAt(1)) {
643                     case '0':
644                         updateState(CHANNEL_NET_MENU0, new StringType(itemData));
645                         break;
646                     case '1':
647                         updateState(CHANNEL_NET_MENU1, new StringType(itemData));
648                         break;
649                     case '2':
650                         updateState(CHANNEL_NET_MENU2, new StringType(itemData));
651                         break;
652                     case '3':
653                         updateState(CHANNEL_NET_MENU3, new StringType(itemData));
654                         break;
655                     case '4':
656                         updateState(CHANNEL_NET_MENU4, new StringType(itemData));
657                         break;
658                     case '5':
659                         updateState(CHANNEL_NET_MENU5, new StringType(itemData));
660                         break;
661                     case '6':
662                         updateState(CHANNEL_NET_MENU6, new StringType(itemData));
663                         break;
664                     case '7':
665                         updateState(CHANNEL_NET_MENU7, new StringType(itemData));
666                         break;
667                     case '8':
668                         updateState(CHANNEL_NET_MENU8, new StringType(itemData));
669                         break;
670                     case '9':
671                         updateState(CHANNEL_NET_MENU9, new StringType(itemData));
672                         break;
673                 }
674                 break;
675
676             case 'C':
677                 updateMenuPosition(data);
678                 break;
679         }
680     }
681
682     private void updateMenuPosition(String data) {
683         char position = data.charAt(1);
684         int pos = Character.getNumericValue(position);
685
686         logger.debug("Updating menu position to {}", pos);
687
688         if (pos == -1) {
689             updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
690         } else {
691             updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
692         }
693
694         if (data.endsWith("P")) {
695             resetNetMenu();
696         }
697     }
698
699     private void resetNetMenu() {
700         logger.debug("Reset net menu");
701         updateState(CHANNEL_NET_MENU0, new StringType("-"));
702         updateState(CHANNEL_NET_MENU1, new StringType("-"));
703         updateState(CHANNEL_NET_MENU2, new StringType("-"));
704         updateState(CHANNEL_NET_MENU3, new StringType("-"));
705         updateState(CHANNEL_NET_MENU4, new StringType("-"));
706         updateState(CHANNEL_NET_MENU5, new StringType("-"));
707         updateState(CHANNEL_NET_MENU6, new StringType("-"));
708         updateState(CHANNEL_NET_MENU7, new StringType("-"));
709         updateState(CHANNEL_NET_MENU8, new StringType("-"));
710         updateState(CHANNEL_NET_MENU9, new StringType("-"));
711     }
712
713     private State convertNetUsbPlayStatus(String data) {
714         State state = UnDefType.UNDEF;
715         switch (data.charAt(0)) {
716             case 'P':
717                 state = PlayPauseType.PLAY;
718                 break;
719             case 'p':
720             case 'S':
721                 state = PlayPauseType.PAUSE;
722                 break;
723             case 'F':
724                 state = RewindFastforwardType.FASTFORWARD;
725                 break;
726             case 'R':
727                 state = RewindFastforwardType.REWIND;
728                 break;
729
730         }
731         return state;
732     }
733
734     public void sendRawCommand(String command, String value) {
735         if (connection != null) {
736             connection.send(command, value);
737         } else {
738             logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
739         }
740     }
741
742     private void sendCommand(EiscpCommand deviceCommand) {
743         if (connection != null) {
744             connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
745         } else {
746             logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
747         }
748     }
749
750     private void sendCommand(EiscpCommand deviceCommand, Command command) {
751         if (connection != null) {
752             final String cmd = deviceCommand.getCommand();
753             String valTemplate = deviceCommand.getValue();
754             String val;
755
756             if (command instanceof OnOffType) {
757                 val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
758
759             } else if (command instanceof StringType) {
760                 val = String.format(valTemplate, command);
761
762             } else if (command instanceof DecimalType) {
763                 val = String.format(valTemplate, ((DecimalType) command).intValue());
764
765             } else if (command instanceof PercentType) {
766                 val = String.format(valTemplate, ((DecimalType) command).intValue());
767             } else {
768                 val = valTemplate;
769             }
770
771             logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
772                     connection.getConnectionName());
773             connection.send(cmd, val);
774         } else {
775             logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
776         }
777     }
778
779     /**
780      * Check the status of the AVR.
781      *
782      * @return
783      */
784     private void checkStatus() {
785         sendCommand(EiscpCommand.POWER_QUERY);
786
787         if (connection != null && connection.isConnected()) {
788             sendCommand(EiscpCommand.VOLUME_QUERY);
789             sendCommand(EiscpCommand.SOURCE_QUERY);
790             sendCommand(EiscpCommand.MUTE_QUERY);
791             sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
792             sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
793             sendCommand(EiscpCommand.INFO_QUERY);
794
795             if (isChannelAvailable(CHANNEL_POWERZONE2)) {
796                 sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
797                 sendCommand(EiscpCommand.ZONE2_VOLUME_QUERY);
798                 sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
799                 sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
800             }
801
802             if (isChannelAvailable(CHANNEL_POWERZONE3)) {
803                 sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
804                 sendCommand(EiscpCommand.ZONE3_VOLUME_QUERY);
805                 sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
806                 sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
807             }
808         } else {
809             updateStatus(ThingStatus.OFFLINE);
810         }
811     }
812
813     private boolean isChannelAvailable(String channel) {
814         List<Channel> channels = getThing().getChannels();
815         for (Channel c : channels) {
816             if (c.getUID().getId().equals(channel)) {
817                 return true;
818             }
819         }
820         return false;
821     }
822
823     private void handleVolumeSet(EiscpCommand.Zone zone, final State currentValue, final Command command) {
824         if (command instanceof PercentType) {
825             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_SET),
826                     downScaleVolume((PercentType) command));
827         } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
828             if (currentValue instanceof PercentType) {
829                 if (((DecimalType) currentValue).intValue() < configuration.volumeLimit) {
830                     sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_UP));
831                 } else {
832                     logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
833                 }
834             }
835         } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
836             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_DOWN));
837         } else if (command.equals(OnOffType.OFF)) {
838             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
839         } else if (command.equals(OnOffType.ON)) {
840             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
841         } else if (command.equals(RefreshType.REFRESH)) {
842             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_QUERY));
843             sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_QUERY));
844         }
845     }
846
847     private State handleReceivedVolume(State volume) {
848         if (volume instanceof DecimalType) {
849             return upScaleVolume(((DecimalType) volume));
850         }
851         return volume;
852     }
853
854     private PercentType upScaleVolume(DecimalType volume) {
855         PercentType newVolume = scaleVolumeFromReceiver(volume);
856
857         if (configuration.volumeLimit < 100) {
858             double scaleCoefficient = 100d / configuration.volumeLimit;
859             PercentType unLimitedVolume = newVolume;
860             newVolume = new PercentType(((Double) (newVolume.doubleValue() * scaleCoefficient)).intValue());
861             logger.debug("Up scaled volume level '{}' to '{}'", unLimitedVolume, newVolume);
862         }
863
864         return newVolume;
865     }
866
867     private DecimalType downScaleVolume(PercentType volume) {
868         PercentType limitedVolume = volume;
869
870         if (configuration.volumeLimit < 100) {
871             double scaleCoefficient = configuration.volumeLimit / 100d;
872             limitedVolume = new PercentType(((Double) (volume.doubleValue() * scaleCoefficient)).intValue());
873             logger.debug("Limited volume level '{}' to '{}'", volume, limitedVolume);
874         }
875
876         return scaleVolumeForReceiver(limitedVolume);
877     }
878
879     private DecimalType scaleVolumeForReceiver(PercentType volume) {
880         return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
881     }
882
883     private PercentType scaleVolumeFromReceiver(DecimalType volume) {
884         return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
885     }
886
887     @Override
888     public PercentType getVolume() throws IOException {
889         if (volumeLevelZone1 instanceof PercentType) {
890             return (PercentType) volumeLevelZone1;
891         }
892
893         throw new IOException();
894     }
895
896     @Override
897     public void setVolume(PercentType volume) throws IOException {
898         handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
899     }
900
901     private String guessMimeTypeFromData(byte[] data) {
902         String mimeType = HttpUtil.guessContentTypeFromData(data);
903         logger.debug("Mime type guess from content: {}", mimeType);
904         if (mimeType == null) {
905             mimeType = RawType.DEFAULT_MIME_TYPE;
906         }
907         logger.debug("Mime type: {}", mimeType);
908         return mimeType;
909     }
910
911     @Override
912     public Collection<Class<? extends ThingHandlerService>> getServices() {
913         return Collections.singletonList(OnkyoThingActions.class);
914     }
915 }