added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.denonmarantz</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,123 @@
# Denon / Marantz Binding
This binding integrates Denon & Marantz AV receivers by using either Telnet or a (undocumented) HTTP API.
## Supported Things
This binding supports Denon and Marantz receivers having a Telnet interface or a web based controller at `http://<AVR IP address>/`.
The thing type for all of them is `avr`.
Tested models: Marantz SR5008, Denon AVR-X2000 / X3000 / X1200W / X2100W / X2200W / X3100W / X3300W
Denon models with HEOS support (`AVR-X..00H`) do not support the HTTP API. They do support Telnet.
During Discovery this is auto-detected and configured.
## Discovery
This binding can discover Denon and Marantz receivers using mDNS.
The serial number (which is the MAC address of the network interface) is used as unique identifier.
It tries to detect the number of zones (when the AVR responds to HTTP).
It defaults to 2 zones.
## Thing Configuration
The DenonMarantz AVR thing requires the `host` it can connect to.
There are more parameters which all have defaults set.
| Parameter | Values | Default |
|---------------------|-------------------------------------------|---------|
| host | hostname / IP address of the AVR | - |
| zoneCount | [1, 2, 3 or 4] | 2 |
| telnetEnabled | true, false | false |
| telnetPort | port number, e.g. 23 | 23 |
| httpPort | port number, e.g. 80 | 80 |
| httpPollingInterval | polling interval in seconds (minimal 5) | 5 |
## Channels
The DenonMarantz AVR supports the following channels (some channels are model specific):
| Channel Type ID | Item Type | Description |
|-------------------------|--------------|--------------|
| *General* | |
| general#power | Switch (RW) | Power on/off
| general#surroundProgram | String (R) | current surround program (e.g. STEREO)
| general#artist | String (R) | artist of current track
| general#album | String (R) | album of current track
| general#track | String (R) | title of current track
| general#command | String (W) | Command to send to the AVR (for use in Rules)
| *Main zone* | |
| mainZone#power | Switch (RW) | Main zone power on/off
| mainZone#volume | Dimmer (RW) | Main zone volume
| mainZone#volumeDB | Number (RW) | Main zone volume in dB (-80 offset)
| mainZone#mute | Switch (RW) | Main zone mute
| mainZone#input | String (RW) | Main zone input (e.g. TV, TUNER, ..)
| *Zone 2* | |
| zone2#power | Switch (RW) | Zone 2 power on/off
| zone2#volume | Dimmer (RW) | Zone 2 volume
| zone2#volumeDB | Number (RW) | Zone 2 volume in dB (-80 offset)
| zone2#mute | Switch (RW) | Zone 2 mute
| zone2#input | String (RW) | Zone 2 input
| *Zone 3* | |
| zone3#power | Switch (RW) | Zone 3 power on/off
| zone3#volume | Dimmer (RW) | Zone 3 volume
| zone3#volumeDB | Number (RW) | Zone 3 volume in dB (-80 offset)
| zone3#mute | Switch (RW) | Zone 3 mute
| zone3#input | String (RW) | Zone 3 input
| *Zone 4* | |
| zone4#power | Switch (RW) | Zone 4 power on/off
| zone4#volume | Dimmer (RW) | Zone 4 volume
| zone4#volumeDB | Number (RW) | Zone 4 volume in dB (-80 offset)
| zone4#mute | Switch (RW) | Zone 4 mute
| zone4#input | String (RW) | Zone 4 input
(R) = read-only (no updates possible)
(RW) = read-write
(W) = write-only (no feedback)
## Full Example
`.things` file:
```
Thing denonmarantz:avr:1 "Receiver" @ "Living room" [host="192.168.1.100"]
```
`.items` file:
```
Switch marantz_power "Receiver" <switch> {channel="denonmarantz:avr:1:general#power"}
Dimmer marantz_volume "Volume" <soundvolume> {channel="denonmarantz:avr:1:mainZone#volume"}
Number marantz_volumeDB "Volume [%.1f dB]" {channel="denonmarantz:avr:1:mainzone#volume"}
Switch marantz_mute "Mute" <mute> {channel="denonmarantz:avr:1:mainZone#mute"}
Switch marantz_z2power "Zone 2" {channel="denonmarantz:avr:1:zone2#power"}
String marantz_input "Input [%s]" {channel="denonmarantz:avr:1:mainZone#input" }
String marantz_surround "Surround: [%s]" {channel="denonmarantz:avr:1:general#surroundProgram"}
String marantz_command {channel="denonmarantz:avr:1:general#command"}
```
`.sitemap` file:
```
...
Group item=marantz_input label="Receiver" icon="receiver" {
Default item=marantz_power
Default item=marantz_mute visibility=[marantz_power==ON]
Setpoint item=marantz_volume label="Volume [%.1f]" minValue=0 maxValue=40 step=0.5 visibility=[marantz_power==ON]
Default item=marantz_volumeDB visibility=[marantz_power==ON]
Selection item=marantz_input mappings=[TV=TV,MPLAY=Kodi] visibility=[marantz_power==ON]
Default item=marantz_surround visibility=[marantz_power==ON]
}
...
```
## Control Protocol Reference
These resources can be useful to learn what to send using the `command`channel:
- [AVR-X2000/E400](http://www2.aerne.com/Public/dok-sw.nsf/0c6187bc750a16fcc1256e3c005a9740/96a2ba120706d10dc1257bdd0033493f/$FILE/AVRX2000_E400_PROTOCOL(10.1.0)_V04.pdf)
- [AVR-X4000](https://usa.denon.com/us/product/hometheater/receivers/avrx4000?docname=AVRX4000_PROTOCOL(10%203%200)_V03.pdf)
- [AVR-3311CI/AVR-3311/AVR-991](https://www.awe-europe.com/documents/Control%20Docs/Denon/Archive/AVR3311CI_AVR3311_991_PROTOCOL_V7.1.0.pdf)
- [CEOL Piccolo DRA-N5/RCD-N8](http://www.audioproducts.com.au/downloadcenter/products/Denon/CEOLPICCOLOBK/Manuals/DRAN5_RCDN8_PROTOCOL_V.1.0.0.pdf)
- [Marantz Control Protocol (2014+)](http://m.us.marantz.com/DocumentMaster/US/Marantz%202014%20NR%20Series%20-%20SR%20Series%20RS232%20IP%20Protocol.xls)

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.denonmarantz</artifactId>
<name>openHAB Add-ons :: Bundles :: Denon / Marantz Binding</name>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.denonmarantz-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-denonmarantz" description="Denon / Marantz Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<feature dependency="true">openhab.tp-jaxb</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.denonmarantz/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link DenonMarantzBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
public class DenonMarantzBindingConstants {
public static final String BINDING_ID = "denonmarantz";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AVR = new ThingTypeUID(BINDING_ID, "avr");
// List of thing Parameters names
public static final String PARAMETER_ZONE_COUNT = "zoneCount";
public static final String PARAMETER_HOST = "host";
public static final String PARAMETER_TELNET_ENABLED = "telnetEnabled";
public static final String PARAMETER_TELNET_PORT = "telnetPort";
public static final String PARAMETER_HTTP_PORT = "httpPort";
public static final String PARAMETER_POLLING_INTERVAL = "httpPollingInterval";
// List of all Channel ids
public static final String CHANNEL_POWER = "general#power";
public static final String CHANNEL_SURROUND_PROGRAM = "general#surroundProgram";
public static final String CHANNEL_COMMAND = "general#command";
public static final String CHANNEL_NOW_PLAYING_ARTIST = "general#artist";
public static final String CHANNEL_NOW_PLAYING_ALBUM = "general#album";
public static final String CHANNEL_NOW_PLAYING_TRACK = "general#track";
public static final String CHANNEL_MAIN_ZONE_POWER = "mainZone#power";
public static final String CHANNEL_MAIN_VOLUME = "mainZone#volume";
public static final String CHANNEL_MAIN_VOLUME_DB = "mainZone#volumeDB";
public static final String CHANNEL_MUTE = "mainZone#mute";
public static final String CHANNEL_INPUT = "mainZone#input";
public static final String CHANNEL_ZONE2_POWER = "zone2#power";
public static final String CHANNEL_ZONE2_VOLUME = "zone2#volume";
public static final String CHANNEL_ZONE2_VOLUME_DB = "zone2#volumeDB";
public static final String CHANNEL_ZONE2_MUTE = "zone2#mute";
public static final String CHANNEL_ZONE2_INPUT = "zone2#input";
public static final String CHANNEL_ZONE3_POWER = "zone3#power";
public static final String CHANNEL_ZONE3_VOLUME = "zone3#volume";
public static final String CHANNEL_ZONE3_VOLUME_DB = "zone3#volumeDB";
public static final String CHANNEL_ZONE3_MUTE = "zone3#mute";
public static final String CHANNEL_ZONE3_INPUT = "zone3#input";
public static final String CHANNEL_ZONE4_POWER = "zone4#power";
public static final String CHANNEL_ZONE4_VOLUME = "zone4#volume";
public static final String CHANNEL_ZONE4_VOLUME_DB = "zone4#volumeDB";
public static final String CHANNEL_ZONE4_MUTE = "zone4#mute";
public static final String CHANNEL_ZONE4_INPUT = "zone4#input";
// Map of Zone2 Channel Type UIDs (to be added to Thing later when needed)
public static final Map<String, ChannelTypeUID> ZONE2_CHANNEL_TYPES = new LinkedHashMap<>();
static {
ZONE2_CHANNEL_TYPES.put(CHANNEL_ZONE2_POWER, new ChannelTypeUID(BINDING_ID, "zonePower"));
ZONE2_CHANNEL_TYPES.put(CHANNEL_ZONE2_VOLUME, new ChannelTypeUID(BINDING_ID, "volume"));
ZONE2_CHANNEL_TYPES.put(CHANNEL_ZONE2_VOLUME_DB, new ChannelTypeUID(BINDING_ID, "volumeDB"));
ZONE2_CHANNEL_TYPES.put(CHANNEL_ZONE2_MUTE, new ChannelTypeUID(BINDING_ID, "mute"));
ZONE2_CHANNEL_TYPES.put(CHANNEL_ZONE2_INPUT, new ChannelTypeUID(BINDING_ID, "input"));
}
// Map of Zone3 Channel Type UIDs (to be added to Thing later when needed)
public static final Map<String, ChannelTypeUID> ZONE3_CHANNEL_TYPES = new LinkedHashMap<>();
static {
ZONE3_CHANNEL_TYPES.put(CHANNEL_ZONE3_POWER, new ChannelTypeUID(BINDING_ID, "zonePower"));
ZONE3_CHANNEL_TYPES.put(CHANNEL_ZONE3_VOLUME, new ChannelTypeUID(BINDING_ID, "volume"));
ZONE3_CHANNEL_TYPES.put(CHANNEL_ZONE3_VOLUME_DB, new ChannelTypeUID(BINDING_ID, "volumeDB"));
ZONE3_CHANNEL_TYPES.put(CHANNEL_ZONE3_MUTE, new ChannelTypeUID(BINDING_ID, "mute"));
ZONE3_CHANNEL_TYPES.put(CHANNEL_ZONE3_INPUT, new ChannelTypeUID(BINDING_ID, "input"));
}
// Map of Zone4 Channel Type UIDs (to be added to Thing later when needed)
public static final Map<String, ChannelTypeUID> ZONE4_CHANNEL_TYPES = new LinkedHashMap<>();
static {
ZONE4_CHANNEL_TYPES.put(CHANNEL_ZONE4_POWER, new ChannelTypeUID(BINDING_ID, "zonePower"));
ZONE4_CHANNEL_TYPES.put(CHANNEL_ZONE4_VOLUME, new ChannelTypeUID(BINDING_ID, "volume"));
ZONE4_CHANNEL_TYPES.put(CHANNEL_ZONE4_VOLUME_DB, new ChannelTypeUID(BINDING_ID, "volumeDB"));
ZONE4_CHANNEL_TYPES.put(CHANNEL_ZONE4_MUTE, new ChannelTypeUID(BINDING_ID, "mute"));
ZONE4_CHANNEL_TYPES.put(CHANNEL_ZONE4_INPUT, new ChannelTypeUID(BINDING_ID, "input"));
}
/**
* Static mapping of ChannelType-to-ItemType (workaround while waiting for
* https://github.com/eclipse/smarthome/issues/4950 as yet there is no convenient way to extract the item type from
* thing-types.xml)
* See https://github.com/eclipse/smarthome/pull/4787#issuecomment-362287430
*/
public static final Map<String, String> CHANNEL_ITEM_TYPES = new HashMap<>();
static {
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE2_POWER, "Switch");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE2_VOLUME, "Dimmer");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE2_VOLUME_DB, "Number");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE2_MUTE, "Switch");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE2_INPUT, "String");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE3_POWER, "Switch");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE3_VOLUME, "Dimmer");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE3_VOLUME_DB, "Number");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE3_MUTE, "Switch");
CHANNEL_ITEM_TYPES.put(CHANNEL_ZONE3_INPUT, "String");
}
// Offset in dB from the actual dB value to the volume as presented by the AVR (0 == -80 dB)
public static final BigDecimal DB_OFFSET = new BigDecimal("80");
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal;
import static org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants.THING_TYPE_AVR;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.denonmarantz.internal.handler.DenonMarantzHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link DenonMarantzHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.denonmarantz")
@NonNullByDefault
public class DenonMarantzHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AVR);
private final HttpClient httpClient;
@Activate
public DenonMarantzHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_AVR)) {
return new DenonMarantzHandler(thing, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,317 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal;
import java.math.BigDecimal;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
/**
* Represents the state of the handled DenonMarantz AVR
*
* @author Jan-Willem Veldhuis - Initial contribution
*
*/
public class DenonMarantzState {
private State power;
private State mainZonePower;
private State mute;
private State mainVolume;
private State mainVolumeDB;
private State input;
private State surroundProgram;
private State artist;
private State album;
private State track;
// ------ Zones ------
private State zone2Power;
private State zone2Volume;
private State zone2VolumeDB;
private State zone2Mute;
private State zone2Input;
private State zone3Power;
private State zone3Volume;
private State zone3VolumeDB;
private State zone3Mute;
private State zone3Input;
private State zone4Power;
private State zone4Volume;
private State zone4VolumeDB;
private State zone4Mute;
private State zone4Input;
private DenonMarantzStateChangedListener handler;
public DenonMarantzState(DenonMarantzStateChangedListener handler) {
this.handler = handler;
}
public void connectionError(String errorMessage) {
handler.connectionError(errorMessage);
}
public State getStateForChannelID(String channelID) {
switch (channelID) {
case DenonMarantzBindingConstants.CHANNEL_POWER:
return power;
case DenonMarantzBindingConstants.CHANNEL_MAIN_ZONE_POWER:
return mainZonePower;
case DenonMarantzBindingConstants.CHANNEL_MUTE:
return mute;
case DenonMarantzBindingConstants.CHANNEL_MAIN_VOLUME:
return mainVolume;
case DenonMarantzBindingConstants.CHANNEL_MAIN_VOLUME_DB:
return mainVolumeDB;
case DenonMarantzBindingConstants.CHANNEL_INPUT:
return input;
case DenonMarantzBindingConstants.CHANNEL_SURROUND_PROGRAM:
return surroundProgram;
case DenonMarantzBindingConstants.CHANNEL_NOW_PLAYING_ARTIST:
return artist;
case DenonMarantzBindingConstants.CHANNEL_NOW_PLAYING_ALBUM:
return album;
case DenonMarantzBindingConstants.CHANNEL_NOW_PLAYING_TRACK:
return track;
case DenonMarantzBindingConstants.CHANNEL_ZONE2_POWER:
return zone2Power;
case DenonMarantzBindingConstants.CHANNEL_ZONE2_VOLUME:
return zone2Volume;
case DenonMarantzBindingConstants.CHANNEL_ZONE2_VOLUME_DB:
return zone2VolumeDB;
case DenonMarantzBindingConstants.CHANNEL_ZONE2_MUTE:
return zone2Mute;
case DenonMarantzBindingConstants.CHANNEL_ZONE2_INPUT:
return zone2Input;
case DenonMarantzBindingConstants.CHANNEL_ZONE3_POWER:
return zone3Power;
case DenonMarantzBindingConstants.CHANNEL_ZONE3_VOLUME:
return zone3Volume;
case DenonMarantzBindingConstants.CHANNEL_ZONE3_VOLUME_DB:
return zone3VolumeDB;
case DenonMarantzBindingConstants.CHANNEL_ZONE3_MUTE:
return zone3Mute;
case DenonMarantzBindingConstants.CHANNEL_ZONE3_INPUT:
return zone3Input;
case DenonMarantzBindingConstants.CHANNEL_ZONE4_POWER:
return zone4Power;
case DenonMarantzBindingConstants.CHANNEL_ZONE4_VOLUME:
return zone4Volume;
case DenonMarantzBindingConstants.CHANNEL_ZONE4_VOLUME_DB:
return zone4VolumeDB;
case DenonMarantzBindingConstants.CHANNEL_ZONE4_MUTE:
return zone4Mute;
case DenonMarantzBindingConstants.CHANNEL_ZONE4_INPUT:
return zone4Input;
default:
return null;
}
}
public void setPower(boolean power) {
OnOffType newVal = power ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.power) {
this.power = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_POWER, this.power);
}
}
public void setMainZonePower(boolean mainPower) {
OnOffType newVal = mainPower ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.mainZonePower) {
this.mainZonePower = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_MAIN_ZONE_POWER, this.mainZonePower);
}
}
public void setMute(boolean mute) {
OnOffType newVal = mute ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.mute) {
this.mute = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_MUTE, this.mute);
}
}
public void setMainVolume(BigDecimal volume) {
PercentType newVal = new PercentType(volume);
if (!newVal.equals(this.mainVolume)) {
this.mainVolume = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_MAIN_VOLUME, this.mainVolume);
// update the main volume in dB too
this.mainVolumeDB = DecimalType.valueOf(volume.subtract(DenonMarantzBindingConstants.DB_OFFSET).toString());
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_MAIN_VOLUME_DB, this.mainVolumeDB);
}
}
public void setInput(String input) {
StringType newVal = StringType.valueOf(input);
if (!newVal.equals(this.input)) {
this.input = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_INPUT, this.input);
}
}
public void setSurroundProgram(String surroundProgram) {
StringType newVal = StringType.valueOf(surroundProgram);
if (!newVal.equals(this.surroundProgram)) {
this.surroundProgram = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_SURROUND_PROGRAM, this.surroundProgram);
}
}
public void setNowPlayingArtist(String artist) {
StringType newVal = StringUtils.isBlank(artist) ? StringType.EMPTY : StringType.valueOf(artist);
if (!newVal.equals(this.artist)) {
this.artist = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_NOW_PLAYING_ARTIST, this.artist);
}
}
public void setNowPlayingAlbum(String album) {
StringType newVal = StringUtils.isBlank(album) ? StringType.EMPTY : StringType.valueOf(album);
if (!newVal.equals(this.album)) {
this.album = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_NOW_PLAYING_ALBUM, this.album);
}
}
public void setNowPlayingTrack(String track) {
StringType newVal = StringUtils.isBlank(track) ? StringType.EMPTY : StringType.valueOf(track);
if (!newVal.equals(this.track)) {
this.track = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_NOW_PLAYING_TRACK, this.track);
}
}
public void setZone2Power(boolean power) {
OnOffType newVal = power ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.zone2Power) {
this.zone2Power = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE2_POWER, this.zone2Power);
}
}
public void setZone2Volume(BigDecimal volume) {
PercentType newVal = new PercentType(volume);
if (!newVal.equals(this.zone2Volume)) {
this.zone2Volume = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE2_VOLUME, this.zone2Volume);
// update the volume in dB too
this.zone2VolumeDB = DecimalType
.valueOf(volume.subtract(DenonMarantzBindingConstants.DB_OFFSET).toString());
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE2_VOLUME_DB, this.zone2VolumeDB);
}
}
public void setZone2Mute(boolean mute) {
OnOffType newVal = mute ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.zone2Mute) {
this.zone2Mute = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE2_MUTE, this.zone2Mute);
}
}
public void setZone2Input(String zone2Input) {
StringType newVal = StringType.valueOf(zone2Input);
if (!newVal.equals(this.zone2Input)) {
this.zone2Input = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE2_INPUT, this.zone2Input);
}
}
public void setZone3Power(boolean power) {
OnOffType newVal = power ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.zone3Power) {
this.zone3Power = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE3_POWER, this.zone3Power);
}
}
public void setZone3Volume(BigDecimal volume) {
PercentType newVal = new PercentType(volume);
if (!newVal.equals(this.zone3Volume)) {
this.zone3Volume = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE3_VOLUME, this.zone3Volume);
// update the volume in dB too
this.zone3VolumeDB = DecimalType
.valueOf(volume.subtract(DenonMarantzBindingConstants.DB_OFFSET).toString());
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE3_VOLUME_DB, this.zone3VolumeDB);
}
}
public void setZone3Mute(boolean mute) {
OnOffType newVal = mute ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.zone3Mute) {
this.zone3Mute = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE3_MUTE, this.zone3Mute);
}
}
public void setZone3Input(String zone3Input) {
StringType newVal = StringType.valueOf(zone3Input);
if (!newVal.equals(this.zone3Input)) {
this.zone3Input = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE2_INPUT, this.zone3Input);
}
}
public void setZone4Power(boolean power) {
OnOffType newVal = power ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.zone4Power) {
this.zone4Power = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE4_POWER, this.zone4Power);
}
}
public void setZone4Volume(BigDecimal volume) {
PercentType newVal = new PercentType(volume);
if (!newVal.equals(this.zone4Volume)) {
this.zone4Volume = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE4_VOLUME, this.zone4Volume);
// update the volume in dB too
this.zone4VolumeDB = DecimalType
.valueOf(volume.subtract(DenonMarantzBindingConstants.DB_OFFSET).toString());
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE4_VOLUME_DB, this.zone4VolumeDB);
}
}
public void setZone4Mute(boolean mute) {
OnOffType newVal = mute ? OnOffType.ON : OnOffType.OFF;
if (newVal != this.zone4Mute) {
this.zone4Mute = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE4_MUTE, this.zone4Mute);
}
}
public void setZone4Input(String zone4Input) {
StringType newVal = StringType.valueOf(zone4Input);
if (!newVal.equals(this.zone4Input)) {
this.zone4Input = newVal;
handler.stateChanged(DenonMarantzBindingConstants.CHANNEL_ZONE4_INPUT, this.zone4Input);
}
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal;
import org.openhab.binding.denonmarantz.internal.handler.DenonMarantzHandler;
import org.openhab.core.types.State;
/**
* Interface to notify the {@link DenonMarantzHandler} about state changes.
*
* @author Jan-Willem Veldhuis - Initial contribution
*
*/
public interface DenonMarantzStateChangedListener {
/**
* Update was received.
*
* @param channelID the channel for which its state changed
* @param state the new state of the channel
*/
void stateChanged(String channelID, State state);
/**
* A connection error occurred
*
* @param errorMessage the error message
*/
void connectionError(String errorMessage);
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal;
/**
* Exception thrown when an unsupported command type is sent to a channel.
*
* @author Jan-Willem Veldhuis - Initial contribution
*
*/
public class UnsupportedCommandTypeException extends Exception {
private static final long serialVersionUID = 42L;
public UnsupportedCommandTypeException() {
super();
}
public UnsupportedCommandTypeException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.config;
import java.math.BigDecimal;
import java.util.List;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
/**
* Configuration class for the Denon Marantz binding.
*
* @author Jan-Willem Veldhuis - Initial contribution
*
*/
public class DenonMarantzConfiguration {
/**
* The hostname (or IP Address) of the Denon Marantz AVR
*/
public String host;
/**
* Whether Telnet communication is enabled
*/
public Boolean telnetEnabled;
/**
* The telnet port
*/
public Integer telnetPort;
/**
* The HTTP port
*/
public Integer httpPort;
/**
* The interval to poll the AVR over HTTP for changes
*/
public Integer httpPollingInterval;
// Default maximum volume
public static final BigDecimal MAX_VOLUME = new BigDecimal("98");
private DenonMarantzConnector connector;
private Integer zoneCount;
private BigDecimal mainVolumeMax = MAX_VOLUME;
public List<String> inputOptions;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Boolean isTelnet() {
return telnetEnabled;
}
public void setTelnet(boolean telnet) {
this.telnetEnabled = telnet;
}
public Integer getTelnetPort() {
return telnetPort;
}
public void setTelnetPort(Integer telnetPort) {
this.telnetPort = telnetPort;
}
public Integer getHttpPort() {
return httpPort;
}
public void setHttpPort(Integer httpPort) {
this.httpPort = httpPort;
}
public DenonMarantzConnector getConnector() {
return connector;
}
public void setConnector(DenonMarantzConnector connector) {
this.connector = connector;
}
public BigDecimal getMainVolumeMax() {
return mainVolumeMax;
}
public void setMainVolumeMax(BigDecimal mainVolumeMax) {
this.mainVolumeMax = mainVolumeMax;
}
public Integer getZoneCount() {
return zoneCount;
}
public void setZoneCount(Integer count) {
Integer zoneCount = count;
this.zoneCount = zoneCount;
}
}

View File

@@ -0,0 +1,208 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.connector;
import static org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants.DB_OFFSET;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.concurrent.ScheduledExecutorService;
import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
import org.openhab.binding.denonmarantz.internal.UnsupportedCommandTypeException;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* Abstract class containing common functionality for the connectors.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
public abstract class DenonMarantzConnector {
private static final BigDecimal POINTFIVE = new BigDecimal("0.5");
protected ScheduledExecutorService scheduler;
protected DenonMarantzState state;
protected DenonMarantzConfiguration config;
public abstract void connect();
public abstract void dispose();
protected abstract void internalSendCommand(String command);
public void sendCustomCommand(Command command) throws UnsupportedCommandTypeException {
String cmd;
if (command instanceof StringType) {
cmd = command.toString();
} else {
throw new UnsupportedCommandTypeException();
}
internalSendCommand(cmd);
}
public void sendInputCommand(Command command, int zone) throws UnsupportedCommandTypeException {
String zonePrefix;
switch (zone) {
case 1:
zonePrefix = "SI";
break;
case 2:
case 3:
case 4:
zonePrefix = "Z" + zone;
break;
default:
throw new UnsupportedCommandTypeException("Zone must be in range [1-4], zone: " + zone);
}
String cmd = zonePrefix;
if (command instanceof StringType) {
cmd += command.toString();
} else if (command instanceof RefreshType) {
cmd += "?";
} else {
throw new UnsupportedCommandTypeException();
}
internalSendCommand(cmd);
}
public void sendSurroundProgramCommand(Command command) throws UnsupportedCommandTypeException {
String cmd = "MS";
if (command instanceof RefreshType) {
cmd += "?";
} else {
throw new UnsupportedCommandTypeException();
}
internalSendCommand(cmd);
}
public void sendMuteCommand(Command command, int zone) throws UnsupportedCommandTypeException {
if (zone < 1 || zone > 4) {
throw new UnsupportedCommandTypeException("Zone must be in range [1-4], zone: " + zone);
}
StringBuilder sb = new StringBuilder();
if (zone != 1) {
sb.append("Z").append(zone);
}
sb.append("MU");
String cmd = sb.toString();
if (command == OnOffType.ON) {
cmd += "ON";
} else if (command == OnOffType.OFF) {
cmd += "OFF";
} else if (command instanceof RefreshType) {
cmd += "?";
} else {
throw new UnsupportedCommandTypeException();
}
internalSendCommand(cmd);
}
public void sendPowerCommand(Command command, int zone) throws UnsupportedCommandTypeException {
String zonePrefix;
switch (zone) {
case 0:
zonePrefix = "PW";
break;
case 1:
zonePrefix = "ZM";
break;
case 2:
case 3:
case 4:
zonePrefix = "Z" + zone;
break;
default:
throw new UnsupportedCommandTypeException("Zone must be in range [0-4], zone: " + zone);
}
String cmd = zonePrefix;
if (command == OnOffType.ON) {
cmd += "ON";
} else if (command == OnOffType.OFF) {
cmd += (zone == 0) ? "STANDBY" : "OFF";
} else if (command instanceof RefreshType) {
cmd += "?";
} else {
throw new UnsupportedCommandTypeException();
}
internalSendCommand(cmd);
}
public void sendVolumeCommand(Command command, int zone) throws UnsupportedCommandTypeException {
String zonePrefix;
switch (zone) {
case 1:
zonePrefix = "MV";
break;
case 2:
case 3:
case 4:
zonePrefix = "Z" + zone;
break;
default:
throw new UnsupportedCommandTypeException("Zone must be in range [1-4], zone: " + zone);
}
String cmd = zonePrefix;
if (command instanceof RefreshType) {
cmd += "?";
} else if (command == IncreaseDecreaseType.INCREASE) {
cmd += "UP";
} else if (command == IncreaseDecreaseType.DECREASE) {
cmd += "DOWN";
} else if (command instanceof DecimalType) {
cmd += toDenonValue(((DecimalType) command));
} else if (command instanceof PercentType) {
cmd += percentToDenonValue(((PercentType) command).toBigDecimal());
} else {
throw new UnsupportedCommandTypeException();
}
internalSendCommand(cmd);
}
public void sendVolumeDbCommand(Command command, int zone) throws UnsupportedCommandTypeException {
Command dbCommand = command;
if (dbCommand instanceof PercentType) {
throw new UnsupportedCommandTypeException();
} else if (dbCommand instanceof DecimalType) {
// convert dB to 'normal' volume by adding the offset of 80
dbCommand = new DecimalType(((DecimalType) command).toBigDecimal().add(DB_OFFSET));
}
sendVolumeCommand(dbCommand, zone);
}
protected String toDenonValue(DecimalType number) {
String dbString = String.valueOf(number.intValue());
BigDecimal num = number.toBigDecimal();
if (num.compareTo(BigDecimal.TEN) == -1) {
dbString = "0" + dbString;
}
if (num.remainder(BigDecimal.ONE).equals(POINTFIVE)) {
dbString = dbString + "5";
}
return dbString;
}
protected String percentToDenonValue(BigDecimal pct) {
// Round to nearest number divisible by 0.5
BigDecimal percent = pct.divide(POINTFIVE).setScale(0, RoundingMode.UP).multiply(POINTFIVE)
.min(config.getMainVolumeMax()).max(BigDecimal.ZERO);
return toDenonValue(new DecimalType(percent));
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.connector;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.openhab.binding.denonmarantz.internal.connector.http.DenonMarantzHttpConnector;
import org.openhab.binding.denonmarantz.internal.connector.telnet.DenonMarantzTelnetConnector;
/**
* Returns the connector based on the configuration.
* Currently there are 2 types: HTTP and Telnet
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
public class DenonMarantzConnectorFactory {
public DenonMarantzConnector getConnector(DenonMarantzConfiguration config, DenonMarantzState state,
ScheduledExecutorService scheduler, HttpClient httpClient) {
if (config.isTelnet()) {
return new DenonMarantzTelnetConnector(config, state, scheduler);
} else {
return new DenonMarantzHttpConnector(config, state, scheduler, httpClient);
}
}
}

View File

@@ -0,0 +1,373 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.connector.http;
import java.beans.Introspector;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.UnmarshalException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.util.StreamReaderDelegate;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
import org.openhab.binding.denonmarantz.internal.xml.entities.Deviceinfo;
import org.openhab.binding.denonmarantz.internal.xml.entities.Main;
import org.openhab.binding.denonmarantz.internal.xml.entities.ZoneStatus;
import org.openhab.binding.denonmarantz.internal.xml.entities.ZoneStatusLite;
import org.openhab.binding.denonmarantz.internal.xml.entities.commands.AppCommandRequest;
import org.openhab.binding.denonmarantz.internal.xml.entities.commands.AppCommandResponse;
import org.openhab.binding.denonmarantz.internal.xml.entities.commands.CommandRx;
import org.openhab.binding.denonmarantz.internal.xml.entities.commands.CommandTx;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class makes the connection to the receiver and manages it.
* It is also responsible for sending commands to the receiver.
* *
*
* @author Jeroen Idserda - Initial Contribution (1.x Binding)
* @author Jan-Willem Veldhuis - Refactored for 2.x
*/
public class DenonMarantzHttpConnector extends DenonMarantzConnector {
private Logger logger = LoggerFactory.getLogger(DenonMarantzHttpConnector.class);
private static final int REQUEST_TIMEOUT_MS = 5000; // 5 seconds
// Main URL for the receiver
private static final String URL_MAIN = "formMainZone_MainZoneXml.xml";
// Main Zone Status URL
private static final String URL_ZONE_MAIN = "formMainZone_MainZoneXmlStatus.xml";
// Secondary zone lite status URL (contains less info)
private static final String URL_ZONE_SECONDARY_LITE = "formZone%d_Zone%dXmlStatusLite.xml";
// Device info URL
private static final String URL_DEVICE_INFO = "Deviceinfo.xml";
// URL to send app commands to
private static final String URL_APP_COMMAND = "AppCommand.xml";
private static final String CONTENT_TYPE_XML = "application/xml";
private final String cmdUrl;
private final String statusUrl;
private final HttpClient httpClient;
private ScheduledFuture<?> pollingJob;
public DenonMarantzHttpConnector(DenonMarantzConfiguration config, DenonMarantzState state,
ScheduledExecutorService scheduler, HttpClient httpClient) {
this.config = config;
this.scheduler = scheduler;
this.state = state;
this.cmdUrl = String.format("http://%s:%d/goform/formiPhoneAppDirect.xml?", config.getHost(),
config.getHttpPort());
this.statusUrl = String.format("http://%s:%d/goform/", config.getHost(), config.getHttpPort());
this.httpClient = httpClient;
}
public DenonMarantzState getState() {
return state;
}
/**
* Set up the connection to the receiver by starting to poll the HTTP API.
*/
@Override
public void connect() {
if (!isPolling()) {
logger.debug("HTTP polling started.");
try {
setConfigProperties();
} catch (IOException e) {
logger.debug("IO error while retrieving document:", e);
state.connectionError("IO error while connecting to AVR: " + e.getMessage());
return;
}
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
try {
refreshHttpProperties();
} catch (IOException e) {
logger.debug("IO error while retrieving document", e);
state.connectionError("IO error while connecting to AVR: " + e.getMessage());
stopPolling();
} catch (RuntimeException e) {
/**
* We need to catch this RuntimeException, as otherwise the polling stops.
* Log as error as it could be a user configuration error.
*/
StringBuilder sb = new StringBuilder();
for (StackTraceElement s : e.getStackTrace()) {
sb.append(s.toString()).append("\n");
}
logger.error("Error while polling Http: \"{}\". Stacktrace: \n{}", e.getMessage(), sb.toString());
}
}, 0, config.httpPollingInterval, TimeUnit.SECONDS);
}
}
private boolean isPolling() {
return pollingJob != null && !pollingJob.isCancelled();
}
private void stopPolling() {
if (isPolling()) {
pollingJob.cancel(true);
logger.debug("HTTP polling stopped.");
}
}
/**
* Shutdown the http client
*/
@Override
public void dispose() {
logger.debug("disposing connector");
stopPolling();
}
@Override
protected void internalSendCommand(String command) {
logger.debug("Sending command '{}'", command);
if (StringUtils.isBlank(command)) {
logger.warn("Trying to send empty command");
return;
}
try {
String url = cmdUrl + URLEncoder.encode(command, Charset.defaultCharset().displayName());
logger.trace("Calling url {}", url);
httpClient.newRequest(url).timeout(5, TimeUnit.SECONDS).send(new Response.CompleteListener() {
@Override
public void onComplete(Result result) {
if (result.getResponse().getStatus() != 200) {
logger.warn("Error {} while sending command", result.getResponse().getReason());
}
}
});
} catch (UnsupportedEncodingException e) {
logger.warn("Error sending command", e);
}
}
private void updateMain() throws IOException {
String url = statusUrl + URL_MAIN;
logger.trace("Refreshing URL: {}", url);
Main statusMain = getDocument(url, Main.class);
if (statusMain != null) {
state.setPower(statusMain.getPower().getValue());
}
}
private void updateMainZone() throws IOException {
String url = statusUrl + URL_ZONE_MAIN;
logger.trace("Refreshing URL: {}", url);
ZoneStatus mainZone = getDocument(url, ZoneStatus.class);
if (mainZone != null) {
state.setInput(mainZone.getInputFuncSelect().getValue());
state.setMainVolume(mainZone.getMasterVolume().getValue());
state.setMainZonePower(mainZone.getPower().getValue());
state.setMute(mainZone.getMute().getValue());
if (config.inputOptions == null) {
config.inputOptions = mainZone.getInputFuncList();
}
if (mainZone.getSurrMode() == null) {
logger.debug("Unable to get the SURROUND_MODE. MainZone update may not be correct.");
} else {
state.setSurroundProgram(mainZone.getSurrMode().getValue());
}
}
}
private void updateSecondaryZones() throws IOException {
for (int i = 2; i <= config.getZoneCount(); i++) {
String url = String.format("%s" + URL_ZONE_SECONDARY_LITE, statusUrl, i, i);
logger.trace("Refreshing URL: {}", url);
ZoneStatusLite zoneSecondary = getDocument(url, ZoneStatusLite.class);
if (zoneSecondary != null) {
switch (i) {
// maximum 2 secondary zones are supported
case 2:
state.setZone2Power(zoneSecondary.getPower().getValue());
state.setZone2Volume(zoneSecondary.getMasterVolume().getValue());
state.setZone2Mute(zoneSecondary.getMute().getValue());
state.setZone2Input(zoneSecondary.getInputFuncSelect().getValue());
break;
case 3:
state.setZone3Power(zoneSecondary.getPower().getValue());
state.setZone3Volume(zoneSecondary.getMasterVolume().getValue());
state.setZone3Mute(zoneSecondary.getMute().getValue());
state.setZone3Input(zoneSecondary.getInputFuncSelect().getValue());
break;
case 4:
state.setZone4Power(zoneSecondary.getPower().getValue());
state.setZone4Volume(zoneSecondary.getMasterVolume().getValue());
state.setZone4Mute(zoneSecondary.getMute().getValue());
state.setZone4Input(zoneSecondary.getInputFuncSelect().getValue());
break;
}
}
}
}
private void updateDisplayInfo() throws IOException {
String url = statusUrl + URL_APP_COMMAND;
logger.trace("Refreshing URL: {}", url);
AppCommandRequest request = AppCommandRequest.of(CommandTx.CMD_NET_STATUS);
AppCommandResponse response = postDocument(url, AppCommandResponse.class, request);
if (response != null) {
CommandRx titleInfo = response.getCommands().get(0);
state.setNowPlayingArtist(titleInfo.getText("artist"));
state.setNowPlayingAlbum(titleInfo.getText("album"));
state.setNowPlayingTrack(titleInfo.getText("track"));
}
}
private boolean setConfigProperties() throws IOException {
String url = statusUrl + URL_DEVICE_INFO;
logger.debug("Refreshing URL: {}", url);
Deviceinfo deviceinfo = getDocument(url, Deviceinfo.class);
if (deviceinfo != null) {
config.setZoneCount(deviceinfo.getDeviceZones());
}
/**
* The maximum volume is received from the telnet connection in the
* form of the MVMAX property. It is not always received reliable however,
* so we're using a default for now.
*/
config.setMainVolumeMax(DenonMarantzConfiguration.MAX_VOLUME);
// if deviceinfo is null, something went wrong (and is logged in getDocument catch blocks)
return (deviceinfo != null);
}
private void refreshHttpProperties() throws IOException {
logger.trace("Refreshing Denon status");
updateMain();
updateMainZone();
updateSecondaryZones();
updateDisplayInfo();
}
@Nullable
private <T> T getDocument(String uri, Class<T> response) throws IOException {
try {
String result = HttpUtil.executeUrl("GET", uri, REQUEST_TIMEOUT_MS);
logger.trace("result of getDocument for uri '{}':\r\n{}", uri, result);
if (StringUtils.isNotBlank(result)) {
JAXBContext jc = JAXBContext.newInstance(response);
XMLInputFactory xif = XMLInputFactory.newInstance();
XMLStreamReader xsr = xif.createXMLStreamReader(IOUtils.toInputStream(result));
xsr = new PropertyRenamerDelegate(xsr);
@SuppressWarnings("unchecked")
T obj = (T) jc.createUnmarshaller().unmarshal(xsr);
return obj;
}
} catch (UnmarshalException e) {
logger.debug("Failed to unmarshal xml document: {}", e.getMessage());
} catch (JAXBException e) {
logger.debug("Unexpected error occurred during unmarshalling of document: {}", e.getMessage());
} catch (XMLStreamException e) {
logger.debug("Communication error: {}", e.getMessage());
}
return null;
}
@Nullable
private <T, S> T postDocument(String uri, Class<T> response, S request) throws IOException {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(request.getClass());
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
StringWriter sw = new StringWriter();
jaxbMarshaller.marshal(request, sw);
ByteArrayInputStream inputStream = new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8));
String result = HttpUtil.executeUrl("POST", uri, inputStream, CONTENT_TYPE_XML, REQUEST_TIMEOUT_MS);
if (StringUtils.isNotBlank(result)) {
JAXBContext jcResponse = JAXBContext.newInstance(response);
@SuppressWarnings("unchecked")
T obj = (T) jcResponse.createUnmarshaller().unmarshal(IOUtils.toInputStream(result));
return obj;
}
} catch (JAXBException e) {
logger.debug("Encoding error in post", e);
}
return null;
}
private static class PropertyRenamerDelegate extends StreamReaderDelegate {
public PropertyRenamerDelegate(XMLStreamReader xsr) {
super(xsr);
}
@Override
public String getAttributeLocalName(int index) {
return Introspector.decapitalize(super.getAttributeLocalName(index));
}
@Override
public String getLocalName() {
return Introspector.decapitalize(super.getLocalName());
}
}
}

