2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.yamahareceiver.internal.protocol.xml;
15 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
16 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.*;
17 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.Commands.*;
18 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getZoneResponse;
19 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
21 import java.io.IOException;
22 import java.lang.ref.WeakReference;
23 import java.util.function.Supplier;
25 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
26 import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
27 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
28 import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
29 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
30 import org.openhab.binding.yamahareceiver.internal.protocol.ZoneControl;
31 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
32 import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
33 import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36 import org.w3c.dom.Node;
39 * The zone protocol class is used to control one zone of a Yamaha receiver with HTTP/xml.
40 * No state will be saved in here, but in {@link ZoneControlState} instead.
42 * @author David Gräff - Refactored
45 * @author Tomasz Maruszak - Refactoring, input mapping fix, added Straight surround, volume DB fix and config
48 public class ZoneControlXML implements ZoneControl {
50 protected Logger logger = LoggerFactory.getLogger(ZoneControlXML.class);
52 private static final String SURROUND_PROGRAM_STRAIGHT = "Straight";
54 private final ZoneControlStateListener observer;
55 private final Supplier<InputConverter> inputConverterSupplier;
56 private final WeakReference<AbstractConnection> comReference;
57 private final Zone zone;
58 private final YamahaZoneConfig zoneConfig;
59 private final DeviceDescriptorXML.ZoneDescriptor zoneDescriptor;
61 protected CommandTemplate power = new CommandTemplate("<Power_Control><Power>%s</Power></Power_Control>",
62 "Power_Control/Power");
63 protected CommandTemplate mute = new CommandTemplate("<Volume><Mute>%s</Mute></Volume>", "Volume/Mute");
64 protected CommandTemplate volume = new CommandTemplate(
65 "<Volume><Lvl><Val>%d</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume>", "Volume/Lvl/Val");
66 protected CommandTemplate inputSel = new CommandTemplate("<Input><Input_Sel>%s</Input_Sel></Input>",
68 protected String inputSelNamePath = "Input/Input_Sel_Item_Info/Title";
69 protected CommandTemplate surroundSelProgram = new CommandTemplate(
70 "<Surround><Program_Sel><Current><Sound_Program>%s</Sound_Program></Current></Program_Sel></Surround>",
71 "Surround/Program_Sel/Current/Sound_Program");
72 protected CommandTemplate surroundSelStraight = new CommandTemplate(
73 "<Surround><Program_Sel><Current><Straight>On</Straight></Current></Program_Sel></Surround>",
74 "Surround/Program_Sel/Current/Straight");
75 protected CommandTemplate sceneSel = new CommandTemplate("<Scene><Scene_Sel>%s</Scene_Sel></Scene>");
76 protected boolean sceneSelSupported = false;
77 protected CommandTemplate dialogueLevel = new CommandTemplate(
78 "<Sound_Video><Dialogue_Adjust><Dialogue_Lvl>%d</Dialogue_Lvl></Dialogue_Adjust></Sound_Video>",
79 "Sound_Video/Dialogue_Adjust/Dialogue_Lvl");
80 protected boolean dialogueLevelSupported = false;
82 public ZoneControlXML(AbstractConnection con, Zone zone, YamahaZoneConfig zoneSettings,
83 ZoneControlStateListener observer, DeviceInformationState deviceInformationState,
84 Supplier<InputConverter> inputConverterSupplier) {
85 this.comReference = new WeakReference<>(con);
87 this.zoneConfig = zoneSettings;
88 this.zoneDescriptor = DeviceDescriptorXML.getAttached(deviceInformationState).zones.getOrDefault(zone, null);
89 this.observer = observer;
90 this.inputConverterSupplier = inputConverterSupplier;
92 this.applyModelVariations();
96 * Apply command changes to ensure compatibility with all supported models
98 protected void applyModelVariations() {
99 if (zoneDescriptor == null) {
100 logger.trace("Zone {} - descriptor not available", getZone());
104 logger.trace("Zone {} - compatibility detection", getZone());
106 // Note: Detection if scene is supported
107 sceneSelSupported = zoneDescriptor.hasCommandEnding("Scene,Scene_Sel", () -> logger
108 .debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_SCENE));
110 // Note: Detection if dialogue level is supported
111 dialogueLevelSupported = zoneDescriptor.hasAnyCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lvl",
112 "Sound_Video,Dialogue_Adjust,Dialogue_Lift");
113 if (zoneDescriptor.hasCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lift")) {
114 dialogueLevel = dialogueLevel.replace("Dialogue_Lvl", "Dialogue_Lift");
115 logger.debug("Zone {} - adjusting command to: {}", getZone(), dialogueLevel);
117 if (!dialogueLevelSupported) {
118 logger.debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_DIALOGUE_LEVEL);
121 // Note: Detection for RX-V3900, which uses <Vol> instead of <Volume>
122 if (zoneDescriptor.hasCommandEnding("Vol,Lvl")) {
123 volume = volume.replace("Volume", "Vol");
124 logger.debug("Zone {} - adjusting command to: {}", getZone(), volume);
126 if (zoneDescriptor.hasCommandEnding("Vol,Mute")) {
127 mute = mute.replace("Volume", "Vol");
128 logger.debug("Zone {} - adjusting command to: {}", getZone(), mute);
132 // Note: Detection for RX-V3900, which has a different XML node for surround program
133 Node basicStatusNode = getZoneResponse(comReference.get(), getZone(), ZONE_BASIC_STATUS_CMD,
134 ZONE_BASIC_STATUS_PATH);
135 String surroundProgram = getNodeContentOrEmpty(basicStatusNode, "Surr/Pgm_Sel/Pgm");
137 if (!surroundProgram.isEmpty()) {
138 surroundSelProgram = new CommandTemplate(
139 "<Surr><Pgm_Sel><Straight>Off</Straight><Pgm>%s</Pgm></Pgm_Sel></Surr>", "Surr/Pgm_Sel/Pgm");
140 logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelProgram);
142 surroundSelStraight = new CommandTemplate("<Surr><Pgm_Sel><Straight>On</Straight></Pgm_Sel></Surr>",
143 "Surr/Pgm_Sel/Straight");
144 logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelStraight);
147 } catch (ReceivedMessageParseException | IOException e) {
148 logger.debug("Could not perform feature detection for RX-V3900");
152 protected void sendCommand(String message) throws IOException {
153 comReference.get().send(XMLUtils.wrZone(zone, message));
159 public Zone getZone() {
164 public void setPower(boolean on) throws IOException, ReceivedMessageParseException {
165 String cmd = power.apply(on ? ON : POWER_STANDBY);
171 public void setMute(boolean on) throws IOException, ReceivedMessageParseException {
172 String cmd = this.mute.apply(on ? ON : OFF);
178 * Sets the absolute volume in decibel.
180 * @param volume Absolute value in decibel ([-80,+12]).
181 * @throws IOException
184 public void setVolumeDB(float volume) throws IOException, ReceivedMessageParseException {
185 if (volume < zoneConfig.getVolumeDbMin()) {
186 volume = zoneConfig.getVolumeDbMin();
188 if (volume > zoneConfig.getVolumeDbMax()) {
189 volume = zoneConfig.getVolumeDbMax();
192 // Yamaha accepts only integer values with .0 or .5 at the end only (-20.5dB, -20.0dB) - at least on RX-S601D.
193 // The order matters here. We want to cast to integer first and then scale by 10.
194 // Effectively we're only allowing dB values with .0 at the end.
195 int vol = (int) volume * 10;
196 sendCommand(this.volume.apply(vol));
201 * Sets the volume in percent
204 * @throws IOException
207 public void setVolume(float volume) throws IOException, ReceivedMessageParseException {
214 // Compute value in db
215 setVolumeDB(zoneConfig.getVolumeDb(volume));
219 * Increase or decrease the volume by the given percentage.
222 * @throws IOException
225 public void setVolumeRelative(ZoneControlState state, float percent)
226 throws IOException, ReceivedMessageParseException {
227 setVolume(zoneConfig.getVolumePercentage(state.volumeDB) + percent);
231 public void setInput(String name) throws IOException, ReceivedMessageParseException {
232 name = inputConverterSupplier.get().toCommandName(name);
233 String cmd = inputSel.apply(name);
239 public void setSurroundProgram(String name) throws IOException, ReceivedMessageParseException {
240 String cmd = name.equalsIgnoreCase(SURROUND_PROGRAM_STRAIGHT) ? surroundSelStraight.apply()
241 : surroundSelProgram.apply(name);
248 public void setDialogueLevel(int level) throws IOException, ReceivedMessageParseException {
249 if (!dialogueLevelSupported) {
252 sendCommand(dialogueLevel.apply(level));
257 public void setScene(String scene) throws IOException, ReceivedMessageParseException {
258 if (!sceneSelSupported) {
261 sendCommand(sceneSel.apply(scene));
266 public void update() throws IOException, ReceivedMessageParseException {
267 if (observer == null) {
271 Node statusNode = getZoneResponse(comReference.get(), zone, ZONE_BASIC_STATUS_CMD, ZONE_BASIC_STATUS_PATH);
275 ZoneControlState state = new ZoneControlState();
277 value = getNodeContentOrEmpty(statusNode, power.getPath());
278 state.power = ON.equalsIgnoreCase(value);
280 value = getNodeContentOrEmpty(statusNode, mute.getPath());
281 state.mute = ON.equalsIgnoreCase(value);
283 // The value comes in dB x 10, on AVR it says -30.5dB, the values comes as -305
284 value = getNodeContentOrDefault(statusNode, volume.getPath(), String.valueOf(zoneConfig.getVolumeDbMin()));
285 state.volumeDB = Float.parseFloat(value) * .1f; // in dB
287 value = getNodeContentOrEmpty(statusNode, inputSel.getPath());
288 state.inputID = inputConverterSupplier.get().fromStateName(value);
289 if (state.inputID == null || state.inputID.isBlank()) {
290 throw new ReceivedMessageParseException("Expected inputID. Failed to read Input/Input_Sel");
293 // Some receivers may use Src_Name instead?
294 value = getNodeContentOrEmpty(statusNode, inputSelNamePath);
295 state.inputName = value;
297 value = getNodeContentOrEmpty(statusNode, surroundSelStraight.getPath());
298 boolean straightOn = ON.equalsIgnoreCase(value);
300 value = getNodeContentOrEmpty(statusNode, surroundSelProgram.getPath());
301 // Surround is either in straight mode or sound program
302 state.surroundProgram = straightOn ? SURROUND_PROGRAM_STRAIGHT : value;
304 value = getNodeContentOrDefault(statusNode, dialogueLevel.getPath(), "0");
305 state.dialogueLevel = Integer.parseInt(value);
307 logger.debug("Zone {} state - power: {}, mute: {}, volumeDB: {}, input: {}, surroundProgram: {}", getZone(),
308 state.power, state.mute, state.volumeDB, state.inputID, state.surroundProgram);
310 observer.zoneStateChanged(state);