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