]> git.basschouten.com Git - openhab-addons.git/blob
801ed5ea2ce0b671e0f282af5f999d3f16b4fe71
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.yamahareceiver.internal.protocol.xml;
14
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.*;
20
21 import java.io.IOException;
22 import java.lang.ref.WeakReference;
23 import java.util.function.Supplier;
24
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;
37
38 /**
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.
41  *
42  * @author David Gräff - Refactored
43  * @author Eric Thill
44  * @author Ben Jones
45  * @author Tomasz Maruszak - Refactoring, input mapping fix, added Straight surround, volume DB fix and config
46  *         improvement.
47  */
48 public class ZoneControlXML implements ZoneControl {
49
50     protected Logger logger = LoggerFactory.getLogger(ZoneControlXML.class);
51
52     private static final String SURROUND_PROGRAM_STRAIGHT = "Straight";
53
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;
60
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>",
67             "Input/Input_Sel");
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 CommandTemplate hdmi1Out = new CommandTemplate(
81             "<System><Sound_Video><HDMI><Output><OUT_1>%s</OUT_1></Output></HDMI></Sound_Video></System>",
82             "Sound_Video/HDMI/Output/OUT_1");
83     protected CommandTemplate hdmi2Out = new CommandTemplate(
84             "<System><Sound_Video><HDMI><Output><OUT_2>%s</OUT_2></Output></HDMI></Sound_Video></System>",
85             "Sound_Video/HDMI/Output/OUT_2");
86     protected boolean dialogueLevelSupported = false;
87     protected boolean hdmi1OutSupported = false;
88     protected boolean hdmi2OutSupported = false;
89
90     public ZoneControlXML(AbstractConnection con, Zone zone, YamahaZoneConfig zoneSettings,
91             ZoneControlStateListener observer, DeviceInformationState deviceInformationState,
92             Supplier<InputConverter> inputConverterSupplier) {
93         this.comReference = new WeakReference<>(con);
94         this.zone = zone;
95         this.zoneConfig = zoneSettings;
96         this.zoneDescriptor = DeviceDescriptorXML.getAttached(deviceInformationState).zones.getOrDefault(zone, null);
97         this.observer = observer;
98         this.inputConverterSupplier = inputConverterSupplier;
99
100         this.applyModelVariations();
101     }
102
103     /**
104      * Apply command changes to ensure compatibility with all supported models
105      */
106     protected void applyModelVariations() {
107         if (zoneDescriptor == null) {
108             logger.trace("Zone {} - descriptor not available", getZone());
109             return;
110         }
111
112         logger.trace("Zone {} - compatibility detection", getZone());
113
114         // Note: Detection if scene is supported
115         sceneSelSupported = zoneDescriptor.hasCommandEnding("Scene,Scene_Sel", () -> logger
116                 .debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_SCENE));
117
118         // Note: Detection if dialogue level is supported
119         dialogueLevelSupported = zoneDescriptor.hasAnyCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lvl",
120                 "Sound_Video,Dialogue_Adjust,Dialogue_Lift");
121         if (zoneDescriptor.hasCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lift")) {
122             dialogueLevel = dialogueLevel.replace("Dialogue_Lvl", "Dialogue_Lift");
123             logger.debug("Zone {} - adjusting command to: {}", getZone(), dialogueLevel);
124         }
125         if (!dialogueLevelSupported) {
126             logger.debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_DIALOGUE_LEVEL);
127         }
128
129         hdmi1OutSupported = zoneDescriptor.hasCommandEnding("Sound_Video,HDMI,Output,OUT_1", () -> logger
130                 .debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_HDMI1OUT));
131
132         hdmi2OutSupported = zoneDescriptor.hasCommandEnding("Sound_Video,HDMI,Output,OUT_2", () -> logger
133                 .debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_HDMI2OUT));
134
135         // Note: Detection for RX-V3900, which uses <Vol> instead of <Volume>
136         if (zoneDescriptor.hasCommandEnding("Vol,Lvl")) {
137             volume = volume.replace("Volume", "Vol");
138             logger.debug("Zone {} - adjusting command to: {}", getZone(), volume);
139         }
140         if (zoneDescriptor.hasCommandEnding("Vol,Mute")) {
141             mute = mute.replace("Volume", "Vol");
142             logger.debug("Zone {} - adjusting command to: {}", getZone(), mute);
143         }
144
145         try {
146             // Note: Detection for RX-V3900, which has a different XML node for surround program
147             Node basicStatusNode = getZoneResponse(comReference.get(), getZone(), ZONE_BASIC_STATUS_CMD,
148                     ZONE_BASIC_STATUS_PATH);
149             String surroundProgram = getNodeContentOrEmpty(basicStatusNode, "Surr/Pgm_Sel/Pgm");
150
151             if (!surroundProgram.isEmpty()) {
152                 surroundSelProgram = new CommandTemplate(
153                         "<Surr><Pgm_Sel><Straight>Off</Straight><Pgm>%s</Pgm></Pgm_Sel></Surr>", "Surr/Pgm_Sel/Pgm");
154                 logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelProgram);
155
156                 surroundSelStraight = new CommandTemplate("<Surr><Pgm_Sel><Straight>On</Straight></Pgm_Sel></Surr>",
157                         "Surr/Pgm_Sel/Straight");
158                 logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelStraight);
159             }
160
161         } catch (ReceivedMessageParseException | IOException e) {
162             logger.debug("Could not perform feature detection for RX-V3900");
163         }
164     }
165
166     protected void sendCommand(String message) throws IOException {
167         comReference.get().send(XMLUtils.wrZone(zone, message));
168     }
169
170     protected void sendCommandWithoutZone(String message) throws IOException {
171         comReference.get().send(message);
172     }
173
174     /**
175      * Return the zone
176      */
177     public Zone getZone() {
178         return zone;
179     }
180
181     @Override
182     public void setPower(boolean on) throws IOException, ReceivedMessageParseException {
183         String cmd = power.apply(on ? ON : POWER_STANDBY);
184         sendCommand(cmd);
185         update();
186     }
187
188     @Override
189     public void setMute(boolean on) throws IOException, ReceivedMessageParseException {
190         String cmd = this.mute.apply(on ? ON : OFF);
191         sendCommand(cmd);
192         update();
193     }
194
195     /**
196      * Sets the absolute volume in decibel.
197      *
198      * @param volume Absolute value in decibel ([-80,+12]).
199      * @throws IOException
200      */
201     @Override
202     public void setVolumeDB(float volume) throws IOException, ReceivedMessageParseException {
203         if (volume < zoneConfig.getVolumeDbMin()) {
204             volume = zoneConfig.getVolumeDbMin();
205         }
206         if (volume > zoneConfig.getVolumeDbMax()) {
207             volume = zoneConfig.getVolumeDbMax();
208         }
209
210         // Yamaha accepts only integer values with .0 or .5 at the end only (-20.5dB, -20.0dB) - at least on RX-S601D.
211         // The order matters here. We want to cast to integer first and then scale by 10.
212         // Effectively we're only allowing dB values with .0 at the end.
213         int vol = (int) volume * 10;
214         sendCommand(this.volume.apply(vol));
215         update();
216     }
217
218     /**
219      * Sets the volume in percent
220      *
221      * @param volume
222      * @throws IOException
223      */
224     @Override
225     public void setVolume(float volume) throws IOException, ReceivedMessageParseException {
226         if (volume < 0) {
227             volume = 0;
228         }
229         if (volume > 100) {
230             volume = 100;
231         }
232         // Compute value in db
233         setVolumeDB(zoneConfig.getVolumeDb(volume));
234     }
235
236     /**
237      * Increase or decrease the volume by the given percentage.
238      *
239      * @param percent
240      * @throws IOException
241      */
242     @Override
243     public void setVolumeRelative(ZoneControlState state, float percent)
244             throws IOException, ReceivedMessageParseException {
245         setVolume(zoneConfig.getVolumePercentage(state.volumeDB) + percent);
246     }
247
248     @Override
249     public void setInput(String name) throws IOException, ReceivedMessageParseException {
250         name = inputConverterSupplier.get().toCommandName(name);
251         String cmd = inputSel.apply(name);
252         sendCommand(cmd);
253         update();
254     }
255
256     @Override
257     public void setSurroundProgram(String name) throws IOException, ReceivedMessageParseException {
258         String cmd = name.equalsIgnoreCase(SURROUND_PROGRAM_STRAIGHT) ? surroundSelStraight.apply()
259                 : surroundSelProgram.apply(name);
260
261         sendCommand(cmd);
262         update();
263     }
264
265     @Override
266     public void setDialogueLevel(int level) throws IOException, ReceivedMessageParseException {
267         if (!dialogueLevelSupported) {
268             return;
269         }
270         sendCommand(dialogueLevel.apply(level));
271         update();
272     }
273
274     @Override
275     public void setHDMI1Out(boolean on) throws IOException, ReceivedMessageParseException {
276         if (!hdmi1OutSupported) {
277             return;
278         }
279         sendCommandWithoutZone(hdmi1Out.apply(on ? ON : OFF));
280         update();
281     }
282
283     @Override
284     public void setHDMI2Out(boolean on) throws IOException, ReceivedMessageParseException {
285         if (!hdmi2OutSupported) {
286             return;
287         }
288         sendCommandWithoutZone(hdmi2Out.apply(on ? ON : OFF));
289         update();
290     }
291
292     @Override
293     public void setScene(String scene) throws IOException, ReceivedMessageParseException {
294         if (!sceneSelSupported) {
295             return;
296         }
297         sendCommand(sceneSel.apply(scene));
298         update();
299     }
300
301     @Override
302     public void update() throws IOException, ReceivedMessageParseException {
303         if (observer == null) {
304             return;
305         }
306
307         Node statusNode = getZoneResponse(comReference.get(), zone, ZONE_BASIC_STATUS_CMD, ZONE_BASIC_STATUS_PATH);
308
309         String value;
310
311         ZoneControlState state = new ZoneControlState();
312
313         value = getNodeContentOrEmpty(statusNode, power.getPath());
314         state.power = ON.equalsIgnoreCase(value);
315
316         value = getNodeContentOrEmpty(statusNode, mute.getPath());
317         state.mute = ON.equalsIgnoreCase(value);
318
319         // The value comes in dB x 10, on AVR it says -30.5dB, the values comes as -305
320         value = getNodeContentOrDefault(statusNode, volume.getPath(), String.valueOf(zoneConfig.getVolumeDbMin()));
321         state.volumeDB = Float.parseFloat(value) * .1f; // in dB
322
323         value = getNodeContentOrEmpty(statusNode, inputSel.getPath());
324         state.inputID = inputConverterSupplier.get().fromStateName(value);
325         if (state.inputID == null || state.inputID.isBlank()) {
326             throw new ReceivedMessageParseException("Expected inputID. Failed to read Input/Input_Sel");
327         }
328
329         value = getNodeContentOrEmpty(statusNode, hdmi1Out.getPath());
330         state.hdmi1Out = ON.equalsIgnoreCase(value);
331
332         value = getNodeContentOrEmpty(statusNode, hdmi1Out.getPath());
333         state.hdmi2Out = ON.equalsIgnoreCase(value);
334
335         // Some receivers may use Src_Name instead?
336         value = getNodeContentOrEmpty(statusNode, inputSelNamePath);
337         state.inputName = value;
338
339         value = getNodeContentOrEmpty(statusNode, surroundSelStraight.getPath());
340         boolean straightOn = ON.equalsIgnoreCase(value);
341
342         value = getNodeContentOrEmpty(statusNode, surroundSelProgram.getPath());
343         // Surround is either in straight mode or sound program
344         state.surroundProgram = straightOn ? SURROUND_PROGRAM_STRAIGHT : value;
345
346         value = getNodeContentOrDefault(statusNode, dialogueLevel.getPath(), "0");
347         state.dialogueLevel = Integer.parseInt(value);
348
349         logger.debug("Zone {} state - power: {}, mute: {}, volumeDB: {}, input: {}, surroundProgram: {}", getZone(),
350                 state.power, state.mute, state.volumeDB, state.inputID, state.surroundProgram);
351
352         observer.zoneStateChanged(state);
353     }
354 }