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,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>