View File

@@ -0,0 +1,175 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.connector.telnet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manage telnet connection to the Denon/Marantz Receiver
*
* @author Jeroen Idserda - Initial contribution (1.x Binding)
* @author Jan-Willem Veldhuis - Refactored for 2.x
*/
public class DenonMarantzTelnetClient implements Runnable {
private Logger logger = LoggerFactory.getLogger(DenonMarantzTelnetClient.class);
private static final Integer RECONNECT_DELAY = 60000; // 1 minute
private static final Integer TIMEOUT = 60000; // 1 minute
private DenonMarantzConfiguration config;
private DenonMarantzTelnetListener listener;
private boolean running = true;
private boolean connected = false;
private Socket socket;
private OutputStreamWriter out;
private BufferedReader in;
public DenonMarantzTelnetClient(DenonMarantzConfiguration config, DenonMarantzTelnetListener listener) {
logger.debug("Denon listener created");
this.config = config;
this.listener = listener;
}
@Override
public void run() {
while (running) {
if (!connected) {
connectTelnetSocket();
}
do {
try {
String line = in.readLine();
if (line == null) {
logger.debug("No more data read from client. Disconnecting..");
listener.telnetClientConnected(false);
disconnect();
break;
}
logger.trace("Received from {}: {}", config.getHost(), line);
if (!StringUtils.isBlank(line)) {
listener.receivedLine(line);
}
} catch (SocketTimeoutException e) {
logger.trace("Socket timeout");
// Disconnects are not always detected unless you write to the socket.
try {
out.write('\r');
out.flush();
} catch (IOException e2) {
logger.debug("Error writing to socket");
connected = false;
}
} catch (IOException e) {
if (!Thread.currentThread().isInterrupted()) {
// only log if we don't stop this on purpose causing a SocketClosed
logger.debug("Error in telnet connection ", e);
}
connected = false;
listener.telnetClientConnected(false);
}
} while (running && connected);
}
}
public void sendCommand(String command) {
if (out != null) {
logger.debug("Sending command {}", command);
try {
out.write(command + '\r');
out.flush();
} catch (IOException e) {
logger.debug("Error sending command", e);
}
} else {
logger.debug("Cannot send command, no telnet connection");
}
}
public void shutdown() {
this.running = false;
disconnect();
}
private void connectTelnetSocket() {
disconnect();
int delay = 0;
while (this.running && (socket == null || !socket.isConnected())) {
try {
Thread.sleep(delay);
logger.debug("Connecting to {}", config.getHost());
// Use raw socket instead of TelnetClient here because TelnetClient sends an extra newline char
// after each write which causes the connection to become unresponsive.
socket = new Socket();
socket.connect(new InetSocketAddress(config.getHost(), config.getTelnetPort()), TIMEOUT);
socket.setKeepAlive(true);
socket.setSoTimeout(TIMEOUT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
connected = true;
listener.telnetClientConnected(true);
} catch (IOException e) {
logger.debug("Cannot connect to {}", config.getHost(), e);
listener.telnetClientConnected(false);
} catch (InterruptedException e) {
logger.debug("Interrupted while connecting to {}", config.getHost(), e);
}
delay = RECONNECT_DELAY;
}
logger.debug("Denon telnet client connected to {}", config.getHost());
}
public boolean isConnected() {
return connected;
}
private void disconnect() {
if (socket != null) {
logger.debug("Disconnecting socket");
try {
socket.close();
} catch (IOException e) {
logger.debug("Error while disconnecting telnet client", e);
} finally {
socket = null;
out = null;
in = null;
listener.telnetClientConnected(false);
}
}
}
}

View File

@@ -0,0 +1,275 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.connector.telnet;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class makes the connection to the receiver and manages it.
* It is also responsible for sending commands to the receiver.
*
* @author Jeroen Idserda - Initial Contribution (1.x Binding)
* @author Jan-Willem Veldhuis - Refactored for 2.x
*/
public class DenonMarantzTelnetConnector extends DenonMarantzConnector implements DenonMarantzTelnetListener {
private final Logger logger = LoggerFactory.getLogger(DenonMarantzTelnetConnector.class);
// All regular commands. Example: PW, SICD, SITV, Z2MU
private static final Pattern COMMAND_PATTERN = Pattern.compile("^([A-Z0-9]{2})(.+)$");
// Example: E2Counting Crows
private static final Pattern DISPLAY_PATTERN = Pattern.compile("^(E|A)([0-9]{1})(.+)$");
private static final BigDecimal NINETYNINE = new BigDecimal("99");
private DenonMarantzTelnetClient telnetClient;
private boolean displayNowplaying = false;
protected boolean disposing = false;
private Future<?> telnetStateRequest;
private Future<?> telnetRunnable;
public DenonMarantzTelnetConnector(DenonMarantzConfiguration config, DenonMarantzState state,
ScheduledExecutorService scheduler) {
this.config = config;
this.scheduler = scheduler;
this.state = state;
}
/**
* Set up the connection to the receiver. Either using Telnet or by polling the HTTP API.
*/
@Override
public void connect() {
telnetClient = new DenonMarantzTelnetClient(config, this);
telnetRunnable = scheduler.submit(telnetClient);
}
@Override
public void telnetClientConnected(boolean connected) {
if (!connected) {
if (config.isTelnet() && !disposing) {
logger.debug("Telnet client disconnected.");
state.connectionError(
"Error connecting to the telnet port. Consider disabling telnet in this Thing's configuration to use HTTP polling instead.");
}
} else {
refreshState();
}
}
/**
* Shutdown the telnet client (if initialized) and the http client
*/
@Override
public void dispose() {
logger.debug("disposing connector");
disposing = true;
if (telnetStateRequest != null) {
telnetStateRequest.cancel(true);
telnetStateRequest = null;
}
if (telnetClient != null && !telnetRunnable.isDone()) {
telnetRunnable.cancel(true);
telnetClient.shutdown();
}
}
private void refreshState() {
// Sends a series of state query commands over the telnet connection
telnetStateRequest = scheduler.submit(() -> {
List<String> cmds = new ArrayList<>(Arrays.asList("PW?", "MS?", "MV?", "ZM?", "MU?", "SI?"));
if (config.getZoneCount() > 1) {
cmds.add("Z2?");
cmds.add("Z2MU?");
}
if (config.getZoneCount() > 2) {
cmds.add("Z3?");
cmds.add("Z3MU?");
}
for (String cmd : cmds) {
internalSendCommand(cmd);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
logger.trace("requestStateOverTelnet() - Interrupted while requesting state.");
}
}
});
}
/**
* This method tries to parse information received over the telnet connection.
* It can be quite unreliable. Some chars go missing or turn into other chars. That's
* why each command is validated using a regex.
*
* @param line The received command (one line)
*/
@Override
public void receivedLine(String line) {
if (COMMAND_PATTERN.matcher(line).matches()) {
/*
* This splits the commandString into the command and the parameter. SICD
* for example has SI as the command and CD as the parameter.
*/
String command = line.substring(0, 2);
String value = line.substring(2, line.length()).trim();
logger.debug("Received Command: {}, value: {}", command, value);
// use received command (event) from telnet to update state
switch (command) {
case "SI": // Switch Input
state.setInput(value);
break;
case "PW": // Power
if (value.equals("ON") || value.equals("STANDBY")) {
state.setPower(value.equals("ON"));
}
break;
case "MS": // Main zone surround program
state.setSurroundProgram(value);
break;
case "MV": // Main zone volume
if (StringUtils.isNumeric(value)) {
state.setMainVolume(fromDenonValue(value));
}
break;
case "MU": // Main zone mute
if (value.equals("ON") || value.equals("OFF")) {
state.setMute(value.equals("ON"));
}
break;
case "NS": // Now playing information
processTitleCommand(value);
break;
case "Z2": // Zone 2
if (value.equals("ON") || value.equals("OFF")) {
state.setZone2Power(value.equals("ON"));
} else if (value.equals("MUON") || value.equals("MUOFF")) {
state.setZone2Mute(value.equals("MUON"));
} else if (StringUtils.isNumeric(value)) {
state.setZone2Volume(fromDenonValue(value));
} else {
state.setZone2Input(value);
}
break;
case "Z3": // Zone 3
if (value.equals("ON") || value.equals("OFF")) {
state.setZone3Power(value.equals("ON"));
} else if (value.equals("MUON") || value.equals("MUOFF")) {
state.setZone3Mute(value.equals("MUON"));
} else if (StringUtils.isNumeric(value)) {
state.setZone3Volume(fromDenonValue(value));
} else {
state.setZone3Input(value);
}
break;
case "Z4": // Zone 4
if (value.equals("ON") || value.equals("OFF")) {
state.setZone4Power(value.equals("ON"));
} else if (value.equals("MUON") || value.equals("MUOFF")) {
state.setZone4Mute(value.equals("MUON"));
} else if (StringUtils.isNumeric(value)) {
state.setZone4Volume(fromDenonValue(value));
} else {
state.setZone4Input(value);
}
break;
case "ZM": // Main zone
if (value.equals("ON") || value.equals("OFF")) {
state.setMainZonePower(value.equals("ON"));
}
break;
}
} else {
logger.trace("Ignoring received line: '{}'", line);
}
}
private BigDecimal fromDenonValue(String string) {
/*
* 455 = 45,5
* 45 = 45
* 045 = 4,5
* 04 = 4
*/
BigDecimal value = new BigDecimal(string);
if (value.compareTo(NINETYNINE) == 1 || (string.startsWith("0") && string.length() > 2)) {
value = value.divide(BigDecimal.TEN);
}
return value;
}
private void processTitleCommand(String value) {
if (DISPLAY_PATTERN.matcher(value).matches()) {
Integer commandNo = Integer.valueOf(value.substring(1, 2));
String titleValue = value.substring(2);
if (commandNo == 0) {
displayNowplaying = titleValue.contains("Now Playing");
}
String nowPlaying = displayNowplaying ? cleanupDisplayInfo(titleValue) : "";
switch (commandNo) {
case 1:
state.setNowPlayingTrack(nowPlaying);
break;
case 2:
state.setNowPlayingArtist(nowPlaying);
break;
case 4:
state.setNowPlayingAlbum(nowPlaying);
break;
}
}
}
@Override
protected void internalSendCommand(String command) {
logger.debug("Sending command '{}'", command);
if (StringUtils.isBlank(command)) {
logger.warn("Trying to send empty command");
return;
}
telnetClient.sendCommand(command);
}
/**
* Display info could contain some garbled text, attempt to clean it up.
*/
private String cleanupDisplayInfo(String titleValue) {
byte firstByteRemoved[] = Arrays.copyOfRange(titleValue.getBytes(), 1, titleValue.getBytes().length);
return new String(firstByteRemoved).replaceAll("[\u0000-\u001f]", "");
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.connector.telnet;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
/**
* Listener interface used to notify the {@link DenonMarantzConnector} about received messages over Telnet
*
* @author Jan-Willem Veldhuis - Initial contribution
*
*/
public interface DenonMarantzTelnetListener {
/**
* The telnet client has received a line.
*
* @param line the received line
*/
void receivedLine(String line);
/**
* The telnet client has successfully connected to the receiver.
*
* @param connected whether or not the connection was successful
*/
void telnetClientConnected(boolean connected);
}

View File

@@ -0,0 +1,139 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.discovery;
import static org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Jan-Willem Veldhuis - Initial contribution
*
*/
@Component(immediate = true)
public class DenonMarantzDiscoveryParticipant implements MDNSDiscoveryParticipant {
private Logger logger = LoggerFactory.getLogger(DenonMarantzDiscoveryParticipant.class);
// Service type for 'Airplay enabled' receivers
private static final String RAOP_SERVICE_TYPE = "_raop._tcp.local.";
/**
* Match the serial number, vendor and model of the discovered AVR.
* Input is like "0006781D58B1@Marantz SR5008._raop._tcp.local."
* A Denon AVR serial (MAC address) starts with 0005CD
* A Marantz AVR serial (MAC address) starts with 000678
*/
private static final Pattern DENON_MARANTZ_PATTERN = Pattern
.compile("^((?:0005CD|000678)[A-Z0-9]+)@(.+)\\._raop\\._tcp\\.local\\.$");
/**
* Denon AVRs have a MAC address / serial number starting with 0005CD
*/
private static final String DENON_MAC_PREFIX = "0005CD";
/**
* Marantz AVRs have a MAC address / serial number starting with 000678
*/
private static final String MARANTZ_MAC_PREFIX = "000678";
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(THING_TYPE_AVR);
}
@Override
public String getServiceType() {
return RAOP_SERVICE_TYPE;
}
@Override
public DiscoveryResult createResult(ServiceInfo serviceInfo) {
String qualifiedName = serviceInfo.getQualifiedName();
logger.debug("AVR found: {}", qualifiedName);
ThingUID thingUID = getThingUID(serviceInfo);
if (thingUID != null) {
Matcher matcher = DENON_MARANTZ_PATTERN.matcher(qualifiedName);
matcher.matches(); // we already know it matches, it was matched in getThingUID
String serial = matcher.group(1).toLowerCase();
/**
* The Vendor is not available from the mDNS result.
* We assign the Vendor based on our assumptions of the MAC address prefix.
*/
String vendor = "";
if (serial.startsWith(MARANTZ_MAC_PREFIX)) {
vendor = "Marantz";
} else if (serial.startsWith(DENON_MAC_PREFIX)) {
vendor = "Denon";
}
// 'am=...' property describes the model name
String model = serviceInfo.getPropertyString("am");
String friendlyName = matcher.group(2).trim();
Map<String, Object> properties = new HashMap<>(2);
if (serviceInfo.getHostAddresses().length == 0) {
logger.debug("Could not determine IP address for the Denon/Marantz AVR");
return null;
}
String host = serviceInfo.getHostAddresses()[0];
logger.debug("IP Address: {}", host);
properties.put(PARAMETER_HOST, host);
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serial);
properties.put(Thing.PROPERTY_VENDOR, vendor);
properties.put(Thing.PROPERTY_MODEL_ID, model);
String label = friendlyName + " (" + vendor + ' ' + model + ")";
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel(label)
.withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
return result;
} else {
return null;
}
}
@Override
public ThingUID getThingUID(ServiceInfo service) {
Matcher matcher = DENON_MARANTZ_PATTERN.matcher(service.getQualifiedName());
if (matcher.matches()) {
logger.debug("This seems like a supported Denon/Marantz AVR!");
String serial = matcher.group(1).toLowerCase();
return new ThingUID(THING_TYPE_AVR, serial);
} else {
logger.trace("This discovered device is not supported by the DenonMarantz binding, ignoring..");
}
return null;
}
}

