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,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.onkyo-${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-onkyo" description="Onkyo Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.onkyo/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,239 @@
/**
* 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.onkyo.internal;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class to handle Onkyo Album Arts.
*
* @author Pauli Anttila - Initial contribution
* @author Stewart Cossey - fixed bug in getAlbumArt function
*/
public class OnkyoAlbumArt {
private final Logger logger = LoggerFactory.getLogger(OnkyoAlbumArt.class);
private enum State {
INVALID,
NOTSTARTED,
STARTED,
NEXT,
READY
}
private enum ImageType {
BMP,
JPEG,
URL,
NONE,
UNKNOWN
}
private StringBuilder albumArtStringBuilder = new StringBuilder();
private ImageType imageType = ImageType.UNKNOWN;
private State state = State.NOTSTARTED;
private String coverArtUrl;
public boolean isAlbumCoverTransferStarted() {
return state == State.STARTED;
}
public boolean isAlbumCoverReady() {
return state == State.READY;
}
public void clearAlbumArt() {
albumArtStringBuilder.setLength(0);
imageType = ImageType.UNKNOWN;
state = State.NOTSTARTED;
coverArtUrl = null;
}
public void addFrame(String data) {
if (data.length() <= 2) {
return;
}
char imgType = data.charAt(0);
imageType = getImageType(imgType);
char packetFlag = data.charAt(1);
String packetFlagStr = "unknown";
switch (packetFlag) {
case '0':
if (state == State.NOTSTARTED || state == State.INVALID) {
state = State.STARTED;
} else {
state = State.INVALID;
}
packetFlagStr = "Start";
albumArtStringBuilder.setLength(0);
break;
case '1':
packetFlagStr = "Next";
if (state == State.STARTED || state == State.NEXT) {
state = State.NEXT;
} else {
state = State.INVALID;
}
break;
case '2':
packetFlagStr = "End";
if (state == State.STARTED || state == State.NEXT) {
state = State.READY;
} else {
state = State.INVALID;
}
break;
case '-':
packetFlagStr = "notUsed";
state = State.READY;
break;
default:
state = State.INVALID;
logger.debug("Unknown album art packet flag '{}'", packetFlag);
}
if (state != State.INVALID) {
switch (imageType) {
case BMP:
case JPEG:
String picData = data.substring(2, data.length());
logger.debug("Received album art fragment in '{}' format, packet flag '{}', picData '{}'",
imageType, packetFlagStr, picData);
albumArtStringBuilder.append(picData);
break;
case URL:
coverArtUrl = data.substring(2);
logger.debug("Received album art url '{}'", coverArtUrl);
break;
case NONE:
logger.debug("Received information: album art not available");
break;
default:
}
} else {
logger.debug("Received album art fragment in wrong order, format '{}', packet flag '{}'", imageType,
packetFlagStr);
}
}
public byte[] getAlbumArt() throws IllegalArgumentException {
byte[] data = null;
if (state == State.READY) {
switch (imageType) {
case BMP:
case JPEG:
data = HexUtils.hexToBytes(albumArtStringBuilder.toString());
break;
case URL:
data = downloadAlbumArt(coverArtUrl);
// Workaround firmware bug providing incorrect headers causing them to be seen as body instead.
if (data != null) {
int bodyLength = data.length;
int i = new String(data).indexOf("image/");
if (i > 0) {
while (i < bodyLength && (data[i] != '\r' && data[i] != '\n')) {
i++;
}
while (i < bodyLength && (data[i] == '\r' || data[i] == '\n')) {
i++;
}
data = Arrays.copyOfRange(data, i, bodyLength);
logger.trace("Onkyo fixed picture data @ {}: {} ", i, new String(data));
}
}
break;
case NONE:
default:
}
return data;
}
throw new IllegalArgumentException("Illegal Album Art");
}
private byte[] downloadAlbumArt(String albumArtUrl) {
try {
URL url = new URL(albumArtUrl);
URLConnection connection = url.openConnection();
InputStream inputStream = connection.getInputStream();
try {
return IOUtils.toByteArray(inputStream);
} finally {
IOUtils.closeQuietly(inputStream);
}
} catch (MalformedURLException e) {
logger.warn("Album Art download failed from url '{}', reason {}", albumArtUrl, e.getMessage());
} catch (IOException e) {
logger.warn("Album Art download failed from url '{}', reason {}", albumArtUrl, e.getMessage());
}
return null;
}
private ImageType getImageType(char imgType) {
ImageType it = ImageType.UNKNOWN;
switch (imgType) {
case '0':
it = ImageType.BMP;
break;
case '1':
it = ImageType.JPEG;
break;
case '2':
it = ImageType.URL;
break;
case 'n':
it = ImageType.NONE;
break;
default:
it = ImageType.UNKNOWN;
}
return it;
}
public @NonNull String getAlbumArtMimeType() {
String mimeType = "";
switch (imageType) {
case BMP:
mimeType = "image/bmp";
break;
case JPEG:
mimeType = "image/jpeg";
break;
case UNKNOWN:
case URL:
case NONE:
default:
break;
}
return mimeType;
}
}

View File

@@ -0,0 +1,95 @@
/**
* 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.onkyo.internal;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link OnkyoBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Paul Frank - Initial contribution
* @author Pauli Anttila - update for openhab 2
* @author Stewart Cossey - add additional receiver models
* @author Wouter Born - Enumerate supported models using OnkyoModel enum
*/
@NonNullByDefault
public class OnkyoBindingConstants {
public static final String BINDING_ID = "onkyo";
// List of Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ONKYOAV = new ThingTypeUID(BINDING_ID, "onkyoAVR");
public static final ThingTypeUID THING_TYPE_ONKYO_UNSUPPORTED = new ThingTypeUID(BINDING_ID, "onkyoUnsupported");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.concat(Stream.of(THING_TYPE_ONKYOAV, THING_TYPE_ONKYO_UNSUPPORTED),
Arrays.stream(OnkyoModel.values()).map(model -> new ThingTypeUID(BINDING_ID, model.getId())))
.collect(Collectors.toSet());
// List of thing parameters names
public static final String HOST_PARAMETER = "ipAddress";
public static final String TCP_PORT_PARAMETER = "port";
public static final String UDN_PARAMETER = "udn";
public static final String REFRESH_INTERVAL = "refreshInterval";
// List of all Channel ids
public static final String CHANNEL_POWER = "zone1#power";
public static final String CHANNEL_INPUT = "zone1#input";
public static final String CHANNEL_MUTE = "zone1#mute";
public static final String CHANNEL_VOLUME = "zone1#volume";
public static final String CHANNEL_POWERZONE2 = "zone2#power";
public static final String CHANNEL_INPUTZONE2 = "zone2#input";
public static final String CHANNEL_MUTEZONE2 = "zone2#mute";
public static final String CHANNEL_VOLUMEZONE2 = "zone2#volume";
public static final String CHANNEL_POWERZONE3 = "zone3#power";
public static final String CHANNEL_INPUTZONE3 = "zone3#input";
public static final String CHANNEL_MUTEZONE3 = "zone3#mute";
public static final String CHANNEL_VOLUMEZONE3 = "zone3#volume";
public static final String CHANNEL_CONTROL = "player#control";
public static final String CHANNEL_CURRENTPLAYINGTIME = "player#currentPlayingTime";
public static final String CHANNEL_ARTIST = "player#artist";
public static final String CHANNEL_TITLE = "player#title";
public static final String CHANNEL_ALBUM = "player#album";
public static final String CHANNEL_ALBUM_ART = "player#albumArt";
public static final String CHANNEL_ALBUM_ART_URL = "player#albumArtUrl";
public static final String CHANNEL_LISTENMODE = "player#listenmode";
public static final String CHANNEL_PLAY_URI = "player#playuri";
public static final String CHANNEL_NET_MENU_TITLE = "netmenu#title";
public static final String CHANNEL_NET_MENU_CONTROL = "netmenu#control";
public static final String CHANNEL_NET_MENU_SELECTION = "netmenu#selection";
public static final String CHANNEL_NET_MENU0 = "netmenu#item0";
public static final String CHANNEL_NET_MENU1 = "netmenu#item1";
public static final String CHANNEL_NET_MENU2 = "netmenu#item2";
public static final String CHANNEL_NET_MENU3 = "netmenu#item3";
public static final String CHANNEL_NET_MENU4 = "netmenu#item4";
public static final String CHANNEL_NET_MENU5 = "netmenu#item5";
public static final String CHANNEL_NET_MENU6 = "netmenu#item6";
public static final String CHANNEL_NET_MENU7 = "netmenu#item7";
public static final String CHANNEL_NET_MENU8 = "netmenu#item8";
public static final String CHANNEL_NET_MENU9 = "netmenu#item9";
// Used for Discovery service
public static final String MANUFACTURER = "ONKYO";
public static final String UPNP_DEVICE_TYPE = "MediaRenderer";
}

View File

@@ -0,0 +1,391 @@
/**
* 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.onkyo.internal;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.io.IOUtils;
import org.openhab.binding.onkyo.internal.eiscp.EiscpCommand;
import org.openhab.binding.onkyo.internal.eiscp.EiscpException;
import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
import org.openhab.binding.onkyo.internal.eiscp.EiscpProtocol;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class open a TCP/IP connection to the Onkyo device and send a command.
*
* @author Pauli Anttila - Initial contribution
*/
public class OnkyoConnection {
private final Logger logger = LoggerFactory.getLogger(OnkyoConnection.class);
/** default eISCP port. **/
public static final int DEFAULT_EISCP_PORT = 60128;
/** Connection timeout in milliseconds **/
private static final int CONNECTION_TIMEOUT = 5000;
/** Connection test interval in milliseconds **/
private static final int CONNECTION_TEST_INTERVAL = 60000;
/** Socket timeout in milliseconds **/
private static final int SOCKET_TIMEOUT = CONNECTION_TEST_INTERVAL + 10000;
/** Connection retry count on error situations **/
private static final int FAST_CONNECTION_RETRY_COUNT = 3;
/** Connection retry delays in milliseconds **/
private static final int FAST_CONNECTION_RETRY_DELAY = 1000;
private static final int SLOW_CONNECTION_RETRY_DELAY = 60000;
private String ip;
private int port;
private Socket eiscpSocket;
private DataListener dataListener;
private DataOutputStream outStream;
private DataInputStream inStream;
private boolean connected;
private List<OnkyoEventListener> listeners = new ArrayList<>();
private int retryCount = 1;
private ConnectionSupervisor connectionSupervisor;
public OnkyoConnection(String ip) {
this.ip = ip;
this.port = DEFAULT_EISCP_PORT;
}
public OnkyoConnection(String ip, int port) {
this.ip = ip;
this.port = port;
}
/**
* Open connection to the Onkyo device.
*/
public void openConnection() {
connectSocket();
}
/**
* Closes the connection to the Onkyo device.
*/
public void closeConnection() {
closeSocket();
}
public void addEventListener(OnkyoEventListener listener) {
this.listeners.add(listener);
}
public void removeEventListener(OnkyoEventListener listener) {
this.listeners.remove(listener);
}
public String getConnectionName() {
return ip + ":" + port;
}
public boolean isConnected() {
return connected;
}
/**
* Sends a command to Onkyo device.
*
* @param cmd eISCP command to send
*/
public void send(final String cmd, final String value) {
try {
sendCommand(new EiscpMessage.MessageBuilder().command(cmd).value(value).build());
} catch (Exception e) {
logger.warn("Could not send command to device on {}:{}: ", ip, port, e);
}
}
private void sendCommand(EiscpMessage msg) {
logger.debug("Send command: {} to {}:{} ({})", msg.toString(), ip, port, eiscpSocket);
sendCommand(msg, retryCount);
}
/**
* Sends to command to the receiver.
*
* @param eiscpCmd the eISCP command to send.
* @param retry retry count when connection fails.
*/
private void sendCommand(EiscpMessage msg, int retry) {
if (connectSocket()) {
try {
String data = EiscpProtocol.createEiscpPdu(msg);
if (logger.isTraceEnabled()) {
logger.trace("Sending {} bytes: {}", data.length(), HexUtils.bytesToHex(data.getBytes()));
}
outStream.writeBytes(data);
outStream.flush();
} catch (IOException ioException) {
logger.warn("Error occurred when sending command: {}", ioException.getMessage());
if (retry > 0) {
logger.debug("Retry {}...", retry);
closeSocket();
sendCommand(msg, retry - 1);
} else {
sendConnectionErrorEvent(ioException.getMessage());
}
}
}
}
/**
* Connects to the receiver by opening a socket connection through the
* IP and port.
*/
private synchronized boolean connectSocket() {
if (eiscpSocket == null || !connected || !eiscpSocket.isConnected()) {
try {
// Creating a socket to connect to the server
eiscpSocket = new Socket();
// start connection tester
if (connectionSupervisor == null) {
connectionSupervisor = new ConnectionSupervisor(CONNECTION_TEST_INTERVAL);
}
eiscpSocket.connect(new InetSocketAddress(ip, port), CONNECTION_TIMEOUT);
logger.debug("Connected to {}:{}", ip, port);
// Get Input and Output streams
outStream = new DataOutputStream(eiscpSocket.getOutputStream());
inStream = new DataInputStream(eiscpSocket.getInputStream());
eiscpSocket.setSoTimeout(SOCKET_TIMEOUT);
outStream.flush();
connected = true;
// start status update listener
if (dataListener == null) {
dataListener = new DataListener();
dataListener.start();
}
} catch (UnknownHostException unknownHost) {
logger.debug("You are trying to connect to an unknown host: {}", unknownHost.getMessage());
sendConnectionErrorEvent(unknownHost.getMessage());
} catch (IOException ioException) {
logger.debug("Can't connect: {}", ioException.getMessage());
sendConnectionErrorEvent(ioException.getMessage());
}
}
return connected;
}
/**
* Closes the socket connection.
*
* @return true if the closed successfully
*/
private boolean closeSocket() {
try {
if (dataListener != null) {
dataListener.setInterrupted(true);
dataListener = null;
logger.debug("closed data listener!");
}
if (connectionSupervisor != null) {
connectionSupervisor.stopConnectionTester();
connectionSupervisor = null;
logger.debug("closed connection tester!");
}
if (inStream != null) {
IOUtils.closeQuietly(inStream);
inStream = null;
logger.debug("closed input stream!");
}
if (outStream != null) {
IOUtils.closeQuietly(outStream);
outStream = null;
logger.debug("closed output stream!");
}
if (eiscpSocket != null) {
IOUtils.closeQuietly(eiscpSocket);
eiscpSocket = null;
logger.debug("closed socket!");
}
connected = false;
} catch (Exception e) {
logger.debug("Closing connection throws an exception, {}", e.getMessage());
}
return connected;
}
/**
* This method wait any state messages form receiver.
*
* @throws IOException
* @throws InterruptedException
* @throws EiscpException
*/
private void waitStateMessages() throws NumberFormatException, IOException, InterruptedException, EiscpException {
if (connected) {
logger.trace("Waiting status messages");
while (true) {
EiscpMessage message = EiscpProtocol.getNextMessage(inStream);
sendMessageEvent(message);
}
} else {
throw new IOException("Not Connected to Receiver");
}
}
private class DataListener extends Thread {
private boolean interrupted = false;
DataListener() {
}
public void setInterrupted(boolean interrupted) {
this.interrupted = interrupted;
this.interrupt();
}
@Override
public void run() {
logger.debug("Data listener started");
boolean restartConnection = false;
long connectionAttempts = 0;
// as long as no interrupt is requested, continue running
while (!interrupted) {
try {
waitStateMessages();
connectionAttempts = 0;
} catch (EiscpException e) {
logger.debug("Error occurred during message waiting: {}", e.getMessage());
} catch (SocketTimeoutException e) {
logger.debug("No data received during supervision interval ({} ms)!", SOCKET_TIMEOUT);
restartConnection = true;
} catch (Exception e) {
if (!interrupted && !this.isInterrupted()) {
logger.debug("Error occurred during message waiting: {}", e.getMessage());
restartConnection = true;
// sleep a while, to prevent fast looping if error situation is permanent
if (++connectionAttempts < FAST_CONNECTION_RETRY_COUNT) {
mysleep(FAST_CONNECTION_RETRY_DELAY);
} else {
// slow down after few faster attempts
if (connectionAttempts == FAST_CONNECTION_RETRY_COUNT) {
logger.debug(
"Connection failed {} times to {}:{}, slowing down automatic connection to {} seconds.",
FAST_CONNECTION_RETRY_COUNT, ip, port, SLOW_CONNECTION_RETRY_DELAY / 1000);
}
mysleep(SLOW_CONNECTION_RETRY_DELAY);
}
}
}
if (restartConnection) {
restartConnection = false;
// reopen connection
logger.debug("Reconnecting...");
try {
connected = false;
connectSocket();
logger.debug("Test connection to {}:{}", ip, port);
sendCommand(new EiscpMessage.MessageBuilder().command(EiscpCommand.POWER_QUERY.getCommand())
.value(EiscpCommand.POWER_QUERY.getValue()).build());
} catch (Exception ex) {
logger.debug("Reconnection invoking error: {}", ex.getMessage());
sendConnectionErrorEvent(ex.getMessage());
}
}
}
logger.debug("Data listener stopped");
}
private void mysleep(long milli) {
try {
sleep(milli);
} catch (InterruptedException e) {
interrupted = true;
}
}
}
private class ConnectionSupervisor {
private Timer timer;
public ConnectionSupervisor(int milliseconds) {
logger.debug("Connection supervisor started, interval {} milliseconds", milliseconds);
timer = new Timer();
timer.schedule(new Task(), milliseconds, milliseconds);
}
public void stopConnectionTester() {
timer.cancel();
}
class Task extends TimerTask {
@Override
public void run() {
logger.debug("Test connection to {}:{}", ip, port);
sendCommand(new EiscpMessage.MessageBuilder().command(EiscpCommand.POWER_QUERY.getCommand())
.value(EiscpCommand.POWER_QUERY.getValue()).build());
}
}
}
private void sendConnectionErrorEvent(String errorMsg) {
// send message to event listeners
try {
for (OnkyoEventListener listener : listeners) {
listener.connectionError(ip, errorMsg);
}
} catch (Exception ex) {
logger.debug("Event listener invoking error: {}", ex.getMessage());
}
}
private void sendMessageEvent(EiscpMessage message) {
// send message to event listeners
try {
for (OnkyoEventListener listener : listeners) {
listener.statusUpdateReceived(ip, message);
}
} catch (Exception e) {
logger.debug("Event listener invoking error: {}", e.getMessage());
}
}
}

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.onkyo.internal;
import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
/**
* This interface defines interface to receive status updates from Onkyo receiver.
*
* @author Pauli Anttila - Initial contribution
*/
public interface OnkyoEventListener {
/**
* Procedure for receive status update from Onkyo AV receiver.
*
* @param data
* Received data.
*/
void statusUpdateReceived(String ip, EiscpMessage data);
/**
* Procedure for connection error events from Onkyo AV receiver.
*
* @param errorMsg
* Reason for error.
*/
void connectionError(String ip, String errorMsg);
}

View File

@@ -0,0 +1,159 @@
/**
* 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.onkyo.internal;
import static org.openhab.binding.onkyo.internal.OnkyoBindingConstants.SUPPORTED_THING_TYPES_UIDS;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.openhab.binding.onkyo.internal.handler.OnkyoHandler;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.io.transport.upnp.UpnpIOService;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
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.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link OnkyoHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Paul Frank - Initial contribution
* @author Stewart Cossey - added dynamic state descriptor provider functions
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.onkyo")
public class OnkyoHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(OnkyoHandlerFactory.class);
private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
private UpnpIOService upnpIOService;
private AudioHTTPServer audioHTTPServer;
private NetworkAddressService networkAddressService;
private OnkyoStateDescriptionProvider stateDescriptionProvider;
// url (scheme+server+port) to use for playing notification sounds
private String callbackUrl;
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
Dictionary<String, Object> properties = componentContext.getProperties();
callbackUrl = (String) properties.get("callbackUrl");
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
String callbackUrl = createCallbackUrl();
OnkyoHandler handler = new OnkyoHandler(thing, upnpIOService, audioHTTPServer, callbackUrl,
stateDescriptionProvider);
if (callbackUrl != null) {
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), handler, new Hashtable<>());
audioSinkRegistrations.put(thing.getUID().toString(), reg);
}
return handler;
}
return null;
}
private String createCallbackUrl() {
if (callbackUrl != null) {
return callbackUrl;
} else {
final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
if (ipAddress == null) {
logger.warn("No network interface could be found.");
return null;
}
// we do not use SSL as it can cause certificate validation issues.
final int port = HttpServiceUtil.getHttpServicePort(bundleContext);
if (port == -1) {
logger.warn("Cannot find port of the http service.");
return null;
}
return "http://" + ipAddress + ":" + port;
}
}
@Override
public void unregisterHandler(Thing thing) {
super.unregisterHandler(thing);
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.get(thing.getUID().toString());
if (reg != null) {
reg.unregister();
}
}
@Reference
protected void setUpnpIOService(UpnpIOService upnpIOService) {
this.upnpIOService = upnpIOService;
}
protected void unsetUpnpIOService(UpnpIOService upnpIOService) {
this.upnpIOService = null;
}
@Reference
protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = audioHTTPServer;
}
protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = null;
}
@Reference
protected void setNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = networkAddressService;
}
protected void unsetNetworkAddressService(NetworkAddressService networkAddressService) {
this.networkAddressService = null;
}
@Reference
protected void setDynamicStateDescriptionProvider(OnkyoStateDescriptionProvider provider) {
this.stateDescriptionProvider = provider;
}
protected void unsetDynamicStateDescriptionProvider(OnkyoStateDescriptionProvider provider) {
this.stateDescriptionProvider = null;
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.onkyo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enumerates all supported Onkyo models.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum OnkyoModel {
// Please also add new supported models to README.md
HT_RC560("HT-RC560"),
TX_NR414("TX-NR414"),
TX_NR474("TX-NR474"),
TX_NR509("TX-NR509"),
TX_NR515("TX-NR515"),
TX_NR525("TX-NR525"),
TX_NR535("TX-NR535"),
TX_NR545("TX-NR545"),
TX_NR555("TX-NR555"),
TX_NR575("TX-NR575"),
TX_NR575E("TX-NR575E"),
TX_NR616("TX-NR616"),
TX_NR626("TX-NR626"),
TX_NR636("TX-NR636"),
TX_NR646("TX-NR646"),
TX_NR656("TX-NR656"),
TX_NR676("TX-NR676"),
TX_NR686("TX-NR686"),
TX_NR708("TX-NR708"),
TX_NR717("TX-NR717"),
TX_NR727("TX-NR727"),
TX_NR737("TX-NR737"),
TX_NR747("TX-NR747"),
TX_NR757("TX-NR757"),
TX_NR809("TX-NR809"),
TX_NR818("TX-NR818"),
TX_NR828("TX-NR828"),
TX_NR838("TX-NR838"),
TX_NR3007("TX-NR3007"),
TX_RZ900("TX-RZ900");
private final String id;
private OnkyoModel(String id) {
this.id = id;
}
public String getId() {
return id;
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.onkyo.internal;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
/**
* The {@link OnkyoStateDescriptionProvider} class is a dynamic provider of state options while leaving other state
* description fields as original.
*
* @author Gregory Moyer - Initial contribution
* @author Stewart Cossey - Adapted for Onkyo Binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, OnkyoStateDescriptionProvider.class })
@NonNullByDefault
public class OnkyoStateDescriptionProvider implements DynamicStateDescriptionProvider {
private final Map<ChannelUID, @Nullable List<StateOption>> channelOptionsMap = new ConcurrentHashMap<>();
public void setStateOptions(ChannelUID channelUID, List<StateOption> options) {
channelOptionsMap.put(channelUID, options);
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
@Nullable Locale locale) {
List<StateOption> options = channelOptionsMap.get(channel.getUID());
if (options == null) {
return null;
}
StateDescriptionFragmentBuilder builder = (original == null) ? StateDescriptionFragmentBuilder.create()
: StateDescriptionFragmentBuilder.create(original);
return builder.withOptions(options).build().toStateDescription();
}
@Deactivate
public void deactivate() {
channelOptionsMap.clear();
}
}

View File

@@ -0,0 +1,133 @@
/**
* 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.onkyo.internal;
/**
* List of Onkyo Net service types
*
* @author Marcel Verpaalen - Initial contribution
*/
public enum ServiceType {
MUSIC_SERVER(0x00),
FAVORITE(0x01),
VTUNER(0x02),
SIRIUSXM(0x03),
PANDORA(0x04),
RHAPSODY(0x06),
LASTFM(0x06),
NAPSTER(0x07),
SLACKER(0x08),
MEDIAFLY(0x09),
SPOTIFY(0x0A),
AUPEO(0x0B),
RADIKO(0x0C),
EONKYO(0x0D),
TUNEIN(0x0E),
MP3TUNES(0x0F),
SIMFY(0x10),
HOMEMEDIA(0x11),
DEEZER(0x12),
IHEARTRADIO(0x13),
AIRPLAY(0x18),
TIDAL(0x19),
ONKYO_MUSIC(0x1A),
USB(0xF0),
USB_REAR(0xF1),
INTERNETRADIO(0xF2),
NET(0xF3),
NONE(0xFF);
private final int id;
ServiceType(int id) {
this.id = id;
}
public int getId() {
return id;
}
public static ServiceType getType(int value) {
for (ServiceType st : ServiceType.values()) {
if (st.getId() == value) {
return st;
}
}
return NONE;
}
@Override
public String toString() {
switch (this) {
case MUSIC_SERVER:
return "Music Server (DLNA)";
case FAVORITE:
return "Favorite";
case VTUNER:
return "vTuner";
case SIRIUSXM:
return "SiriusXM";
case PANDORA:
return "Pandora";
case RHAPSODY:
return "Rhapsody";
case LASTFM:
return "Last.fm";
case NAPSTER:
return "Napster";
case SLACKER:
return "Slacker";
case MEDIAFLY:
return "Mediafly";
case SPOTIFY:
return "Spotify";
case AUPEO:
return "AUPEO!";
case RADIKO:
return "radiko";
case EONKYO:
return "e-onkyo";
case TUNEIN:
return "TuneIn Radio";
case MP3TUNES:
return "MP3tunes";
case SIMFY:
return "Simfy";
case HOMEMEDIA:
return "Home Media";
case DEEZER:
return "Deezer";
case IHEARTRADIO:
return "iHeartRadio";
case AIRPLAY:
return "Airplay";
case TIDAL:
return "TIDAL";
case ONKYO_MUSIC:
return "onkyo music";
case USB:
return "USB/USB(Front)";
case USB_REAR:
return "USB(Rear)";
case INTERNETRADIO:
return "Internet Radio";
case NET:
return "NET";
case NONE:
return "None";
default:
return "Invalid/unknown";
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.onkyo.internal.automation.modules;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link OnkyoThingActions} defines the interface for all thing actions supported by the binding.
*
* @author Laurent Garnier - initial contribution
*/
@NonNullByDefault
public interface OnkyoThingActions {
public void sendRawCommand(@Nullable String command, @Nullable String value);
}

View File

@@ -0,0 +1,96 @@
/**
* 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.onkyo.internal.automation.modules;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.onkyo.internal.handler.OnkyoHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Some automation actions to be used with a {@link OnkyoThingActionsService}
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof OnkyoThingActionsService</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link OnkyoThingActionsService} class
* can be loaded by a different classloader than the <i>actions</i> instance.
*
* @author David Masshardt - initial contribution
*
*/
@ThingActionsScope(name = "onkyo")
@NonNullByDefault
public class OnkyoThingActionsService implements ThingActions, OnkyoThingActions {
private final Logger logger = LoggerFactory.getLogger(OnkyoThingActionsService.class);
private @Nullable OnkyoHandler handler;
@Override
@SuppressWarnings("null")
@RuleAction(label = "Onkyo sendRawCommand", description = "Action that sends raw command to the receiver")
public void sendRawCommand(@ActionInput(name = "command") @Nullable String command,
@ActionInput(name = "command") @Nullable String value) {
logger.debug("sendRawCommand called with raw command: {} value: {}", command, value);
if (handler == null) {
logger.warn("Onkyo Action service ThingHandler is null!");
return;
}
handler.sendRawCommand(command, value);
}
public static void sendRawCommand(@Nullable ThingActions actions, @Nullable String command,
@Nullable String value) {
invokeMethodOf(actions).sendRawCommand(command, value);
}
private static OnkyoThingActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(OnkyoThingActionsService.class.getName())) {
if (actions instanceof OnkyoThingActions) {
return (OnkyoThingActions) actions;
} else {
return (OnkyoThingActions) Proxy.newProxyInstance(OnkyoThingActions.class.getClassLoader(),
new Class[] { OnkyoThingActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of OnkyoThingActionsService");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof OnkyoHandler) {
this.handler = (OnkyoHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.onkyo.internal.config;
/**
* Configuration class for {@link OnkyoBinding} device.
*
* @author Pauli Anttila - Initial contribution
*/
public class OnkyoDeviceConfiguration {
public String ipAddress;
public int port;
public String udn;
public int refreshInterval;
public int volumeLimit;
public double volumeScale = 1.0d;
@Override
public String toString() {
String str = "";
str += "ipAddress = " + ipAddress;
str += ", port = " + port;
str += ", udn = " + udn;
str += ", refreshInterval = " + refreshInterval;
str += ", volumeLimit = " + volumeLimit;
str += ", volumeScale = " + volumeScale;
return str;
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.onkyo.internal.discovery;
import static org.openhab.binding.onkyo.internal.OnkyoBindingConstants.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.binding.onkyo.internal.OnkyoModel;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An UpnpDiscoveryParticipant which allows to discover Onkyo AVRs.
*
* @author Paul Frank - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true)
public class OnkyoUpnpDiscoveryParticipant implements UpnpDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(OnkyoUpnpDiscoveryParticipant.class);
private boolean isAutoDiscoveryEnabled;
private Set<ThingTypeUID> supportedThingTypes;
public OnkyoUpnpDiscoveryParticipant() {
this.isAutoDiscoveryEnabled = true;
this.supportedThingTypes = SUPPORTED_THING_TYPES_UIDS;
}
/**
* Called at the service activation.
*
* @param componentContext
*/
@Activate
protected void activate(ComponentContext componentContext) {
if (componentContext.getProperties() != null) {
String autoDiscoveryPropertyValue = (String) componentContext.getProperties().get("enableAutoDiscovery");
if (StringUtils.isNotEmpty(autoDiscoveryPropertyValue)) {
isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue);
}
}
supportedThingTypes = isAutoDiscoveryEnabled ? SUPPORTED_THING_TYPES_UIDS : new HashSet<>();
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return supportedThingTypes;
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
DiscoveryResult result = null;
ThingUID thingUid = getThingUID(device);
if (thingUid != null) {
String label = StringUtils.isEmpty(device.getDetails().getFriendlyName()) ? device.getDisplayString()
: device.getDetails().getFriendlyName();
Map<String, Object> properties = new HashMap<>(2, 1);
properties.put(HOST_PARAMETER, device.getIdentity().getDescriptorURL().getHost());
properties.put(UDN_PARAMETER, device.getIdentity().getUdn().getIdentifierString());
result = DiscoveryResultBuilder.create(thingUid).withLabel(label).withProperties(properties).build();
}
return result;
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
ThingUID result = null;
if (isAutoDiscoveryEnabled) {
if (StringUtils.containsIgnoreCase(device.getDetails().getManufacturerDetails().getManufacturer(),
MANUFACTURER)) {
logger.debug("Manufacturer matched: search: {}, device value: {}.", MANUFACTURER,
device.getDetails().getManufacturerDetails().getManufacturer());
if (StringUtils.containsIgnoreCase(device.getType().getType(), UPNP_DEVICE_TYPE)) {
logger.debug("Device type matched: search: {}, device value: {}.", UPNP_DEVICE_TYPE,
device.getType().getType());
String deviceModel = device.getDetails().getModelDetails() != null
? device.getDetails().getModelDetails().getModelName()
: null;
logger.debug("Device model: {}.", deviceModel);
ThingTypeUID thingTypeUID = findThingType(deviceModel);
result = new ThingUID(thingTypeUID, device.getIdentity().getUdn().getIdentifierString());
}
}
}
return result;
}
private ThingTypeUID findThingType(@Nullable String deviceModel) {
ThingTypeUID thingTypeUID = THING_TYPE_ONKYO_UNSUPPORTED;
for (ThingTypeUID thingType : SUPPORTED_THING_TYPES_UIDS) {
if (thingType.getId().equalsIgnoreCase(deviceModel)) {
return thingType;
}
}
if (isSupportedDeviceModel(deviceModel)) {
thingTypeUID = THING_TYPE_ONKYOAV;
}
return thingTypeUID;
}
/**
* Return true only if the given device model is supported.
*
* @param deviceModel
* @return
*/
private boolean isSupportedDeviceModel(final @Nullable String deviceModel) {
return StringUtils.isNotBlank(deviceModel) && Arrays.stream(OnkyoModel.values())
.anyMatch(model -> StringUtils.startsWithIgnoreCase(deviceModel, model.getId()));
}
}

View File

@@ -0,0 +1,226 @@
/**
* 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.onkyo.internal.eiscp;
/**
* Represents all possible eISCP commands.
*
* @author Thomas.Eichstaedt-Engelen - initial contribution
* @author Pauli Anttila - add additional commands
* @author Paul Frank - update for openHAB 2
* @author Marcel Verpaalen - fix issues with some Zone 2 commands
*/
public enum EiscpCommand {
// Main zone
POWER_QUERY("PWR", "QSTN"),
POWER_SET("PWR", "%02X"),
POWER("PWR", ""),
MUTE_QUERY("AMT", "QSTN"),
MUTE_SET("AMT", "%02X"),
MUTE("AMT", ""),
VOLUME_UP("MVL", "UP"),
VOLUME_DOWN("MVL", "DOWN"),
VOLUME_QUERY("MVL", "QSTN"),
VOLUME_SET("MVL", "%02X"),
VOLUME("MVL", ""),
SOURCE_UP("SLI", "UP"),
SOURCE_DOWN("SLI", "DOWN"),
SOURCE_QUERY("SLI", "QSTN"),
SOURCE_SET("SLI", "%02X"),
SOURCE("SLI", ""),
LISTEN_MODE_UP("LMD", "UP"),
LISTEN_MODE_DOWN("LMD", "DOWN"),
LISTEN_MODE_QUERY("LMD", "QSTN"),
LISTEN_MODE_SET("LMD", "%02X"),
LISTEN_MODE("LMD", ""),
INFO_QUERY("NRI", "QSTN"),
INFO("NRI", ""),
NETUSB_OP_PLAY("NTC", "PLAY"),
NETUSB_OP_STOP("NTC", "STOP"),
NETUSB_OP_PAUSE("NTC", "PAUSE"),
NETUSB_OP_TRACKUP("NTC", "TRUP"),
NETUSB_OP_TRACKDWN("NTC", "TRDN"),
NETUSB_OP_FF("NTC", "FF"),
NETUSB_OP_REW("NTC", "REW"),
NETUSB_OP_REPEAT("NTC", "REPEAT"),
NETUSB_OP_RANDOM("NTC", "RANDOM"),
NETUSB_OP_DISPLAY("NTC", "DISPLAY"),
NETUSB_OP_RIGHT("NTC", "RIGHT"),
NETUSB_OP_LEFT("NTC", "LEFT"),
NETUSB_OP_UP("NTC", "UP"),
NETUSB_OP_DOWN("NTC", "DOWN"),
NETUSB_OP_SELECT("NTC", "SELECT"),
NETUSB_OP_1("NTC", "1"),
NETUSB_OP_2("NTC", "2"),
NETUSB_OP_3("NTC", "3"),
NETUSB_OP_4("NTC", "4"),
NETUSB_OP_5("NTC", "5"),
NETUSB_OP_6("NTC", "6"),
NETUSB_OP_7("NTC", "7"),
NETUSB_OP_8("NTC", "8"),
NETUSB_OP_9("NTC", "9"),
NETUSB_OP_0("NTC", "0"),
NETUSB_OP_DELETE("NTC", "DELETE"),
NETUSB_OP_CAPS("NTC", "CAPS"),
NETUSB_OP_SETUP("NTC", "SETUP"),
NETUSB_OP_RETURN("NTC", "RETURN"),
NETUSB_OP_CHANUP("NTC", "CHUP"),
NETUSB_OP_CHANDWN("NTC", "CHDN"),
NETUSB_OP_MENU("NTC", "MENU"),
NETUSB_OP_TOPMENU("NTC", "TOP"),
NETUSB_SONG_ARTIST_QUERY("NAT", "QSTN"),
NETUSB_SONG_ARTIST("NAT", ""),
NETUSB_SONG_ALBUM_QUERY("NAL", "QSTN"),
NETUSB_SONG_ALBUM("NAL", ""),
NETUSB_SONG_TITLE_QUERY("NTI", "QSTN"),
NETUSB_SONG_TITLE("NTI", ""),
NETUSB_SONG_ELAPSEDTIME_QUERY("NTM", "QSTN"),
NETUSB_SONG_ELAPSEDTIME("NTM", ""),
NETUSB_SONG_TRACK_QUERY("NTR", "QSTN"),
NETUSB_SONG_TRACK("NTR", ""),
NETUSB_PLAY_STATUS_QUERY("NST", "QSTN"),
NETUSB_PLAY_STATUS("NST", ""),
NETUSB_MENU_SELECT("NLS", "L%X"),
NETUSB_MENU("NLS", ""),
NETUSB_TITLE("NLT", ""),
NETUSB_TITLE_QUERY("NLT", "QSTN"),
NETUSB_ALBUM_ART_QUERY("NJA", "REQ"),
NETUSB_ALBUM_ART("NJA", ""),
/*
* Zone 2
*/
ZONE2_POWER_QUERY("ZPW", "QSTN"),
ZONE2_POWER_SET("ZPW", "%02X"),
ZONE2_POWER("ZPW", ""),
ZONE2_MUTE_QUERY("ZMT", "QSTN"),
ZONE2_MUTE_SET("ZMT", "%02X"),
ZONE2_MUTE("ZMT", ""),
ZONE2_VOLUME_UP("ZVL", "UP"),
ZONE2_VOLUME_DOWN("ZVL", "DOWN"),
ZONE2_VOLUME_QUERY("ZVL", "QSTN"),
ZONE2_VOLUME_SET("ZVL", "%02X"),
ZONE2_VOLUME("ZVL", ""),
ZONE2_SOURCE_UP("SLZ", "UP"),
ZONE2_SOURCE_DOWN("SLZ", "DOWN"),
ZONE2_SOURCE_QUERY("SLZ", "QSTN"),
ZONE2_SOURCE_SET("SLZ", "%02X"),
ZONE2_SOURCE("SLZ", ""),
/*
* Zone 3
*/
ZONE3_POWER_QUERY("PW3", "QSTN"),
ZONE3_POWER_SET("PW3", "%02X"),
ZONE3_POWER("PW3", ""),
ZONE3_MUTE_QUERY("MT3", "QSTN"),
ZONE3_MUTE_SET("MT3", "%02X"),
ZONE3_MUTE("MT3", ""),
ZONE3_VOLUME_UP("VL3", "UP"),
ZONE3_VOLUME_DOWN("VL3", "DOWN"),
ZONE3_VOLUME_QUERY("VL3", "QSTN"),
ZONE3_VOLUME_SET("VL3", "%02X"),
ZONE3_VOLUME("VL3", ""),
ZONE3_SOURCE_UP("SL3", "UP"),
ZONE3_SOURCE_DOWN("SL3", "DOWN"),
ZONE3_SOURCE_QUERY("SL3", "QSTN"),
ZONE3_SOURCE_SET("SL3", "%02X"),
ZONE3_SOURCE("SL3", "");
public static enum Zone {
MAIN,
ZONE1,
ZONE2,
ZONE3
}
private String command;
private String value;
private EiscpCommand(String command, String value) {
this.command = command;
this.value = value;
}
/**
* @return the iscp command string (example 'PWR')
*/
public String getCommand() {
return command;
}
/**
* @return the iscp value string (example 'QSTN')
*/
public String getValue() {
return value;
}
public static EiscpCommand getCommandForZone(Zone zone, EiscpCommand baseCommand) throws IllegalArgumentException {
if (zone == Zone.MAIN || zone == Zone.ZONE1) {
return baseCommand;
} else {
return EiscpCommand.valueOf(zone.toString() + "_" + baseCommand);
}
}
/**
* @param command the command to find a matching command name for.
* @return the commandName that is associated with the passed command.
*/
public static EiscpCommand getCommandByCommandStr(String command) throws IllegalArgumentException {
for (EiscpCommand candidate : values()) {
if (candidate.getCommand().equals(command)) {
return candidate;
}
}
throw new IllegalArgumentException("There is no matching commandName for command '" + command + "'");
}
/**
* @param command the command to find a matching command name for.
* @param value the value to find a matching value for.
*
* @return the commandName that is associated with the passed command.
*/
public static EiscpCommand getCommandByCommandAndValueStr(String command, String value)
throws IllegalArgumentException {
for (EiscpCommand candidate : values()) {
if (candidate.getCommand().equals(command) && candidate.getValue().equals(value)) {
return candidate;
}
}
throw new IllegalArgumentException(
"There is no matching commandName for command '" + command + "' and 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.onkyo.internal.eiscp;
/**
* Exception for eISCP errors.
*
* @author Pauli Anttila - Initial contribution
*/
public class EiscpException extends Exception {
private static final long serialVersionUID = -7970958467980752003L;
public EiscpException() {
super();
}
public EiscpException(String message) {
super(message);
}
public EiscpException(String message, Throwable cause) {
super(message, cause);
}
public EiscpException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.onkyo.internal.eiscp;
/**
* Class to handle Onkyo eISCP messages.
*
* @author Pauli Anttila - Initial contribution
*/
public class EiscpMessage {
private String command = "";
private String value = "";
private EiscpMessage(MessageBuilder messageBuilder) {
this.command = messageBuilder.command;
this.value = messageBuilder.value;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
String str = "[";
str += "command=" + command;
str += ", value=" + value;
str += "]";
return str;
}
public static class MessageBuilder {
private String command;
private String value;
public MessageBuilder command(String command) {
this.command = command;
return this;
}
public MessageBuilder value(String value) {
this.value = value;
return this;
}
public EiscpMessage build() {
return new EiscpMessage(this);
}
}
}

View File

@@ -0,0 +1,270 @@
/**
* 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.onkyo.internal.eiscp;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Arrays;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class to handle Onkyo eISCP protocol.
*
* @author Pauli Anttila - Initial contribution
*/
public class EiscpProtocol {
private static final Logger LOGGER = LoggerFactory.getLogger(EiscpProtocol.class);
/**
* Wraps a command in a eISCP data message (data characters).
*
* @param msg
* eISCP command.
* @return String holding the full eISCP message packet
*/
public static String createEiscpPdu(EiscpMessage msg) {
String data = msg.getCommand() + msg.getValue();
StringBuilder sb = new StringBuilder();
int eiscpDataSize = 2 + data.length() + 1; // this is the eISCP data size
/*
* This is where I construct the entire message character by character.
* Each char is represented by a 2 digit hex value
*/
sb.append("ISCP");
// the following are all in HEX representing one char
// 4 char Big Endian Header
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x10);
// 4 char Big Endian data size
sb.append((char) ((eiscpDataSize >> 24) & 0xFF));
sb.append((char) ((eiscpDataSize >> 16) & 0xFF));
sb.append((char) ((eiscpDataSize >> 8) & 0xFF));
sb.append((char) (eiscpDataSize & 0xFF));
// eISCP version = "01";
sb.append((char) 0x01);
// 3 chars reserved = "00"+"00"+"00";
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x00);
// eISCP data
// Start Character
sb.append("!");
// eISCP data - unit type char '1' is receiver
sb.append("1");
// eISCP data - 3 char command and param ie PWR01
sb.append(data);
// msg end - EOF
sb.append((char) 0x0D);
if (LOGGER.isTraceEnabled()) {
String d = sb.toString();
LOGGER.trace("Created eISCP message: {} -> {}", HexUtils.bytesToHex(d.getBytes()), toPrintable(d));
}
return sb.toString();
}
/**
* Method to read eISCP message from input stream.
*
* @return message
*
* @throws IOException
* @throws InterruptedException
* @throws EiscpException
*/
public static EiscpMessage getNextMessage(DataInputStream stream)
throws IOException, InterruptedException, EiscpException {
while (true) {
// 1st 4 chars are the lead in
byte firstByte = stream.readByte();
if (firstByte != 'I') {
LOGGER.trace("Expected character 'I', received '{}'",
toPrintable(new String(new byte[] { firstByte })));
continue;
}
if (stream.readByte() != 'S') {
continue;
}
if (stream.readByte() != 'C') {
continue;
}
if (stream.readByte() != 'P') {
continue;
}
// header size
final int headerSize = (stream.readByte() & 0xFF) << 24 | (stream.readByte() & 0xFF) << 16
| (stream.readByte() & 0xFF) << 8 | (stream.readByte() & 0xFF);
if (headerSize != 16) {
throw new EiscpException("Unsupported header size: " + headerSize);
}
// data size
final int dataSize = (stream.readByte() & 0xFF) << 24 | (stream.readByte() & 0xFF) << 16
| (stream.readByte() & 0xFF) << 8 | (stream.readByte() & 0xFF);
LOGGER.trace("Data size: {}", dataSize);
// version
final byte versionChar = stream.readByte();
if (versionChar != 1) {
throw new EiscpException("Unsupported version " + String.valueOf(versionChar));
}
// skip 3 reserved bytes
byte b1 = stream.readByte();
byte b2 = stream.readByte();
byte b3 = stream.readByte();
byte[] data = new byte[dataSize];
int bytesReceived = 0;
try {
while (bytesReceived < dataSize) {
bytesReceived = bytesReceived + stream.read(data, bytesReceived, data.length - bytesReceived);
if (LOGGER.isTraceEnabled()) {
// create header for debugging purposes
final StringBuilder sb = new StringBuilder();
sb.append("ISCP");
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x00);
sb.append((char) 0x10);
// 4 char Big Endian data size
sb.append((char) ((dataSize >> 24) & 0xFF));
sb.append((char) ((dataSize >> 16) & 0xFF));
sb.append((char) ((dataSize >> 8) & 0xFF));
sb.append((char) (dataSize & 0xFF));
// eiscp version;
sb.append((char) versionChar);
// reserved bytes
sb.append((char) b1).append((char) b2).append((char) b3);
// data
sb.append(new String(data, "UTF-8"));
LOGGER.trace("Received eISCP message, {} -> {}", HexUtils.bytesToHex(sb.toString().getBytes()),
toPrintable(sb.toString()));
}
}
} catch (IOException t) {
if (bytesReceived != dataSize) {
LOGGER.debug("Received bad data: '{}'", toPrintable(new String(data, "UTF-8")));
throw new EiscpException(
"Data missing, expected + " + dataSize + " received " + bytesReceived + " bytes");
} else {
throw t;
}
}
// start char
final byte startChar = data[0];
if (startChar != '!') {
throw new EiscpException("Illegal start char " + startChar);
}
// unit type
@SuppressWarnings("unused")
final byte unitType = data[1];
// data should be end to "[EOF]" or "[EOF][CR]" or "[EOF][CR][LF]" characters depend on model
// [EOF] End of File ASCII Code 0x1A
// [CR] Carriage Return ASCII Code 0x0D (\r)
// [LF] Line Feed ASCII Code 0x0A (\n)
int endBytes = 0;
// TODO: Simplify this by implementation, which find [EOF] character and ignore rest of the bytes after
// that. But before that, proper junit test should be implement to be sure that it does not broke
// anything.
if (data[dataSize - 5] == (byte) 0x1A && data[dataSize - 4] == '\n' && data[dataSize - 3] == '\n'
&& data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') {
// skip "[EOF][LF][LF][CR][LF]"
endBytes = 5;
} else if (data[dataSize - 4] == (byte) 0x1A && data[dataSize - 3] == '\r' && data[dataSize - 2] == '\n'
&& data[dataSize - 1] == 0x00) {
// skip "[EOF][CR][LF][NULL]"
endBytes = 4;
} else if (data[dataSize - 3] == (byte) 0x1A && data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') {
// skip "[EOF][CR][LF]"
endBytes = 3;
} else if (data[dataSize - 2] == (byte) 0x1A && data[dataSize - 1] == '\r') {
// "[EOF][CR]"
endBytes = 2;
} else if (data[dataSize - 1] == (byte) 0x1A) {
// "[EOF]"
endBytes = 1;
} else {
throw new EiscpException("Illegal end of message");
}
try {
String command = new String(Arrays.copyOfRange(data, 2, 5));
String value = new String(Arrays.copyOfRange(data, 5, data.length - endBytes));
return new EiscpMessage.MessageBuilder().command(command).value(value).build();
} catch (Exception e) {
throw new EiscpException("Fatal error occurred when parsing eISCP message, cause=" + e.getCause());
}
}
}
public static String toPrintable(final String rawData) {
final StringBuilder sb = new StringBuilder();
if (rawData == null) {
return "";
}
for (final char c : rawData.toCharArray()) {
if (c <= 31 || c == 127) {
switch (c) {
case '\r':
sb.append("[CR]");
break;
case '\n':
sb.append("[LF]");
break;
case (byte) 0x1A:
sb.append("[EOF]");
break;
default:
sb.append(String.format("[%02X]", (int) c));
}
} else {
sb.append(c);
}
}
return sb.toString();
}
}

View File

@@ -0,0 +1,915 @@
/**
* 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.onkyo.internal.handler;
import static org.openhab.binding.onkyo.internal.OnkyoBindingConstants.*;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.openhab.binding.onkyo.internal.OnkyoAlbumArt;
import org.openhab.binding.onkyo.internal.OnkyoConnection;
import org.openhab.binding.onkyo.internal.OnkyoEventListener;
import org.openhab.binding.onkyo.internal.OnkyoStateDescriptionProvider;
import org.openhab.binding.onkyo.internal.ServiceType;
import org.openhab.binding.onkyo.internal.automation.modules.OnkyoThingActionsService;
import org.openhab.binding.onkyo.internal.config.OnkyoDeviceConfiguration;
import org.openhab.binding.onkyo.internal.eiscp.EiscpCommand;
import org.openhab.binding.onkyo.internal.eiscp.EiscpMessage;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.io.transport.upnp.UpnpIOService;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
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.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* The {@link OnkyoHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Paul Frank - Initial contribution
* @author Marcel Verpaalen - parsing additional commands
* @author Pauli Anttila - lot of refactoring
* @author Stewart Cossey - add dynamic state description provider
*/
public class OnkyoHandler extends UpnpAudioSinkHandler implements OnkyoEventListener {
private final Logger logger = LoggerFactory.getLogger(OnkyoHandler.class);
private OnkyoDeviceConfiguration configuration;
private OnkyoConnection connection;
private ScheduledFuture<?> resourceUpdaterFuture;
@SuppressWarnings("unused")
private int currentInput = -1;
private State volumeLevelZone1 = UnDefType.UNDEF;
private State volumeLevelZone2 = UnDefType.UNDEF;
private State volumeLevelZone3 = UnDefType.UNDEF;
private State lastPowerState = OnOffType.OFF;
private final OnkyoStateDescriptionProvider stateDescriptionProvider;
private final OnkyoAlbumArt onkyoAlbumArt = new OnkyoAlbumArt();
private static final int NET_USB_ID = 43;
public OnkyoHandler(Thing thing, UpnpIOService upnpIOService, AudioHTTPServer audioHTTPServer, String callbackUrl,
OnkyoStateDescriptionProvider stateDescriptionProvider) {
super(thing, upnpIOService, audioHTTPServer, callbackUrl);
this.stateDescriptionProvider = stateDescriptionProvider;
}
/**
* Initialize the state of the receiver.
*/
@Override
public void initialize() {
logger.debug("Initializing handler for Onkyo Receiver");
configuration = getConfigAs(OnkyoDeviceConfiguration.class);
logger.info("Using configuration: {}", configuration.toString());
connection = new OnkyoConnection(configuration.ipAddress, configuration.port);
connection.addEventListener(this);
scheduler.execute(() -> {
logger.debug("Open connection to Onkyo Receiver @{}", connection.getConnectionName());
connection.openConnection();
if (connection.isConnected()) {
updateStatus(ThingStatus.ONLINE);
sendCommand(EiscpCommand.INFO_QUERY);
}
});
if (configuration.refreshInterval > 0) {
// Start resource refresh updater
resourceUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
try {
logger.debug("Send resource update requests to Onkyo Receiver @{}", connection.getConnectionName());
checkStatus();
} catch (LinkageError e) {
logger.warn("Failed to send resource update requests to Onkyo Receiver @{}. Cause: {}",
connection.getConnectionName(), e.getMessage());
} catch (Exception ex) {
logger.warn("Exception in resource refresh Thread Onkyo Receiver @{}. Cause: {}",
connection.getConnectionName(), ex.getMessage());
}
}, configuration.refreshInterval, configuration.refreshInterval, TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
super.dispose();
if (resourceUpdaterFuture != null) {
resourceUpdaterFuture.cancel(true);
}
if (connection != null) {
connection.removeEventListener(this);
connection.closeConnection();
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("handleCommand for channel {}: {}", channelUID.getId(), command.toString());
switch (channelUID.getId()) {
/*
* ZONE 1
*/
case CHANNEL_POWER:
if (command instanceof OnOffType) {
sendCommand(EiscpCommand.POWER_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.POWER_QUERY);
}
break;
case CHANNEL_MUTE:
if (command instanceof OnOffType) {
sendCommand(EiscpCommand.MUTE_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.MUTE_QUERY);
}
break;
case CHANNEL_VOLUME:
handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, command);
break;
case CHANNEL_INPUT:
if (command instanceof DecimalType) {
selectInput(((DecimalType) command).intValue());
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.SOURCE_QUERY);
}
break;
case CHANNEL_LISTENMODE:
if (command instanceof DecimalType) {
sendCommand(EiscpCommand.LISTEN_MODE_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
}
break;
/*
* ZONE 2
*/
case CHANNEL_POWERZONE2:
if (command instanceof OnOffType) {
sendCommand(EiscpCommand.ZONE2_POWER_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
}
break;
case CHANNEL_MUTEZONE2:
if (command instanceof OnOffType) {
sendCommand(EiscpCommand.ZONE2_MUTE_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
}
break;
case CHANNEL_VOLUMEZONE2:
handleVolumeSet(EiscpCommand.Zone.ZONE2, volumeLevelZone2, command);
break;
case CHANNEL_INPUTZONE2:
if (command instanceof DecimalType) {
sendCommand(EiscpCommand.ZONE2_SOURCE_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
}
break;
/*
* ZONE 3
*/
case CHANNEL_POWERZONE3:
if (command instanceof OnOffType) {
sendCommand(EiscpCommand.ZONE3_POWER_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
}
break;
case CHANNEL_MUTEZONE3:
if (command instanceof OnOffType) {
sendCommand(EiscpCommand.ZONE3_MUTE_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
}
break;
case CHANNEL_VOLUMEZONE3:
handleVolumeSet(EiscpCommand.Zone.ZONE3, volumeLevelZone3, command);
break;
case CHANNEL_INPUTZONE3:
if (command instanceof DecimalType) {
sendCommand(EiscpCommand.ZONE3_SOURCE_SET, command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
}
break;
/*
* NET PLAYER
*/
case CHANNEL_CONTROL:
if (command instanceof PlayPauseType) {
if (command.equals(PlayPauseType.PLAY)) {
sendCommand(EiscpCommand.NETUSB_OP_PLAY);
} else if (command.equals(PlayPauseType.PAUSE)) {
sendCommand(EiscpCommand.NETUSB_OP_PAUSE);
}
} else if (command instanceof NextPreviousType) {
if (command.equals(NextPreviousType.NEXT)) {
sendCommand(EiscpCommand.NETUSB_OP_TRACKUP);
} else if (command.equals(NextPreviousType.PREVIOUS)) {
sendCommand(EiscpCommand.NETUSB_OP_TRACKDWN);
}
} else if (command instanceof RewindFastforwardType) {
if (command.equals(RewindFastforwardType.REWIND)) {
sendCommand(EiscpCommand.NETUSB_OP_REW);
} else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
sendCommand(EiscpCommand.NETUSB_OP_FF);
}
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_PLAY_STATUS_QUERY);
}
break;
case CHANNEL_PLAY_URI:
handlePlayUri(command);
break;
case CHANNEL_ALBUM_ART:
case CHANNEL_ALBUM_ART_URL:
if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_ALBUM_ART_QUERY);
}
break;
case CHANNEL_ARTIST:
if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_SONG_ARTIST_QUERY);
}
break;
case CHANNEL_ALBUM:
if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_SONG_ALBUM_QUERY);
}
break;
case CHANNEL_TITLE:
if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_SONG_TITLE_QUERY);
}
break;
case CHANNEL_CURRENTPLAYINGTIME:
if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_SONG_ELAPSEDTIME_QUERY);
}
break;
/*
* NET MENU
*/
case CHANNEL_NET_MENU_CONTROL:
if (command instanceof StringType) {
final String cmdName = command.toString();
handleNetMenuCommand(cmdName);
}
break;
case CHANNEL_NET_MENU_TITLE:
if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
}
break;
/*
* MISC
*/
default:
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
break;
}
}
private void populateInputs(NodeList selectorlist) {
List<StateOption> options = new ArrayList<>();
for (int i = 0; i < selectorlist.getLength(); i++) {
Element selectorItem = (Element) selectorlist.item(i);
options.add(new StateOption(String.valueOf(Integer.parseInt(selectorItem.getAttribute("id"), 16)),
selectorItem.getAttribute("name")));
}
logger.debug("Got Input List from Receiver {}", options);
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUT), options);
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE2), options);
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_INPUTZONE3), options);
}
private void doPowerOnCheck(State state) {
if (configuration.refreshInterval == 0 && lastPowerState == OnOffType.OFF && state == OnOffType.ON) {
sendCommand(EiscpCommand.INFO_QUERY);
}
lastPowerState = state;
}
@Override
public void statusUpdateReceived(String ip, EiscpMessage data) {
logger.debug("Received status update from Onkyo Receiver @{}: data={}", connection.getConnectionName(), data);
updateStatus(ThingStatus.ONLINE);
try {
EiscpCommand receivedCommand = null;
try {
receivedCommand = EiscpCommand.getCommandByCommandAndValueStr(data.getCommand(), "");
} catch (IllegalArgumentException ex) {
logger.debug("Received unknown status update from Onkyo Receiver @{}: data={}",
connection.getConnectionName(), data);
return;
}
logger.debug("Received command {}", receivedCommand);
switch (receivedCommand) {
/*
* ZONE 1
*/
case POWER:
State powerState = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
updateState(CHANNEL_POWER, powerState);
doPowerOnCheck(powerState);
break;
case MUTE:
updateState(CHANNEL_MUTE, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
break;
case VOLUME:
volumeLevelZone1 = handleReceivedVolume(
convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
updateState(CHANNEL_VOLUME, volumeLevelZone1);
break;
case SOURCE:
updateState(CHANNEL_INPUT, convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
break;
case LISTEN_MODE:
updateState(CHANNEL_LISTENMODE,
convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
break;
/*
* ZONE 2
*/
case ZONE2_POWER:
State powerZone2State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
updateState(CHANNEL_POWERZONE2, powerZone2State);
doPowerOnCheck(powerZone2State);
break;
case ZONE2_MUTE:
updateState(CHANNEL_MUTEZONE2, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
break;
case ZONE2_VOLUME:
volumeLevelZone2 = handleReceivedVolume(
convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
updateState(CHANNEL_VOLUMEZONE2, volumeLevelZone2);
break;
case ZONE2_SOURCE:
updateState(CHANNEL_INPUTZONE2,
convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
break;
/*
* ZONE 3
*/
case ZONE3_POWER:
State powerZone3State = convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class);
updateState(CHANNEL_POWERZONE3, powerZone3State);
doPowerOnCheck(powerZone3State);
break;
case ZONE3_MUTE:
updateState(CHANNEL_MUTEZONE3, convertDeviceValueToOpenHabState(data.getValue(), OnOffType.class));
break;
case ZONE3_VOLUME:
volumeLevelZone3 = handleReceivedVolume(
convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
updateState(CHANNEL_VOLUMEZONE3, volumeLevelZone3);
break;
case ZONE3_SOURCE:
updateState(CHANNEL_INPUTZONE3,
convertDeviceValueToOpenHabState(data.getValue(), DecimalType.class));
break;
/*
* NET PLAYER
*/
case NETUSB_SONG_ARTIST:
updateState(CHANNEL_ARTIST, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
break;
case NETUSB_SONG_ALBUM:
updateState(CHANNEL_ALBUM, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
break;
case NETUSB_SONG_TITLE:
updateState(CHANNEL_TITLE, convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
break;
case NETUSB_SONG_ELAPSEDTIME:
updateState(CHANNEL_CURRENTPLAYINGTIME,
convertDeviceValueToOpenHabState(data.getValue(), StringType.class));
break;
case NETUSB_PLAY_STATUS:
updateState(CHANNEL_CONTROL, convertNetUsbPlayStatus(data.getValue()));
break;
case NETUSB_ALBUM_ART:
updateAlbumArt(data.getValue());
break;
case NETUSB_TITLE:
updateNetTitle(data.getValue());
break;
case NETUSB_MENU:
updateNetMenu(data.getValue());
break;
/*
* MISC
*/
case INFO:
processInfo(data.getValue());
logger.debug("Info message: '{}'", data.getValue());
break;
default:
logger.debug("Received unhandled status update from Onkyo Receiver @{}: data={}",
connection.getConnectionName(), data);
}
} catch (Exception ex) {
logger.warn("Exception in statusUpdateReceived for Onkyo Receiver @{}. Cause: {}, data received: {}",
connection.getConnectionName(), ex.getMessage(), data);
}
}
private void processInfo(String infoXML) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
try (StringReader sr = new StringReader(infoXML)) {
InputSource is = new InputSource(sr);
Document doc = builder.parse(is);
NodeList selectableInputs = doc.getDocumentElement().getElementsByTagName("selector");
populateInputs(selectableInputs);
}
} catch (ParserConfigurationException | SAXException | IOException e) {
logger.debug("Error occured during Info XML parsing.", e);
}
}
@Override
public void connectionError(String ip, String errorMsg) {
logger.debug("Connection error occurred to Onkyo Receiver @{}", ip);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
}
private State convertDeviceValueToOpenHabState(String data, Class<?> classToConvert) {
State state = UnDefType.UNDEF;
try {
int index;
if (data.contentEquals("N/A")) {
state = UnDefType.UNDEF;
} else if (classToConvert == OnOffType.class) {
index = Integer.parseInt(data, 16);
state = index == 0 ? OnOffType.OFF : OnOffType.ON;
} else if (classToConvert == DecimalType.class) {
index = Integer.parseInt(data, 16);
state = new DecimalType(index);
} else if (classToConvert == PercentType.class) {
index = Integer.parseInt(data, 16);
state = new PercentType(index);
} else if (classToConvert == StringType.class) {
state = new StringType(data);
}
} catch (Exception e) {
logger.debug("Cannot convert value '{}' to data type {}", data, classToConvert);
}
logger.debug("Converted data '{}' to openHAB state '{}' ({})", data, state, classToConvert);
return state;
}
private void handleNetMenuCommand(String cmdName) {
if ("Up".equals(cmdName)) {
sendCommand(EiscpCommand.NETUSB_OP_UP);
} else if ("Down".equals(cmdName)) {
sendCommand(EiscpCommand.NETUSB_OP_DOWN);
} else if ("Select".equals(cmdName)) {
sendCommand(EiscpCommand.NETUSB_OP_SELECT);
} else if ("PageUp".equals(cmdName)) {
sendCommand(EiscpCommand.NETUSB_OP_LEFT);
} else if ("PageDown".equals(cmdName)) {
sendCommand(EiscpCommand.NETUSB_OP_RIGHT);
} else if ("Back".equals(cmdName)) {
sendCommand(EiscpCommand.NETUSB_OP_RETURN);
} else if (cmdName.matches("Select[0-9]")) {
int pos = Integer.parseInt(cmdName.substring(6));
sendCommand(EiscpCommand.NETUSB_MENU_SELECT, new DecimalType(pos));
} else {
logger.debug("Received unknown menucommand {}", cmdName);
}
}
private void selectInput(int inputId) {
sendCommand(EiscpCommand.SOURCE_SET, new DecimalType(inputId));
currentInput = inputId;
}
@SuppressWarnings("unused")
private void onInputChanged(int newInput) {
currentInput = newInput;
if (newInput != NET_USB_ID) {
resetNetMenu();
updateState(CHANNEL_ARTIST, UnDefType.UNDEF);
updateState(CHANNEL_ALBUM, UnDefType.UNDEF);
updateState(CHANNEL_TITLE, UnDefType.UNDEF);
updateState(CHANNEL_CURRENTPLAYINGTIME, UnDefType.UNDEF);
}
}
private void updateAlbumArt(String data) {
onkyoAlbumArt.addFrame(data);
if (onkyoAlbumArt.isAlbumCoverReady()) {
try {
byte[] imgData = onkyoAlbumArt.getAlbumArt();
if (imgData != null && imgData.length > 0) {
String mimeType = onkyoAlbumArt.getAlbumArtMimeType();
if (mimeType.isEmpty()) {
mimeType = guessMimeTypeFromData(imgData);
}
updateState(CHANNEL_ALBUM_ART, new RawType(imgData, mimeType));
} else {
updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
}
} catch (IllegalArgumentException e) {
updateState(CHANNEL_ALBUM_ART, UnDefType.UNDEF);
}
onkyoAlbumArt.clearAlbumArt();
}
if (data.startsWith("2-")) {
updateState(CHANNEL_ALBUM_ART_URL, new StringType(data.substring(2, data.length())));
} else if (data.startsWith("n-")) {
updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
} else {
logger.debug("Not supported album art URL type: {}", data.substring(0, 2));
updateState(CHANNEL_ALBUM_ART_URL, UnDefType.UNDEF);
}
}
private void updateNetTitle(String data) {
// first 2 characters is service type
int type = Integer.parseInt(data.substring(0, 2), 16);
ServiceType service = ServiceType.getType(type);
String title = "";
if (data.length() > 21) {
title = data.substring(22, data.length());
}
updateState(CHANNEL_NET_MENU_TITLE,
new StringType(service.toString() + ((title.length() > 0) ? ": " + title : "")));
}
private void updateNetMenu(String data) {
switch (data.charAt(0)) {
case 'U':
String itemData = data.substring(3, data.length());
switch (data.charAt(1)) {
case '0':
updateState(CHANNEL_NET_MENU0, new StringType(itemData));
break;
case '1':
updateState(CHANNEL_NET_MENU1, new StringType(itemData));
break;
case '2':
updateState(CHANNEL_NET_MENU2, new StringType(itemData));
break;
case '3':
updateState(CHANNEL_NET_MENU3, new StringType(itemData));
break;
case '4':
updateState(CHANNEL_NET_MENU4, new StringType(itemData));
break;
case '5':
updateState(CHANNEL_NET_MENU5, new StringType(itemData));
break;
case '6':
updateState(CHANNEL_NET_MENU6, new StringType(itemData));
break;
case '7':
updateState(CHANNEL_NET_MENU7, new StringType(itemData));
break;
case '8':
updateState(CHANNEL_NET_MENU8, new StringType(itemData));
break;
case '9':
updateState(CHANNEL_NET_MENU9, new StringType(itemData));
break;
}
break;
case 'C':
updateMenuPosition(data);
break;
}
}
private void updateMenuPosition(String data) {
char position = data.charAt(1);
int pos = Character.getNumericValue(position);
logger.debug("Updating menu position to {}", pos);
if (pos == -1) {
updateState(CHANNEL_NET_MENU_SELECTION, UnDefType.UNDEF);
} else {
updateState(CHANNEL_NET_MENU_SELECTION, new DecimalType(pos));
}
if (data.endsWith("P")) {
resetNetMenu();
}
}
private void resetNetMenu() {
logger.debug("Reset net menu");
updateState(CHANNEL_NET_MENU0, new StringType("-"));
updateState(CHANNEL_NET_MENU1, new StringType("-"));
updateState(CHANNEL_NET_MENU2, new StringType("-"));
updateState(CHANNEL_NET_MENU3, new StringType("-"));
updateState(CHANNEL_NET_MENU4, new StringType("-"));
updateState(CHANNEL_NET_MENU5, new StringType("-"));
updateState(CHANNEL_NET_MENU6, new StringType("-"));
updateState(CHANNEL_NET_MENU7, new StringType("-"));
updateState(CHANNEL_NET_MENU8, new StringType("-"));
updateState(CHANNEL_NET_MENU9, new StringType("-"));
}
private State convertNetUsbPlayStatus(String data) {
State state = UnDefType.UNDEF;
switch (data.charAt(0)) {
case 'P':
state = PlayPauseType.PLAY;
break;
case 'p':
case 'S':
state = PlayPauseType.PAUSE;
break;
case 'F':
state = RewindFastforwardType.FASTFORWARD;
break;
case 'R':
state = RewindFastforwardType.REWIND;
break;
}
return state;
}
public void sendRawCommand(String command, String value) {
if (connection != null) {
connection.send(command, value);
} else {
logger.debug("Cannot send command to onkyo receiver since the onkyo binding is not initialized");
}
}
private void sendCommand(EiscpCommand deviceCommand) {
if (connection != null) {
connection.send(deviceCommand.getCommand(), deviceCommand.getValue());
} else {
logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
}
}
private void sendCommand(EiscpCommand deviceCommand, Command command) {
if (connection != null) {
final String cmd = deviceCommand.getCommand();
String valTemplate = deviceCommand.getValue();
String val;
if (command instanceof OnOffType) {
val = String.format(valTemplate, command == OnOffType.ON ? 1 : 0);
} else if (command instanceof StringType) {
val = String.format(valTemplate, command);
} else if (command instanceof DecimalType) {
val = String.format(valTemplate, ((DecimalType) command).intValue());
} else if (command instanceof PercentType) {
val = String.format(valTemplate, ((DecimalType) command).intValue());
} else {
val = valTemplate;
}
logger.debug("Sending command '{}' with value '{}' to Onkyo Receiver @{}", cmd, val,
connection.getConnectionName());
connection.send(cmd, val);
} else {
logger.debug("Connect send command to onkyo receiver since the onkyo binding is not initialized");
}
}
/**
* Check the status of the AVR.
*
* @return
*/
private void checkStatus() {
sendCommand(EiscpCommand.POWER_QUERY);
if (connection != null && connection.isConnected()) {
sendCommand(EiscpCommand.VOLUME_QUERY);
sendCommand(EiscpCommand.SOURCE_QUERY);
sendCommand(EiscpCommand.MUTE_QUERY);
sendCommand(EiscpCommand.NETUSB_TITLE_QUERY);
sendCommand(EiscpCommand.LISTEN_MODE_QUERY);
sendCommand(EiscpCommand.INFO_QUERY);
if (isChannelAvailable(CHANNEL_POWERZONE2)) {
sendCommand(EiscpCommand.ZONE2_POWER_QUERY);
sendCommand(EiscpCommand.ZONE2_VOLUME_QUERY);
sendCommand(EiscpCommand.ZONE2_SOURCE_QUERY);
sendCommand(EiscpCommand.ZONE2_MUTE_QUERY);
}
if (isChannelAvailable(CHANNEL_POWERZONE3)) {
sendCommand(EiscpCommand.ZONE3_POWER_QUERY);
sendCommand(EiscpCommand.ZONE3_VOLUME_QUERY);
sendCommand(EiscpCommand.ZONE3_SOURCE_QUERY);
sendCommand(EiscpCommand.ZONE3_MUTE_QUERY);
}
} else {
updateStatus(ThingStatus.OFFLINE);
}
}
private boolean isChannelAvailable(String channel) {
List<Channel> channels = getThing().getChannels();
for (Channel c : channels) {
if (c.getUID().getId().equals(channel)) {
return true;
}
}
return false;
}
private void handleVolumeSet(EiscpCommand.Zone zone, final State currentValue, final Command command) {
if (command instanceof PercentType) {
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_SET),
downScaleVolume((PercentType) command));
} else if (command.equals(IncreaseDecreaseType.INCREASE)) {
if (currentValue instanceof PercentType) {
if (((DecimalType) currentValue).intValue() < configuration.volumeLimit) {
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_UP));
} else {
logger.info("Volume level is limited to {}, ignore volume up command.", configuration.volumeLimit);
}
}
} else if (command.equals(IncreaseDecreaseType.DECREASE)) {
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_DOWN));
} else if (command.equals(OnOffType.OFF)) {
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
} else if (command.equals(OnOffType.ON)) {
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_SET), command);
} else if (command.equals(RefreshType.REFRESH)) {
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.VOLUME_QUERY));
sendCommand(EiscpCommand.getCommandForZone(zone, EiscpCommand.MUTE_QUERY));
}
}
private State handleReceivedVolume(State volume) {
if (volume instanceof DecimalType) {
return upScaleVolume(((DecimalType) volume));
}
return volume;
}
private PercentType upScaleVolume(DecimalType volume) {
PercentType newVolume = scaleVolumeFromReceiver(volume);
if (configuration.volumeLimit < 100) {
double scaleCoefficient = 100d / configuration.volumeLimit;
PercentType unLimitedVolume = newVolume;
newVolume = new PercentType(((Double) (newVolume.doubleValue() * scaleCoefficient)).intValue());
logger.debug("Up scaled volume level '{}' to '{}'", unLimitedVolume, newVolume);
}
return newVolume;
}
private DecimalType downScaleVolume(PercentType volume) {
PercentType limitedVolume = volume;
if (configuration.volumeLimit < 100) {
double scaleCoefficient = configuration.volumeLimit / 100d;
limitedVolume = new PercentType(((Double) (volume.doubleValue() * scaleCoefficient)).intValue());
logger.debug("Limited volume level '{}' to '{}'", volume, limitedVolume);
}
return scaleVolumeForReceiver(limitedVolume);
}
private DecimalType scaleVolumeForReceiver(PercentType volume) {
return new DecimalType(((Double) (volume.doubleValue() * configuration.volumeScale)).intValue());
}
private PercentType scaleVolumeFromReceiver(DecimalType volume) {
return new PercentType(((Double) (volume.intValue() / configuration.volumeScale)).intValue());
}
@Override
public PercentType getVolume() throws IOException {
if (volumeLevelZone1 instanceof PercentType) {
return (PercentType) volumeLevelZone1;
}
throw new IOException();
}
@Override
public void setVolume(PercentType volume) throws IOException {
handleVolumeSet(EiscpCommand.Zone.ZONE1, volumeLevelZone1, downScaleVolume(volume));
}
private String guessMimeTypeFromData(byte[] data) {
String mimeType = HttpUtil.guessContentTypeFromData(data);
logger.debug("Mime type guess from content: {}", mimeType);
if (mimeType == null) {
mimeType = RawType.DEFAULT_MIME_TYPE;
}
logger.debug("Mime type: {}", mimeType);
return mimeType;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(OnkyoThingActionsService.class);
}
}

