[denonmarantz] Add HTTP protocol support for newer receivers (#16748)

* Add HTTP protocol support for newer receivers

Resolves #16747

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2024-05-25 13:53:02 +02:00 committed by GitHub
parent 7dcaf86607
commit f8700a9cdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 272 additions and 98 deletions

View File

@ -7,18 +7,18 @@ This binding integrates Denon & Marantz AV receivers by using either Telnet or a
This binding supports Denon and Marantz receivers having a Telnet interface or a web based controller at `http://<AVR IP address>/`. This binding supports Denon and Marantz receivers having a Telnet interface or a web based controller at `http://<AVR IP address>/`.
The thing type for all of them is `avr`. The thing type for all of them is `avr`.
Tested models: Marantz SR5008, Denon AVR-X2000 / X3000 / X1200W / X2100W / X2200W / X3100W / X3300W / X4400H Tested models: Marantz SR5008, Denon AVR-3808 / AVR-4520 / AVR-X2000 / X3000 / X1200W / X2100W / X2200W / X3100W / X3300W / X4400H / X4800H
Denon models with HEOS support (`AVR-X..00H`) do not support the HTTP API. They do support Telnet.
During Discovery this is auto-detected and configured.
## Discovery ## Discovery
This binding can discover Denon and Marantz receivers using mDNS. This binding can discover Denon and Marantz receivers using mDNS.
The serial number (which is the MAC address of the network interface) is used as unique identifier. The serial number (which is the MAC address of the network interface) is used as unique identifier.
The protocol will be auto-detected.
The HTTP port as well as slight variations in the API will be auto-detected as well.
It tries to detect the number of zones (when the AVR responds to HTTP). It tries to detect the number of zones (when the AVR responds to HTTP).
It defaults to 2 zones. It defaults to two zones.
## Thing Configuration ## Thing Configuration

View File

@ -14,14 +14,16 @@ package org.openhab.binding.denonmarantz.internal.connector.http;
import java.beans.Introspector; import java.beans.Introspector;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.math.BigDecimal;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
@ -35,11 +37,17 @@ import javax.xml.stream.util.StreamReaderDelegate;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.denonmarantz.internal.DenonMarantzState; import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration; import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector; import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
import org.openhab.binding.denonmarantz.internal.exception.HttpCommunicationException;
import org.openhab.binding.denonmarantz.internal.xml.dto.Deviceinfo; import org.openhab.binding.denonmarantz.internal.xml.dto.Deviceinfo;
import org.openhab.binding.denonmarantz.internal.xml.dto.Main; import org.openhab.binding.denonmarantz.internal.xml.dto.Main;
import org.openhab.binding.denonmarantz.internal.xml.dto.ZoneStatus; import org.openhab.binding.denonmarantz.internal.xml.dto.ZoneStatus;
@ -48,7 +56,7 @@ import org.openhab.binding.denonmarantz.internal.xml.dto.commands.AppCommandRequ
import org.openhab.binding.denonmarantz.internal.xml.dto.commands.AppCommandResponse; import org.openhab.binding.denonmarantz.internal.xml.dto.commands.AppCommandResponse;
import org.openhab.binding.denonmarantz.internal.xml.dto.commands.CommandRx; import org.openhab.binding.denonmarantz.internal.xml.dto.commands.CommandRx;
import org.openhab.binding.denonmarantz.internal.xml.dto.commands.CommandTx; import org.openhab.binding.denonmarantz.internal.xml.dto.commands.CommandTx;
import org.openhab.core.io.net.http.HttpUtil; import org.openhab.binding.denonmarantz.internal.xml.dto.types.StringType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -92,6 +100,8 @@ public class DenonMarantzHttpConnector extends DenonMarantzConnector {
private @Nullable ScheduledFuture<?> pollingJob; private @Nullable ScheduledFuture<?> pollingJob;
private boolean legacyApiSupported = true;
public DenonMarantzHttpConnector(DenonMarantzConfiguration config, DenonMarantzState state, public DenonMarantzHttpConnector(DenonMarantzConfiguration config, DenonMarantzState state,
ScheduledExecutorService scheduler, HttpClient httpClient) { ScheduledExecutorService scheduler, HttpClient httpClient) {
super(config, scheduler, state); super(config, scheduler, state);
@ -114,16 +124,19 @@ public class DenonMarantzHttpConnector extends DenonMarantzConnector {
logger.debug("HTTP polling started."); logger.debug("HTTP polling started.");
try { try {
setConfigProperties(); setConfigProperties();
} catch (IOException e) { } catch (TimeoutException | ExecutionException | HttpCommunicationException e) {
logger.debug("IO error while retrieving document:", e); logger.debug("IO error while retrieving document:", e);
state.connectionError("IO error while connecting to AVR: " + e.getMessage()); state.connectionError("IO error while connecting to AVR: " + e.getMessage());
return; return;
} catch (InterruptedException e) {
logger.debug("Interrupted while retrieving document: {}", e.getMessage());
Thread.currentThread().interrupt();
} }
pollingJob = scheduler.scheduleWithFixedDelay(() -> { pollingJob = scheduler.scheduleWithFixedDelay(() -> {
try { try {
refreshHttpProperties(); refreshHttpProperties();
} catch (IOException e) { } catch (TimeoutException | ExecutionException e) {
logger.debug("IO error while retrieving document", e); logger.debug("IO error while retrieving document", e);
state.connectionError("IO error while connecting to AVR: " + e.getMessage()); state.connectionError("IO error while connecting to AVR: " + e.getMessage());
stopPolling(); stopPolling();
@ -137,6 +150,9 @@ public class DenonMarantzHttpConnector extends DenonMarantzConnector {
sb.append(s.toString()).append("\n"); sb.append(s.toString()).append("\n");
} }
logger.error("Error while polling Http: \"{}\". Stacktrace: \n{}", e.getMessage(), sb.toString()); logger.error("Error while polling Http: \"{}\". Stacktrace: \n{}", e.getMessage(), sb.toString());
} catch (InterruptedException e) {
logger.debug("Interrupted while polling: {}", e.getMessage());
Thread.currentThread().interrupt();
} }
}, 0, config.httpPollingInterval, TimeUnit.SECONDS); }, 0, config.httpPollingInterval, TimeUnit.SECONDS);
} }
@ -186,96 +202,163 @@ public class DenonMarantzHttpConnector extends DenonMarantzConnector {
}); });
} }
private void updateMain() throws IOException { private void updateMain() throws TimeoutException, ExecutionException, InterruptedException {
String url = statusUrl + URL_MAIN; String url = statusUrl + URL_MAIN;
logger.trace("Refreshing URL: {}", url); logger.trace("Refreshing URL: {}", url);
Main statusMain = getDocument(url, Main.class); try {
if (statusMain != null) { Main statusMain = getDocument(url, Main.class);
state.setPower(statusMain.getPower().getValue()); if (statusMain != null) {
state.setPower(statusMain.getPower().getValue());
}
} catch (HttpCommunicationException e) {
if (e.getHttpStatus() == HttpStatus.FORBIDDEN_403) {
legacyApiSupported = false;
logger.debug("Legacy API not supported, will attempt app command method");
} else {
logger.debug("Failed to update main by legacy API: {}", e.getMessage());
}
} }
} }
private void updateMainZone() throws IOException { private void updateMainZone() throws TimeoutException, ExecutionException, InterruptedException {
String url = statusUrl + URL_ZONE_MAIN; String url = statusUrl + URL_ZONE_MAIN;
logger.trace("Refreshing URL: {}", url); logger.trace("Refreshing URL: {}", url);
ZoneStatus mainZone = getDocument(url, ZoneStatus.class); try {
if (mainZone != null) { ZoneStatus mainZone = getDocument(url, ZoneStatus.class);
state.setInput(mainZone.getInputFuncSelect().getValue()); if (mainZone != null) {
state.setMainVolume(mainZone.getMasterVolume().getValue()); state.setInput(mainZone.getInputFuncSelect().getValue());
state.setMainZonePower(mainZone.getPower().getValue()); state.setMainVolume(mainZone.getMasterVolume().getValue());
state.setMute(mainZone.getMute().getValue()); state.setMainZonePower(mainZone.getPower().getValue());
state.setMute(mainZone.getMute().getValue());
if (config.inputOptions == null) { if (config.inputOptions == null) {
config.inputOptions = mainZone.getInputFuncList(); config.inputOptions = mainZone.getInputFuncList();
}
StringType surroundMode = mainZone.getSurrMode();
if (surroundMode == null) {
logger.debug("Unable to get the SURROUND_MODE. MainZone update may not be correct.");
} else {
state.setSurroundProgram(surroundMode.getValue());
}
} }
} catch (HttpCommunicationException e) {
if (mainZone.getSurrMode() == null) { if (e.getHttpStatus() == HttpStatus.FORBIDDEN_403) {
logger.debug("Unable to get the SURROUND_MODE. MainZone update may not be correct."); legacyApiSupported = false;
logger.debug("Legacy API not supported, will attempt app command method");
} else { } else {
state.setSurroundProgram(mainZone.getSurrMode().getValue()); logger.debug("Failed to update main zone by legacy API: {}", e.getMessage());
} }
} }
} }
private void updateSecondaryZones() throws IOException { private void updateMainZoneByAppCommand() throws TimeoutException, ExecutionException, InterruptedException {
String url = statusUrl + URL_APP_COMMAND;
logger.trace("Refreshing URL: {}", url);
AppCommandRequest request = AppCommandRequest.of(CommandTx.CMD_ALL_POWER).add(CommandTx.CMD_VOLUME_LEVEL)
.add(CommandTx.CMD_MUTE_STATUS).add(CommandTx.CMD_SOURCE_STATUS).add(CommandTx.CMD_SURROUND_STATUS);
try {
AppCommandResponse response = postDocument(url, AppCommandResponse.class, request);
if (response != null) {
for (CommandRx rx : response.getCommands()) {
String inputSource = rx.getSource();
if (inputSource != null) {
state.setInput(inputSource);
}
Boolean power = rx.getZone1();
if (power != null) {
state.setMainZonePower(power.booleanValue());
}
BigDecimal volume = rx.getVolume();
if (volume != null) {
state.setMainVolume(volume);
}
Boolean mute = rx.getMute();
if (mute != null) {
state.setMute(mute.booleanValue());
}
String surroundMode = rx.getSurround();
if (surroundMode != null) {
state.setSurroundProgram(surroundMode);
}
}
}
} catch (HttpCommunicationException e) {
logger.debug("Failed to update main zone by app command: {}", e.getMessage());
}
}
private void updateSecondaryZones() throws TimeoutException, ExecutionException, InterruptedException {
for (int i = 2; i <= config.getZoneCount(); i++) { for (int i = 2; i <= config.getZoneCount(); i++) {
String url = String.format("%s" + URL_ZONE_SECONDARY_LITE, statusUrl, i, i); String url = String.format("%s" + URL_ZONE_SECONDARY_LITE, statusUrl, i, i);
logger.trace("Refreshing URL: {}", url); logger.trace("Refreshing URL: {}", url);
ZoneStatusLite zoneSecondary = getDocument(url, ZoneStatusLite.class); try {
if (zoneSecondary != null) { ZoneStatusLite zoneSecondary = getDocument(url, ZoneStatusLite.class);
switch (i) { if (zoneSecondary != null) {
// maximum 2 secondary zones are supported switch (i) {
case 2: // maximum 2 secondary zones are supported
state.setZone2Power(zoneSecondary.getPower().getValue()); case 2:
state.setZone2Volume(zoneSecondary.getMasterVolume().getValue()); state.setZone2Power(zoneSecondary.getPower().getValue());
state.setZone2Mute(zoneSecondary.getMute().getValue()); state.setZone2Volume(zoneSecondary.getMasterVolume().getValue());
state.setZone2Input(zoneSecondary.getInputFuncSelect().getValue()); state.setZone2Mute(zoneSecondary.getMute().getValue());
break; state.setZone2Input(zoneSecondary.getInputFuncSelect().getValue());
case 3: break;
state.setZone3Power(zoneSecondary.getPower().getValue()); case 3:
state.setZone3Volume(zoneSecondary.getMasterVolume().getValue()); state.setZone3Power(zoneSecondary.getPower().getValue());
state.setZone3Mute(zoneSecondary.getMute().getValue()); state.setZone3Volume(zoneSecondary.getMasterVolume().getValue());
state.setZone3Input(zoneSecondary.getInputFuncSelect().getValue()); state.setZone3Mute(zoneSecondary.getMute().getValue());
break; state.setZone3Input(zoneSecondary.getInputFuncSelect().getValue());
case 4: break;
state.setZone4Power(zoneSecondary.getPower().getValue()); case 4:
state.setZone4Volume(zoneSecondary.getMasterVolume().getValue()); state.setZone4Power(zoneSecondary.getPower().getValue());
state.setZone4Mute(zoneSecondary.getMute().getValue()); state.setZone4Volume(zoneSecondary.getMasterVolume().getValue());
state.setZone4Input(zoneSecondary.getInputFuncSelect().getValue()); state.setZone4Mute(zoneSecondary.getMute().getValue());
break; state.setZone4Input(zoneSecondary.getInputFuncSelect().getValue());
break;
}
} }
} catch (HttpCommunicationException e) {
logger.debug("Failed to update zone {}: {}", i, e.getMessage());
} }
} }
} }
private void updateDisplayInfo() throws IOException { private void updateDisplayInfo() throws TimeoutException, ExecutionException, InterruptedException {
String url = statusUrl + URL_APP_COMMAND; String url = statusUrl + URL_APP_COMMAND;
logger.trace("Refreshing URL: {}", url); logger.trace("Refreshing URL: {}", url);
AppCommandRequest request = AppCommandRequest.of(CommandTx.CMD_NET_STATUS); AppCommandRequest request = AppCommandRequest.of(CommandTx.CMD_NET_STATUS);
AppCommandResponse response = postDocument(url, AppCommandResponse.class, request); try {
AppCommandResponse response = postDocument(url, AppCommandResponse.class, request);
if (response == null) { if (response == null) {
return; return;
} }
CommandRx titleInfo = response.getCommands().get(0); CommandRx titleInfo = response.getCommands().get(0);
String artist = titleInfo.getText("artist"); String artist = titleInfo.getText("artist");
if (artist != null) { if (artist != null) {
state.setNowPlayingArtist(artist); state.setNowPlayingArtist(artist);
} }
String album = titleInfo.getText("album"); String album = titleInfo.getText("album");
if (album != null) { if (album != null) {
state.setNowPlayingAlbum(album); state.setNowPlayingAlbum(album);
} }
String track = titleInfo.getText("track"); String track = titleInfo.getText("track");
if (track != null) { if (track != null) {
state.setNowPlayingTrack(track); state.setNowPlayingTrack(track);
}
} catch (HttpCommunicationException e) {
logger.debug("Failed to update display info: {}", e.getMessage());
} }
} }
private boolean setConfigProperties() throws IOException { private boolean setConfigProperties()
throws TimeoutException, ExecutionException, InterruptedException, HttpCommunicationException {
String url = statusUrl + URL_DEVICE_INFO; String url = statusUrl + URL_DEVICE_INFO;
logger.debug("Refreshing URL: {}", url); logger.debug("Refreshing URL: {}", url);
@ -295,20 +378,39 @@ public class DenonMarantzHttpConnector extends DenonMarantzConnector {
return (deviceinfo != null); return (deviceinfo != null);
} }
private void refreshHttpProperties() throws IOException { private void refreshHttpProperties() throws TimeoutException, ExecutionException, InterruptedException {
logger.trace("Refreshing Denon status"); logger.trace("Refreshing Denon status");
updateMain(); if (legacyApiSupported) {
updateMainZone(); updateMain();
updateMainZone();
}
if (!legacyApiSupported) {
updateMainZoneByAppCommand();
}
updateSecondaryZones(); updateSecondaryZones();
updateDisplayInfo(); updateDisplayInfo();
} }
@Nullable @Nullable
private <T> T getDocument(String uri, Class<T> response) throws IOException { private <T> T getDocument(String uri, Class<T> response)
throws TimeoutException, ExecutionException, InterruptedException, HttpCommunicationException {
try { try {
String result = HttpUtil.executeUrl("GET", uri, REQUEST_TIMEOUT_MS); Request request = httpClient.newRequest(uri).timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
logger.trace("result of getDocument for uri '{}':\r\n{}", uri, result); .method(HttpMethod.GET);
ContentResponse contentResponse = request.send();
String result = contentResponse.getContentAsString();
int status = contentResponse.getStatus();
logger.trace("result of getDocument for uri '{}' (status code {}):\r\n{}", uri, status, result);
if (!HttpStatus.isSuccess(status)) {
throw new HttpCommunicationException("Error retrieving document for uri '" + uri + "'", status);
}
if (result != null && !result.isBlank()) { if (result != null && !result.isBlank()) {
JAXBContext jc = JAXBContext.newInstance(response); JAXBContext jc = JAXBContext.newInstance(response);
@ -336,15 +438,28 @@ public class DenonMarantzHttpConnector extends DenonMarantzConnector {
} }
@Nullable @Nullable
private <T, S> T postDocument(String uri, Class<T> response, S request) throws IOException { private <T, S> T postDocument(String uri, Class<T> response, S request)
throws TimeoutException, ExecutionException, InterruptedException, HttpCommunicationException {
try { try {
JAXBContext jaxbContext = JAXBContext.newInstance(request.getClass()); JAXBContext jaxbContext = JAXBContext.newInstance(request.getClass());
Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
jaxbMarshaller.marshal(request, sw); jaxbMarshaller.marshal(request, sw);
ByteArrayInputStream inputStream = new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8)); Request httpRequest = httpClient.newRequest(uri).timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
String result = HttpUtil.executeUrl("POST", uri, inputStream, CONTENT_TYPE_XML, REQUEST_TIMEOUT_MS); .content(new StringContentProvider(sw.toString(), StandardCharsets.UTF_8), CONTENT_TYPE_XML)
.method(HttpMethod.POST);
ContentResponse contentResponse = httpRequest.send();
String result = contentResponse.getContentAsString();
int status = contentResponse.getStatus();
logger.trace("result of postDocument for uri '{}' (status code {}):\r\n{}", uri, status, result);
if (!HttpStatus.isSuccess(status)) {
throw new HttpCommunicationException("Error retrieving document for uri '" + uri + "'", status);
}
if (result != null && !result.isBlank()) { if (result != null && !result.isBlank()) {
JAXBContext jcResponse = JAXBContext.newInstance(response); JAXBContext jcResponse = JAXBContext.newInstance(response);

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.denonmarantz.internal.exception;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link HttpCommunicationException} is a generic exception thrown in case
* of communication failure or unexpected response.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class HttpCommunicationException extends Exception {
private static final long serialVersionUID = 1L;
private int httpStatus = 0;
public HttpCommunicationException(String message, int httpStatus) {
super(message);
this.httpStatus = httpStatus;
}
public int getHttpStatus() {
return httpStatus;
}
}

View File

@ -229,7 +229,7 @@ public class DenonMarantzHandler extends BaseThingHandler implements DenonMarant
httpApiUsable = true; httpApiUsable = true;
} }
} catch (TimeoutException | ExecutionException e) { } catch (TimeoutException | ExecutionException e) {
logger.debug("Error when trying to access AVR using HTTP on port 80, reverting to Telnet mode.", e); logger.debug("Error when trying to access AVR using HTTP on port 80.", e);
} }
if (telnetEnable) { if (telnetEnable) {
@ -239,13 +239,15 @@ public class DenonMarantzHandler extends BaseThingHandler implements DenonMarant
response = httpClient.newRequest("http://" + host + ":8080/goform/Deviceinfo.xml") response = httpClient.newRequest("http://" + host + ":8080/goform/Deviceinfo.xml")
.timeout(3, TimeUnit.SECONDS).send(); .timeout(3, TimeUnit.SECONDS).send();
if (response.getStatus() == HttpURLConnection.HTTP_OK) { if (response.getStatus() == HttpURLConnection.HTTP_OK) {
logger.debug( logger.debug("This model responds to HTTP port 8080, disabling the Telnet mode by default.");
"This model responds to HTTP port 8080, we use this port to retrieve the number of zones."); telnetEnable = false;
httpPort = 8080; httpPort = 8080;
httpApiUsable = true; httpApiUsable = true;
} }
} catch (TimeoutException | ExecutionException e) { } catch (TimeoutException | ExecutionException e) {
logger.debug("Additionally tried to connect to port 8080, this also failed", e); logger.debug(
"Additionally tried to connect to port 8080, this also failed. Reverting to Telnet mode.",
e);
} }
} }

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.denonmarantz.internal.xml.dto.commands; package org.openhab.binding.denonmarantz.internal.xml.dto.commands;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -20,9 +21,12 @@ import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.denonmarantz.internal.xml.adapters.OnOffAdapter;
import org.openhab.binding.denonmarantz.internal.xml.adapters.VolumeAdapter;
/** /**
* Response to a {@link CommandTx} * Response to a {@link CommandTx}
@ -33,21 +37,27 @@ import org.eclipse.jdt.annotation.Nullable;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CommandRx { public class CommandRx {
private String zone1; @XmlJavaTypeAdapter(OnOffAdapter.class)
private Boolean zone1;
private String zone2; @XmlJavaTypeAdapter(OnOffAdapter.class)
private Boolean zone2;
private String zone3; @XmlJavaTypeAdapter(OnOffAdapter.class)
private Boolean zone3;
private String zone4; @XmlJavaTypeAdapter(OnOffAdapter.class)
private Boolean zone4;
private String volume; @XmlJavaTypeAdapter(value = VolumeAdapter.class)
private BigDecimal volume;
private String disptype; private String disptype;
private String dispvalue; private String dispvalue;
private String mute; @XmlJavaTypeAdapter(OnOffAdapter.class)
private Boolean mute;
private String type; private String type;
@ -72,46 +82,48 @@ public class CommandRx {
private String source; private String source;
private String surround;
public CommandRx() { public CommandRx() {
} }
public String getZone1() { public Boolean getZone1() {
return zone1; return zone1;
} }
public void setZone1(String zone1) { public void setZone1(Boolean zone1) {
this.zone1 = zone1; this.zone1 = zone1;
} }
public String getZone2() { public Boolean getZone2() {
return zone2; return zone2;
} }
public void setZone2(String zone2) { public void setZone2(Boolean zone2) {
this.zone2 = zone2; this.zone2 = zone2;
} }
public String getZone3() { public Boolean getZone3() {
return zone3; return zone3;
} }
public void setZone3(String zone3) { public void setZone3(Boolean zone3) {
this.zone3 = zone3; this.zone3 = zone3;
} }
public String getZone4() { public Boolean getZone4() {
return zone4; return zone4;
} }
public void setZone4(String zone4) { public void setZone4(Boolean zone4) {
this.zone4 = zone4; this.zone4 = zone4;
} }
public String getVolume() { public BigDecimal getVolume() {
return volume; return volume;
} }
public void setVolume(String volume) { public void setVolume(BigDecimal volume) {
this.volume = volume; this.volume = volume;
} }
@ -131,11 +143,11 @@ public class CommandRx {
this.dispvalue = dispvalue; this.dispvalue = dispvalue;
} }
public String getMute() { public Boolean getMute() {
return mute; return mute;
} }
public void setMute(String mute) { public void setMute(Boolean mute) {
this.mute = mute; this.mute = mute;
} }
@ -187,6 +199,14 @@ public class CommandRx {
this.source = source; this.source = source;
} }
public String getSurround() {
return surround;
}
public void setSurround(String surround) {
this.surround = surround;
}
public @Nullable String getText(@NonNull String key) { public @Nullable String getText(@NonNull String key) {
for (Text text : texts) { for (Text text : texts) {
if (key.equals(text.getId())) { if (key.equals(text.getId())) {