View File

@@ -0,0 +1,444 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.handler;
import static org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants.*;
import java.io.IOException;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
import org.openhab.binding.denonmarantz.internal.DenonMarantzStateChangedListener;
import org.openhab.binding.denonmarantz.internal.UnsupportedCommandTypeException;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnectorFactory;
import org.openhab.binding.denonmarantz.internal.connector.http.DenonMarantzHttpConnector;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* The {@link DenonMarantzHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
public class DenonMarantzHandler extends BaseThingHandler implements DenonMarantzStateChangedListener {
private final Logger logger = LoggerFactory.getLogger(DenonMarantzHandler.class);
private static final int RETRY_TIME_SECONDS = 30;
private HttpClient httpClient;
private DenonMarantzConnector connector;
private DenonMarantzConfiguration config;
private DenonMarantzConnectorFactory connectorFactory = new DenonMarantzConnectorFactory();
private DenonMarantzState denonMarantzState;
private ScheduledFuture<?> retryJob;
public DenonMarantzHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (connector == null) {
return;
}
if (connector instanceof DenonMarantzHttpConnector && command instanceof RefreshType) {
// Refreshing individual channels isn't supported by the Http connector.
// The connector refreshes all channels together at the configured polling interval.
return;
}
try {
switch (channelUID.getId()) {
case CHANNEL_POWER:
connector.sendPowerCommand(command, 0);
break;
case CHANNEL_MAIN_ZONE_POWER:
connector.sendPowerCommand(command, 1);
break;
case CHANNEL_MUTE:
connector.sendMuteCommand(command, 1);
break;
case CHANNEL_MAIN_VOLUME:
connector.sendVolumeCommand(command, 1);
break;
case CHANNEL_MAIN_VOLUME_DB:
connector.sendVolumeDbCommand(command, 1);
break;
case CHANNEL_INPUT:
connector.sendInputCommand(command, 1);
break;
case CHANNEL_SURROUND_PROGRAM:
connector.sendSurroundProgramCommand(command);
break;
case CHANNEL_COMMAND:
connector.sendCustomCommand(command);
break;
case CHANNEL_ZONE2_POWER:
connector.sendPowerCommand(command, 2);
break;
case CHANNEL_ZONE2_MUTE:
connector.sendMuteCommand(command, 2);
break;
case CHANNEL_ZONE2_VOLUME:
connector.sendVolumeCommand(command, 2);
break;
case CHANNEL_ZONE2_VOLUME_DB:
connector.sendVolumeDbCommand(command, 2);
break;
case CHANNEL_ZONE2_INPUT:
connector.sendInputCommand(command, 2);
break;
case CHANNEL_ZONE3_POWER:
connector.sendPowerCommand(command, 3);
break;
case CHANNEL_ZONE3_MUTE:
connector.sendMuteCommand(command, 3);
break;
case CHANNEL_ZONE3_VOLUME:
connector.sendVolumeCommand(command, 3);
break;
case CHANNEL_ZONE3_VOLUME_DB:
connector.sendVolumeDbCommand(command, 3);
break;
case CHANNEL_ZONE3_INPUT:
connector.sendInputCommand(command, 3);
break;
case CHANNEL_ZONE4_POWER:
connector.sendPowerCommand(command, 4);
break;
case CHANNEL_ZONE4_MUTE:
connector.sendMuteCommand(command, 4);
break;
case CHANNEL_ZONE4_VOLUME:
connector.sendVolumeCommand(command, 4);
break;
case CHANNEL_ZONE4_VOLUME_DB:
connector.sendVolumeDbCommand(command, 4);
break;
case CHANNEL_ZONE4_INPUT:
connector.sendInputCommand(command, 4);
break;
default:
throw new UnsupportedCommandTypeException();
}
} catch (UnsupportedCommandTypeException e) {
logger.debug("Unsupported command {} for channel {}", command, channelUID.getId());
}
}
public boolean checkConfiguration() {
// prevent too low values for polling interval
if (config.httpPollingInterval < 5) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"The polling interval should be at least 5 seconds!");
return false;
}
// Check zone count is within supported range
if (config.getZoneCount() < 1 || config.getZoneCount() > 4) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"This binding supports 1 to 4 zones. Please update the zone count.");
return false;
}
return true;
}
/**
* Try to auto configure the connection type (Telnet or HTTP)
* for Things not added through Paper UI.
*/
private void autoConfigure() {
/*
* The isTelnet parameter has no default.
* When not set we will try to auto-detect the correct values
* for isTelnet and zoneCount and update the Thing accordingly.
*/
if (config.isTelnet() == null) {
logger.debug("Trying to auto-detect the connection.");
ContentResponse response;
boolean telnetEnable = true;
int httpPort = 80;
boolean httpApiUsable = false;
// try to reach the HTTP API at port 80 (most models, except Denon ...H should respond.
String host = config.getHost();
try {
response = httpClient.newRequest("http://" + host + "/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
logger.debug("We can access the HTTP API, disabling the Telnet mode by default.");
telnetEnable = false;
httpApiUsable = true;
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Error when trying to access AVR using HTTP on port 80, reverting to Telnet mode.", e);
}
if (telnetEnable) {
// the above attempt failed. Let's try on port 8080, as for some models a subset of the HTTP API is
// available
try {
response = httpClient.newRequest("http://" + host + ":8080/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) {
logger.debug(
"This model responds to HTTP port 8080, we use this port to retrieve the number of zones.");
httpPort = 8080;
httpApiUsable = true;
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Additionally tried to connect to port 8080, this also failed", e);
}
}
// default zone count
int zoneCount = 2;
// try to determine the zone count by checking the Deviceinfo.xml file
if (httpApiUsable) {
int status = 0;
response = null;
try {
response = httpClient.newRequest("http://" + host + ":" + httpPort + "/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send();
status = response.getStatus();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Failed in fetching the Deviceinfo.xml to determine zone count", e);
}
if (status == HttpURLConnection.HTTP_OK && response != null) {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder;
try {
builder = domFactory.newDocumentBuilder();
Document dDoc = builder.parse(new InputSource(new StringReader(response.getContentAsString())));
XPath xPath = XPathFactory.newInstance().newXPath();
Node node = (Node) xPath.evaluate("/Device_Info/DeviceZones/text()", dDoc, XPathConstants.NODE);
if (node != null) {
String nodeValue = node.getNodeValue();
logger.trace("/Device_Info/DeviceZones/text() = {}", nodeValue);
zoneCount = Integer.parseInt(nodeValue);
logger.debug("Discovered number of zones: {}", zoneCount);
}
} catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException
| NumberFormatException e) {
logger.debug("Something went wrong with looking up the zone count in Deviceinfo.xml: {}",
e.getMessage());
}
}
}
config.setTelnet(telnetEnable);
config.setZoneCount(zoneCount);
Configuration configuration = editConfiguration();
configuration.put(PARAMETER_TELNET_ENABLED, telnetEnable);
configuration.put(PARAMETER_ZONE_COUNT, zoneCount);
updateConfiguration(configuration);
}
}
@Override
public void initialize() {
cancelRetry();
config = getConfigAs(DenonMarantzConfiguration.class);
// Configure Connection type (Telnet/HTTP) and number of zones
// Note: this only happens for discovered Things
autoConfigure();
if (!checkConfiguration()) {
return;
}
denonMarantzState = new DenonMarantzState(this);
configureZoneChannels();
updateStatus(ThingStatus.UNKNOWN);
// create connection (either Telnet or HTTP)
// ThingStatus ONLINE/OFFLINE is set when AVR status is known.
createConnection();
}
private void createConnection() {
if (connector != null) {
connector.dispose();
}
connector = connectorFactory.getConnector(config, denonMarantzState, scheduler, httpClient);
connector.connect();
}
private void cancelRetry() {
ScheduledFuture<?> localRetryJob = retryJob;
if (localRetryJob != null && !localRetryJob.isDone()) {
localRetryJob.cancel(false);
}
}
private void configureZoneChannels() {
logger.debug("Configuring zone channels");
Integer zoneCount = config.getZoneCount();
List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
boolean channelsUpdated = false;
// construct a set with the existing channel type UIDs, to quickly check
Set<String> currentChannels = new HashSet<>();
channels.forEach(channel -> currentChannels.add(channel.getUID().getId()));
Set<Entry<String, ChannelTypeUID>> channelsToRemove = new HashSet<>();
if (zoneCount > 1) {
List<Entry<String, ChannelTypeUID>> channelsToAdd = new ArrayList<>(ZONE2_CHANNEL_TYPES.entrySet());
if (zoneCount > 2) {
// add channels for zone 3
channelsToAdd.addAll(ZONE3_CHANNEL_TYPES.entrySet());
if (zoneCount > 3) {
// add channels for zone 4 (more zones currently not supported)
channelsToAdd.addAll(ZONE4_CHANNEL_TYPES.entrySet());
} else {
channelsToRemove.addAll(ZONE4_CHANNEL_TYPES.entrySet());
}
} else {
channelsToRemove.addAll(ZONE3_CHANNEL_TYPES.entrySet());
channelsToRemove.addAll(ZONE4_CHANNEL_TYPES.entrySet());
}
// filter out the already existing channels
channelsToAdd.removeIf(c -> currentChannels.contains(c.getKey()));
// add the channels that were not yet added
if (!channelsToAdd.isEmpty()) {
for (Entry<String, ChannelTypeUID> entry : channelsToAdd) {
String itemType = CHANNEL_ITEM_TYPES.get(entry.getKey());
Channel channel = ChannelBuilder
.create(new ChannelUID(this.getThing().getUID(), entry.getKey()), itemType)
.withType(entry.getValue()).build();
channels.add(channel);
}
channelsUpdated = true;
} else {
logger.debug("No zone channels have been added");
}
} else {
channelsToRemove.addAll(ZONE2_CHANNEL_TYPES.entrySet());
channelsToRemove.addAll(ZONE3_CHANNEL_TYPES.entrySet());
channelsToRemove.addAll(ZONE4_CHANNEL_TYPES.entrySet());
}
// filter out the non-existing channels
channelsToRemove.removeIf(c -> !currentChannels.contains(c.getKey()));
// remove the channels that were not yet added
if (!channelsToRemove.isEmpty()) {
for (Entry<String, ChannelTypeUID> entry : channelsToRemove) {
if (channels.removeIf(c -> (entry.getKey()).equals(c.getUID().getId()))) {
logger.trace("Removed channel {}", entry.getKey());
} else {
logger.trace("Could NOT remove channel {}", entry.getKey());
}
}
channelsUpdated = true;
} else {
logger.debug("No zone channels have been removed");
}
// update Thing if channels changed
if (channelsUpdated) {
updateThing(editThing().withChannels(channels).build());
}
}
@Override
public void dispose() {
if (connector != null) {
connector.dispose();
connector = null;
}
cancelRetry();
super.dispose();
}
@Override
public void channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
String channelID = channelUID.getId();
if (isLinked(channelID)) {
State state = denonMarantzState.getStateForChannelID(channelID);
if (state != null) {
updateState(channelID, state);
}
}
}
@Override
public void stateChanged(String channelID, State state) {
logger.debug("Received state {} for channelID {}", state, channelID);
// Don't flood the log with thing 'updated: ONLINE' each time a single channel changed
if (this.getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
updateState(channelID, state);
}
@Override
public void connectionError(String errorMessage) {
if (this.getThing().getStatus() != ThingStatus.OFFLINE) {
// Don't flood the log with thing 'updated: OFFLINE' when already offline
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
}
connector.dispose();
retryJob = scheduler.schedule(this::createConnection, RETRY_TIME_SECONDS, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.adapters;
import javax.xml.bind.annotation.adapters.XmlAdapter;
/**
* Maps 'On' and 'Off' string values to a boolean
*
* @author Jeroen Idserda - Initial contribution
*/
public class OnOffAdapter extends XmlAdapter<String, Boolean> {
@Override
public Boolean unmarshal(String v) throws Exception {
if (v != null) {
return Boolean.valueOf(v.toLowerCase().equals("on"));
}
return Boolean.FALSE;
}
@Override
public String marshal(Boolean v) throws Exception {
return v ? "On" : "Off";
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.adapters;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.apache.commons.lang.StringUtils;
/**
* Adapter to clean up string values
*
* @author Jeroen Idserda - Initial contribution
*/
public class StringAdapter extends XmlAdapter<String, String> {
@Override
public String unmarshal(String v) throws Exception {
String val = v;
if (val != null) {
val = StringUtils.trimToEmpty(val);
}
return val;
}
@Override
public String marshal(String v) throws Exception {
return v;
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.adapters;
import static org.openhab.binding.denonmarantz.internal.DenonMarantzBindingConstants.DB_OFFSET;
import java.math.BigDecimal;
import javax.xml.bind.annotation.adapters.XmlAdapter;
/**
* Maps Denon volume values in db to percentage
*
* @author Jeroen Idserda - Initial contribution
*/
public class VolumeAdapter extends XmlAdapter<String, BigDecimal> {
@Override
public BigDecimal unmarshal(String v) throws Exception {
if (v != null && !v.trim().equals("--")) {
return new BigDecimal(v.trim()).add(DB_OFFSET);
}
return BigDecimal.ZERO;
}
@Override
public String marshal(BigDecimal v) throws Exception {
if (v.equals(BigDecimal.ZERO)) {
return "--";
}
return v.subtract(DB_OFFSET).toString();
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Contains information about a Denon/Marantz receiver.
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "device_Info")
@XmlAccessorType(XmlAccessType.FIELD)
public class Deviceinfo {
private Integer deviceZones;
private String modelName;
public Integer getDeviceZones() {
return deviceZones;
}
public void setDeviceZones(Integer deviceZones) {
this.deviceZones = deviceZones;
}
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.OnOffType;
/**
* Holds information about the Main zone of the receiver
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "item")
@XmlAccessorType(XmlAccessType.FIELD)
public class Main {
private OnOffType power;
public OnOffType getPower() {
return power;
}
public void setPower(OnOffType power) {
this.power = power;
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.OnOffType;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.StringType;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.VolumeType;
/**
* Holds information about the secondary zones of the receiver
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "item")
@XmlAccessorType(XmlAccessType.FIELD)
public class ZoneStatus {
private OnOffType power;
@XmlElementWrapper(name = "inputFuncList")
@XmlElement(name = "value")
private List<String> inputFunctions;
private StringType inputFuncSelect;
private StringType volumeDisplay;
private StringType surrMode;
private VolumeType masterVolume;
private OnOffType mute;
public OnOffType getPower() {
return power;
}
public void setPower(OnOffType power) {
this.power = power;
}
public StringType getInputFuncSelect() {
return inputFuncSelect;
}
public void setInputFuncSelect(StringType inputFuncSelect) {
this.inputFuncSelect = inputFuncSelect;
}
public StringType getVolumeDisplay() {
return volumeDisplay;
}
public void setVolumeDisplay(StringType volumeDisplay) {
this.volumeDisplay = volumeDisplay;
}
public StringType getSurrMode() {
return surrMode;
}
public void setSurrMode(StringType surrMode) {
this.surrMode = surrMode;
}
public VolumeType getMasterVolume() {
return masterVolume;
}
public void setMasterVolume(VolumeType masterVolume) {
this.masterVolume = masterVolume;
}
public OnOffType getMute() {
return mute;
}
public void setMute(OnOffType mute) {
this.mute = mute;
}
public List<String> getInputFuncList() {
return this.inputFunctions;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.OnOffType;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.StringType;
import org.openhab.binding.denonmarantz.internal.xml.entities.types.VolumeType;
/**
* Holds limited information about the secondary zones of the receiver
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "item")
@XmlAccessorType(XmlAccessType.FIELD)
public class ZoneStatusLite {
private OnOffType power;
private StringType inputFuncSelect;
private StringType volumeDisplay;
private VolumeType masterVolume;
private OnOffType mute;
public OnOffType getPower() {
return power;
}
public void setPower(OnOffType power) {
this.power = power;
}
public StringType getInputFuncSelect() {
return inputFuncSelect;
}
public void setInputFuncSelect(StringType inputFuncSelect) {
this.inputFuncSelect = inputFuncSelect;
}
public StringType getVolumeDisplay() {
return volumeDisplay;
}
public void setVolumeDisplay(StringType volumeDisplay) {
this.volumeDisplay = volumeDisplay;
}
public VolumeType getMasterVolume() {
return masterVolume;
}
public void setMasterVolume(VolumeType masterVolume) {
this.masterVolume = masterVolume;
}
public OnOffType getMute() {
return mute;
}
public void setMute(OnOffType mute) {
this.mute = mute;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Wrapper for a list of {@link CommandTx}
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "tx")
@XmlAccessorType(XmlAccessType.FIELD)
public class AppCommandRequest {
@XmlElement(name = "cmd")
private List<CommandTx> commands = new ArrayList<>();
public AppCommandRequest() {
}
public List<CommandTx> getCommands() {
return commands;
}
public void setCommands(List<CommandTx> commands) {
this.commands = commands;
}
public AppCommandRequest add(CommandTx command) {
commands.add(command);
return this;
}
public static AppCommandRequest of(CommandTx command) {
AppCommandRequest tx = new AppCommandRequest();
return tx.add(command);
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Response to an {@link AppCommandRequest}, wraps a list of {@link CommandRx}
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "rx")
@XmlAccessorType(XmlAccessType.FIELD)
public class AppCommandResponse {
@XmlElement(name = "cmd")
private List<CommandRx> commands = new ArrayList<>();
public AppCommandResponse() {
}
public List<CommandRx> getCommands() {
return commands;
}
public void setCommands(List<CommandRx> commands) {
this.commands = commands;
}
}

View File

@@ -0,0 +1,203 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Response to a {@link CommandTx}
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "cmd")
@XmlAccessorType(XmlAccessType.FIELD)
public class CommandRx {
private String zone1;
private String zone2;
private String zone3;
private String zone4;
private String volume;
private String disptype;
private String dispvalue;
private String mute;
private String type;
@XmlElement(name = "text")
private List<Text> texts = new ArrayList<>();
@XmlElementWrapper(name = "functionrename")
@XmlElement(name = "list")
private List<RenameSourceList> renameSourceLists;
@XmlElementWrapper(name = "functiondelete")
@XmlElement(name = "list")
private List<DeletedSourceList> deletedSourceLists;
private String playstatus;
private String playcontents;
private String repeat;
private String shuffle;
private String source;
public CommandRx() {
}
public String getZone1() {
return zone1;
}
public void setZone1(String zone1) {
this.zone1 = zone1;
}
public String getZone2() {
return zone2;
}
public void setZone2(String zone2) {
this.zone2 = zone2;
}
public String getZone3() {
return zone3;
}
public void setZone3(String zone3) {
this.zone3 = zone3;
}
public String getZone4() {
return zone4;
}
public void setZone4(String zone4) {
this.zone4 = zone4;
}
public String getVolume() {
return volume;
}
public void setVolume(String volume) {
this.volume = volume;
}
public String getDisptype() {
return disptype;
}
public void setDisptype(String disptype) {
this.disptype = disptype;
}
public String getDispvalue() {
return dispvalue;
}
public void setDispvalue(String dispvalue) {
this.dispvalue = dispvalue;
}
public String getMute() {
return mute;
}
public void setMute(String mute) {
this.mute = mute;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getPlaystatus() {
return playstatus;
}
public void setPlaystatus(String playstatus) {
this.playstatus = playstatus;
}
public String getPlaycontents() {
return playcontents;
}
public void setPlaycontents(String playcontents) {
this.playcontents = playcontents;
}
public String getRepeat() {
return repeat;
}
public void setRepeat(String repeat) {
this.repeat = repeat;
}
public String getShuffle() {
return shuffle;
}
public void setShuffle(String shuffle) {
this.shuffle = shuffle;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getText(String key) {
for (Text text : texts) {
if (text.getId().equals(key)) {
return text.getValue();
}
}
return null;
}
public List<RenameSourceList> getRenameSourceLists() {
return renameSourceLists;
}
public List<DeletedSourceList> getDeletedSourceLists() {
return deletedSourceLists;
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
/**
* Individual commands that can be sent to a Denon/Marantz receiver to request specific information.
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "cmd")
@XmlAccessorType(XmlAccessType.FIELD)
public class CommandTx {
private static final String DEFAULT_ID = "1";
public static final CommandTx CMD_ALL_POWER = of("GetAllZonePowerStatus");
public static final CommandTx CMD_VOLUME_LEVEL = of("GetVolumeLevel");
public static final CommandTx CMD_MUTE_STATUS = of("GetMuteStatus");
public static final CommandTx CMD_SOURCE_STATUS = of("GetSourceStatus");
public static final CommandTx CMD_SURROUND_STATUS = of("GetSurroundModeStatus");
public static final CommandTx CMD_ZONE_NAME = of("GetZoneName");
public static final CommandTx CMD_NET_STATUS = of("GetNetAudioStatus");
public static final CommandTx CMD_RENAME_SOURCE = of("GetRenameSource");
public static final CommandTx CMD_DELETED_SOURCE = of("GetDeletedSource");
@XmlAttribute(name = "id")
private String id;
@XmlValue
private String value;
public CommandTx() {
}
public CommandTx(String value) {
this.value = value;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public static CommandTx of(String command) {
CommandTx cmdTx = new CommandTx(command);
cmdTx.setId(DEFAULT_ID);
return cmdTx;
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Used to unmarshall <list> items of the <functiondelete> CommandRX.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
@XmlRootElement(name = "list")
@XmlAccessorType(XmlAccessType.FIELD)
public class DeletedSourceList {
private String name;
private String funcName;
private Integer use;
public String getName() {
return name;
}
public String getFuncName() {
return funcName;
}
public Integer getUse() {
return use;
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Used to unmarshall <list> items of the <functionrename> CommandRX.
*
* @author Jan-Willem Veldhuis - Initial contribution
*/
@XmlRootElement(name = "list")
@XmlAccessorType(XmlAccessType.FIELD)
public class RenameSourceList {
private String name;
private String rename;
public String getName() {
return name;
}
public String getRename() {
return rename;
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.commands;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
/**
* Holds text values with a certain id
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlRootElement(name = "text")
@XmlAccessorType(XmlAccessType.FIELD)
public class Text {
@XmlAttribute(name = "id")
private String id;
@XmlValue
private String value;
public Text() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.types;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.openhab.binding.denonmarantz.internal.xml.adapters.OnOffAdapter;
/**
* Contains an On/Off value in the form of a boolean
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class OnOffType {
@XmlJavaTypeAdapter(OnOffAdapter.class)
private Boolean value;
public Boolean getValue() {
return value;
}
public void setValue(Boolean value) {
this.value = value;
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.types;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.openhab.binding.denonmarantz.internal.xml.adapters.StringAdapter;
/**
* Contains a string value
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class StringType {
@XmlJavaTypeAdapter(value = StringAdapter.class)
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.xml.entities.types;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.openhab.binding.denonmarantz.internal.xml.adapters.VolumeAdapter;
/**
* Contains a volume value (percentage)
*
* @author Jeroen Idserda - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class VolumeType {
@XmlJavaTypeAdapter(value = VolumeAdapter.class)
private BigDecimal value;
public BigDecimal getValue() {
return value;
}
public void setValue(BigDecimal value) {
this.value = value;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="denonmarantz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>DenonMarantz Binding</name>
<description>Binding for controlling network enabled Denon and Marantz receivers.</description>
<author>Jan-Willem Veldhuis, Jeroen Idserda</author>
</binding:binding>

View File

@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="denonmarantz"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- AVR control using Telnet -->
<thing-type id="avr">
<label>Denon/Marantz AVR</label>
<description>Control a Denon/Marantz AVR.</description>
<channel-groups>
<channel-group id="general" typeId="general"/>
<channel-group id="mainZone" typeId="zone">
<label>Main Zone Control</label>
<description>Channels for the main zone of this AVR.</description>
</channel-group>
<channel-group id="zone2" typeId="zone">
<label>Zone 2 Control</label>
<description>Channels for zone2 of this AVR.</description>
</channel-group>
<channel-group id="zone3" typeId="zone">
<label>Zone 3 Control</label>
<description>Channels for zone3 of this AVR.</description>
</channel-group>
<channel-group id="zone4" typeId="zone">
<label>Zone 4 Control</label>
<description>Channels for zone4 of this AVR.</description>
</channel-group>
</channel-groups>
<config-description>
<parameter-group name="receiverProperties">
<label>Receiver Properties</label>
</parameter-group>
<parameter-group name="connectionSettings">
<label>General Connection Settings</label>
</parameter-group>
<parameter-group name="telnetSettings">
<label>Telnet Settings</label>
<description>Settings for the Telnet port of the AVR</description>
<advanced>true</advanced>
</parameter-group>
<parameter-group name="httpSettings">
<label>HTTP Settings</label>
<description>Settings for the HTTP port of the AVR</description>
<advanced>true</advanced>
</parameter-group>
<parameter name="zoneCount" type="integer" groupName="receiverProperties">
<label>Zone Count of the Receiver</label>
<description>Number of zones (including main zone), values 1-4 are supported.</description>
<default>2</default>
</parameter>
<parameter name="host" type="text" required="true" groupName="connectionSettings">
<context>network-address</context>
<label>AVR Host or IP Address</label>
<description>Hostname or IP address of the AVR to control.</description>
</parameter>
<parameter name="telnetEnabled" type="boolean" groupName="connectionSettings">
<label>Use Telnet Port</label>
<description>By using telnet the AVR updates are received immediately. Also, some devices only support telnet.
However, the AVR only allows 1 simultaneous connection. Uncheck if you are using dedicated apps to control the AVR.
Then HTTP polling is used instead.</description>
</parameter>
<parameter name="telnetPort" type="integer" groupName="telnetSettings">
<label>Telnet Port</label>
<description>Telnet port used for AVR communication. Normally shouldn't be changed.</description>
<default>23</default>
<advanced>true</advanced>
</parameter>
<parameter name="httpPort" type="integer" groupName="httpSettings">
<label>HTTP Port</label>
<description>HTTP Port used for AVR communication. Normally shouldn't be changed.</description>
<default>80</default>
<advanced>true</advanced>
</parameter>
<parameter name="httpPollingInterval" type="integer" groupName="httpSettings">
<label>Polling Interval</label>
<description>Refresh interval of the HTTP API in seconds (minimal 5)</description>
<default>5</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="general">
<label>General Control</label>
<description>General channels for this AVR.</description>
<channels>
<channel id="power" typeId="mainPower"/>
<channel id="surroundProgram" typeId="surroundProgram"/>
<channel id="artist" typeId="artist"/>
<channel id="album" typeId="album"/>
<channel id="track" typeId="track"/>
<channel id="command" typeId="command"/>
</channels>
</channel-group-type>
<channel-group-type id="zone">
<label>Zone Control</label>
<description>Channels for a zone of this AVR.</description>
<channels>
<channel id="power" typeId="zonePower"/>
<channel id="volume" typeId="volume"/>
<channel id="volumeDB" typeId="volumeDB"/>
<channel id="mute" typeId="mute"/>
<channel id="input" typeId="input"/>
</channels>
</channel-group-type>
<channel-type id="mainPower">
<item-type>Switch</item-type>
<label>Power</label>
<description>Power ON/OFF the AVR</description>
</channel-type>
<channel-type id="zonePower">
<item-type>Switch</item-type>
<label>Power (zone)</label>
<description>Power ON/OFF this zone of the AVR</description>
</channel-type>
<channel-type id="volume">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Set the volume level of this zone</description>
<category>SoundVolume</category>
</channel-type>
<channel-type id="volumeDB" advanced="true">
<item-type>Number</item-type>
<label>Volume (dB)</label>
<description>Set the volume level (dB). Same as [mainVolume - 80].</description>
<category>SoundVolume</category>
<state min="-80" max="18" step="0.5" pattern="%.1f dB"/>
</channel-type>
<channel-type id="mute">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Enable/Disable Mute on this zone of the AVR</description>
</channel-type>
<channel-type id="input">
<item-type>String</item-type>
<label>Input Source</label>
<description>Select the input source for this zone of the AVR</description>
<state>
<options>
<option value="DVD">DVD</option>
<option value="BD">BD</option>
<option value="TV">TV</option>
<option value="SAT/CBL">SAT/CBL</option>
<option value="SAT">SAT</option>
<option value="MPLAY">MPLAY</option>
<option value="VCR">VCR</option>
<option value="GAME">GAME</option>
<option value="V.AUX">V.AUX</option>
<option value="TUNER">TUNER</option>
<option value="HDRADIO">HDRADIO</option>
<option value="SIRIUS">SIRIUS</option>
<option value="SPOTIFY">SPOTIFY</option>
<option value="SIRIUSXM">SIRIUSXM</option>
<option value="RHAPSODY">RHAPSODY</option>
<option value="PANDORA">PANDORA</option>
<option value="NAPSTER">NAPSTER</option>
<option value="LASTFM">LASTFM</option>
<option value="FLICKR">FLICKR</option>
<option value="IRADIO">IRADIO</option>
<option value="SERVER">SERVER</option>
<option value="FAVORITES">FAVORITES</option>
<option value="CDR">CDR</option>
<option value="AUX1">AUX1</option>
<option value="AUX2">AUX2</option>
<option value="AUX3">AUX3</option>
<option value="AUX4">AUX4</option>
<option value="AUX5">AUX5</option>
<option value="AUX6">AUX6</option>
<option value="AUX7">AUX7</option>
<option value="NET">NET</option>
<option value="NET/USB">NET/USB</option>
<option value="BT">BT</option>
<option value="M-XPORT">M-XPORT</option>
<option value="USB/IPOD">USB/IPOD</option>
<option value="USB">USB</option>
<option value="IPD">IPD</option>
<option value="IRP">IRP</option>
<option value="FVP">FVP</option>
<option value="OTP">OTP</option>
</options>
</state>
</channel-type>
<channel-type id="surroundProgram">
<item-type>String</item-type>
<label>Surround Program</label>
<description>Select the surround program of the AVR</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="artist">
<item-type>String</item-type>
<label>Now Playing (artist)</label>
<description>Displays the artist of the now playing song.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="album">
<item-type>String</item-type>
<label>Now Playing (album)</label>
<description>Displays the album of the now playing song.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="track">
<item-type>String</item-type>
<label>Now Playing (track)</label>
<description>Displays the title of the now playing track.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="command" advanced="true">
<item-type>String</item-type>
<label>Send a Custom Command</label>
<description>Use this channel to send any custom command, e.g. SITV or MSSTANDARD (check the protocol documentation)</description>
</channel-type>
</thing:thing-descriptions>