View File

@@ -0,0 +1,221 @@
/**
* 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.onkyo.internal.handler;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.onkyo.internal.OnkyoBindingConstants;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FixedLengthAudioStream;
import org.openhab.core.audio.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
import org.openhab.core.io.transport.upnp.UpnpIOService;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* * The {@link UpnpAudioSinkHandler} is a base class for ThingHandlers for devices which support UPnP playback. It
* implements the AudioSink interface.
* This will allow to register the derived ThingHandler to be registered as a AudioSink in the framework.
*
* @author Paul Frank - Initial contribution
*/
public abstract class UpnpAudioSinkHandler extends BaseThingHandler implements AudioSink, UpnpIOParticipant {
private static final Set<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
static {
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_FORMATS.add(AudioFormat.MP3);
SUPPORTED_STREAMS.add(AudioStream.class);
}
private final Logger logger = LoggerFactory.getLogger(getClass());
private AudioHTTPServer audioHTTPServer;
private String callbackUrl;
private UpnpIOService service;
public UpnpAudioSinkHandler(Thing thing, UpnpIOService upnpIOService, AudioHTTPServer audioHTTPServer,
String callbackUrl) {
super(thing);
this.audioHTTPServer = audioHTTPServer;
this.callbackUrl = callbackUrl;
if (upnpIOService != null) {
this.service = upnpIOService;
}
}
protected void handlePlayUri(Command command) {
if (command != null && command instanceof StringType) {
try {
playMedia(command.toString());
} catch (IllegalStateException e) {
logger.warn("Cannot play URI ({})", e.getMessage());
}
}
}
private void playMedia(String url) {
stop();
removeAllTracksFromQueue();
if (!url.startsWith("x-") && (!url.startsWith("http"))) {
url = "x-file-cifs:" + url;
}
setCurrentURI(url, "");
play();
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return SUPPORTED_FORMATS;
}
@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_STREAMS;
}
private void stop() {
Map<String, String> inputs = new HashMap<>();
inputs.put("InstanceID", "0");
Map<String, String> result = service.invokeAction(this, "AVTransport", "Stop", inputs);
for (String variable : result.keySet()) {
this.onValueReceived(variable, result.get(variable), "AVTransport");
}
}
private void play() {
Map<String, String> inputs = new HashMap<>();
inputs.put("InstanceID", "0");
inputs.put("Speed", "1");
Map<String, String> result = service.invokeAction(this, "AVTransport", "Play", inputs);
for (String variable : result.keySet()) {
this.onValueReceived(variable, result.get(variable), "AVTransport");
}
}
private void removeAllTracksFromQueue() {
Map<String, String> inputs = new HashMap<>();
inputs.put("InstanceID", "0");
Map<String, String> result = service.invokeAction(this, "AVTransport", "RemoveAllTracksFromQueue", inputs);
for (String variable : result.keySet()) {
this.onValueReceived(variable, result.get(variable), "AVTransport");
}
}
private void setCurrentURI(String uri, String uriMetaData) {
if (uri != null && uriMetaData != null) {
Map<String, String> inputs = new HashMap<>();
try {
inputs.put("InstanceID", "0");
inputs.put("CurrentURI", uri);
inputs.put("CurrentURIMetaData", uriMetaData);
} catch (NumberFormatException ex) {
logger.error("Action Invalid Value Format Exception {}", ex.getMessage());
}
Map<String, String> result = service.invokeAction(this, "AVTransport", "SetAVTransportURI", inputs);
for (String variable : result.keySet()) {
this.onValueReceived(variable, result.get(variable), "AVTransport");
}
}
}
@Override
public String getId() {
return getThing().getUID().toString();
}
@Override
public String getLabel(Locale locale) {
return getThing().getLabel();
}
@Override
public void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
if (audioStream == null) {
stop();
return;
}
String url = null;
if (audioStream instanceof URLAudioStream) {
// it is an external URL, the speaker can access it itself and play it.
URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
url = urlAudioStream.getURL();
} else {
if (callbackUrl != null) {
String relativeUrl;
if (audioStream instanceof FixedLengthAudioStream) {
// we serve it on our own HTTP server
relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 20);
} else {
relativeUrl = audioHTTPServer.serve(audioStream);
}
url = callbackUrl + relativeUrl;
} else {
logger.warn("We do not have any callback url, so onkyo cannot play the audio stream!");
return;
}
}
playMedia(url);
}
@Override
public String getUDN() {
return (String) this.getConfig().get(OnkyoBindingConstants.UDN_PARAMETER);
}
@Override
public void onValueReceived(String variable, String value, String service) {
logger.debug("received variable {} with value {} from service {}", variable, value, service);
}
@Override
public void onServiceSubscribed(String service, boolean succeeded) {
}
@Override
public void onStatusChanged(boolean status) {
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="onkyo" 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>Onkyo Binding</name>
<description>This is the binding for Onkyo receivers.</description>
<author>Paul Frank, Pauli Anttila</author>
<config-description>
<parameter name="callbackUrl" type="text">
<label>Callback URL</label>
<description>url to use for playing notification sounds, e.g. http://192.168.0.2:8080</description>
<required>false</required>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:onkyo:config">
<parameter name="ipAddress" type="text" required="true">
<label>Network Address</label>
<description>The IP or host name of the Onkyo Receiver</description>
<context>network-address</context>
</parameter>
<parameter name="port" type="integer" min="1" max="65535">
<label>Port</label>
<description>Port of the Onkyo to control</description>
<default>60128</default>
<advanced>true</advanced>
</parameter>
<parameter name="udn" type="text">
<label>Unique Device Name</label>
<description>The UDN identifies the Onkyo AVR.</description>
<advanced>true</advanced>
</parameter>
<parameter name="refreshInterval" type="integer" min="0">
<label>Refresh Interval</label>
<description>The refresh interval in seconds for polling the receiver (0=disable). Binding receive automatically
updates from
</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="volumeLimit" type="integer" min="0" max="100">
<label>Volume Limit</label>
<description>Limit maximum volume level to defined percentage.</description>
<default>100</default>
<advanced>true</advanced>
</parameter>
<parameter name="volumeScale" type="decimal">
<label>Volume Scale</label>
<description>Applies a scale to the volume.</description>
<default>1</default>
<advanced>true</advanced>
<options>
<option value="1">0-100</option>
<option value="2">0-100 in 0.5 steps</option>
<option value="0.8">0-80</option>
<option value="0.5">0-50</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<!-- Channel Groups -->
<channel-group-type id="zone1Controls">
<label>Zone 1 (Main Zone)</label>
<channels>
<channel id="power" typeId="power"/>
<channel id="input" typeId="input"/>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
</channels>
</channel-group-type>
<channel-group-type id="zone2Controls">
<label>Zone 2</label>
<channels>
<channel id="power" typeId="power"/>
<channel id="input" typeId="input"/>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
</channels>
</channel-group-type>
<channel-group-type id="zone3Controls">
<label>Zone 3</label>
<channels>
<channel id="power" typeId="power"/>
<channel id="input" typeId="input"/>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
</channels>
</channel-group-type>
<channel-group-type id="playerControls">
<label>Player</label>
<channels>
<channel id="control" typeId="control"/>
<channel id="currentPlayingTime" typeId="currentPlayingTime"/>
<channel id="title" typeId="title"/>
<channel id="album" typeId="album"/>
<channel id="artist" typeId="artist"/>
<channel id="listenmode" typeId="listenmode"/>
<channel id="playuri" typeId="playuri"/>
<channel id="albumArt" typeId="albumArt"/>
<channel id="albumArtUrl" typeId="albumArtUrl"/>
</channels>
</channel-group-type>
<channel-group-type id="netMenuControls">
<label>Net/USB Menu</label>
<channels>
<channel id="title" typeId="title"/>
<channel id="control" typeId="netControl"/>
<channel id="item0" typeId="menuItem">
<label>Menu Item 0</label>
<description>Net/USB menu item at position 0</description>
</channel>
<channel id="item1" typeId="menuItem">
<label>Menu Item 1</label>
<description>Net/USB menu item at position 1</description>
</channel>
<channel id="item2" typeId="menuItem">
<label>Menu Item 2</label>
<description>Net/USB menu item at position 2</description>
</channel>
<channel id="item3" typeId="menuItem">
<label>Menu Item 3</label>
<description>Net/USB menu item at position 3</description>
</channel>
<channel id="item4" typeId="menuItem">
<label>Menu Item 4</label>
<description>Net/USB menu item at position 4</description>
</channel>
<channel id="item5" typeId="menuItem">
<label>Menu Item 5</label>
<description>Net/USB menu item at position 5</description>
</channel>
<channel id="item6" typeId="menuItem">
<label>Menu Item 6</label>
<description>Net/USB menu item at position 6</description>
</channel>
<channel id="item7" typeId="menuItem">
<label>Menu Item 7</label>
<description>Net/USB menu item at position 7</description>
</channel>
<channel id="item8" typeId="menuItem">
<label>Menu Item 8</label>
<description>Net/USB menu item at position 8</description>
</channel>
<channel id="item9" typeId="menuItem">
<label>Menu Item 9</label>
<description>Net/USB menu item at position 9</description>
</channel>
<channel id="selection" typeId="menuSelection"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<!-- Commands -->
<channel-type id="power">
<item-type>Switch</item-type>
<label>Power</label>
<description>Power on/off your device</description>
</channel-type>
<channel-type id="input">
<item-type>Number</item-type>
<label>Input Source</label>
<description>Select the input source of the AVR</description>
<state>
<options>
<option value="0">DVR/VCR</option>
<option value="1">SATELLITE/CABLE</option>
<option value="2">GAME</option>
<option value="3">AUX</option>
<option value="4">GAME2</option>
<option value="5">PC</option>
<option value="16">BLURAY/DVD</option>
<option value="17">STRM BOX</option>
<option value="18">TV</option>
<option value="32">TAPE1</option>
<option value="33">TAPE2</option>
<option value="34">PHONO</option>
<option value="35">CD/TV</option>
<option value="36">TUNER FM</option>
<option value="37">TUNER AM</option>
<option value="38">TUNER</option>
<option value="39">MUSICSERVER</option>
<option value="40">INTERNETRADIO</option>
<option value="41">USB</option>
<option value="42">USB_BACK</option>
<option value="43">NETWORK</option>
<option value="44">USB_TOGGLE</option>
<option value="45">AIRPLAY</option>
<option value="46">BLUETOOTH</option>
<option value="48">MULTICH</option>
<option value="50">SIRIUS</option>
<option value="128">SOURCE</option>
</options>
</state>
</channel-type>
<channel-type id="mute">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Mute/unmute your device</description>
</channel-type>
<channel-type id="volume">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Volume of your device</description>
<state min="0" max="100" pattern="%d %%">
</state>
</channel-type>
<channel-type id="control" advanced="true">
<item-type>Player</item-type>
<label>Control</label>
<description>Control the Zone Player, e.g. start/stop/next/previous/ffward/rewind</description>
<category>Player</category>
</channel-type>
<channel-type id="listenmode" advanced="true">
<item-type>Number</item-type>
<label>Listen Mode</label>
<description>Listen mode</description>
<state>
<options>
<option value="0">Stereo</option>
<option value="1">Direct</option>
<option value="3">Game RPG</option>
<option value="5">Game Action</option>
<option value="6">Game Rock</option>
<option value="8">Orchestra</option>
<option value="9">unplugged</option>
<option value="10">Studio Mix</option>
<option value="11">TV Logic</option>
<option value="12">All Channel Stereo</option>
<option value="13">Theater Dimensional</option>
<option value="14">Game-Sports</option>
<option value="15">Mono</option>
<option value="17">Pure Audio</option>
<option value="19">Full Mono</option>
<option value="22">Audyssey DSX</option>
<option value="64">5.1ch Surround</option>
<option value="128">PLII/PLIIx Movie</option>
<option value="129">PLII/PLIIx Music</option>
<option value="130">Neo 6/Neo:X Cinema + DTS:X/Neural:X</option>
<option value="131">Neo 6/Neo:X Music</option>
<option value="134">PLII/PLIIx Game</option>
<option value="160">PLIIx/PLII Movie + Audyssey DSX</option>
<option value="161">PLIIx/PLII Music + Audyssey DSX</option>
<option value="162">PLIIx/PLII Game + Audyssey DSX</option>
<option value="163">Neo Cinema DSX</option>
<option value="164">Neo Music DSX</option>
<option value="165">Neural Surround DSX</option>
<option value="166">Neural Digital DSX</option>
</options>
</state>
</channel-type>
<channel-type id="playuri" advanced="true">
<item-type>String</item-type>
<label>Play URI</label>
<description>Plays a given URI</description>
</channel-type>
<channel-type id="netControl" advanced="true">
<item-type>String</item-type>
<label>Control</label>
<description>Control the USB/Net Menu, e.g. Up/Down/Select/Back/PageUp/PageDown/Select[0-9]</description>
<state>
<options>
<option value="Up">Selection Up</option>
<option value="Down">Selection Down</option>
<option value="Select">Select Entry</option>
<option value="Back">Go Back</option>
<option value="PageUp">Scroll Page Up</option>
<option value="PageDown">Scroll Page Down</option>
<option value="Select0">Select Entry 0</option>
<option value="Select1">Select Entry 1</option>
<option value="Select2">Select Entry 2</option>
<option value="Select3">Select Entry 3</option>
<option value="Select4">Select Entry 4</option>
<option value="Select5">Select Entry 5</option>
<option value="Select6">Select Entry 6</option>
<option value="Select7">Select Entry 7</option>
<option value="Select8">Select Entry 8</option>
<option value="Select9">Select Entry 9</option>
</options>
</state>
</channel-type>
<!-- Onkyo variables -->
<channel-type id="currentPlayingTime" advanced="true">
<item-type>String</item-type>
<label>Playing Time</label>
<description>Current Playing Time</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="title" advanced="true">
<item-type>String</item-type>
<label>Title</label>
<description>Title of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="album" advanced="true">
<item-type>String</item-type>
<label>Album</label>
<description>Album name of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="albumArt" advanced="true">
<item-type>Image</item-type>
<label>Album Art</label>
<description>Image of cover art of the current song</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="albumArtUrl" advanced="true">
<item-type>String</item-type>
<label>Album Art Url</label>
<description>Url to the image of cover art of the current song</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="artist" advanced="true">
<item-type>String</item-type>
<label>Artist</label>
<description>Artist name of the current song</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="menuItem" advanced="true">
<item-type>String</item-type>
<label>Menu Item</label>
<description>Net/USB menu item</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="menuSelection" advanced="true">
<item-type>Number</item-type>
<label>Selected Item</label>
<description>Position of the currently selected menu item</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="HT-RC560">
<label>Onkyo HT-RC560 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<!-- Generic OnkyoAVR Thing Type -->
<thing-type id="onkyoAVR">
<label>Onkyo AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<!-- Onkyo unknown Thing Type -->
<thing-type id="onkyoUnsupported" listed="false">
<label>Unsupported Onkyo AV Receiver</label>
<description>Network enabled Onkyo AV Receivers for models that are not officially supported. You may experience some
odd behaviors.</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR3007">
<label>Onkyo TX-NR3007 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR414">
<label>Onkyo TX-NR414 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR474">
<label>Onkyo TX-NR474 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR509">
<label>Onkyo TX-NR509 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR515">
<label>Onkyo TX-NR515 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR525">
<label>Onkyo TX-NR525 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR535">
<label>Onkyo TX-NR535 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR545">
<label>Onkyo TX-NR545 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR555">
<label>Onkyo TX-NR555 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR575">
<label>Onkyo TX-NR575 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR575E">
<label>Onkyo TX-NR575E AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR616">
<label>Onkyo TX-NR616 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR626">
<label>Onkyo TX-NR626 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR636">
<label>Onkyo TX-NR636 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR646">
<label>Onkyo TX-NR646 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR656">
<label>Onkyo TX-NR656 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR676">
<label>Onkyo TX-NR676 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR686">
<label>Onkyo TX-NR686 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR708">
<label>Onkyo TX-NR708 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR717">
<label>Onkyo TX-NR717 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR727">
<label>Onkyo TX-NR727 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR737">
<label>Onkyo TX-NR737 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR747">
<label>Onkyo TX-NR747 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR809">
<label>Onkyo TX-NR809 AV Receiver</label>
<description>Network enabled Onkyo AV Receivers</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR818">
<label>Onkyo TX-NR818 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR828">
<label>Onkyo TX-NR828 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-NR838">
<label>Onkyo TX-NR838 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="onkyo"
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">
<thing-type id="TX-RZ900">
<label>Onkyo TX-RZ900 AV Receiver</label>
<description>Network enabled Onkyo AV Receiver</description>
<channel-groups>
<channel-group typeId="zone1Controls" id="zone1"/>
<channel-group typeId="zone2Controls" id="zone2"/>
<channel-group typeId="zone3Controls" id="zone3"/>
<channel-group typeId="playerControls" id="player"/>
<channel-group typeId="netMenuControls" id="netmenu"/>
</channel-groups>
<config-description-ref uri="thing-type:onkyo:config"/>
</thing-type>
</thing:thing-descriptions>