]> git.basschouten.com Git - openhab-addons.git/blob
639fbfc2b3c373aa530a2985c85639222c2173e9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.apache.commons.lang.StringUtils;
26 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
27 import org.openhab.binding.yamahareceiver.internal.config.YamahaZoneConfig;
28 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
29 import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
30 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
31 import org.openhab.binding.yamahareceiver.internal.protocol.ZoneControl;
32 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
33 import org.openhab.binding.yamahareceiver.internal.state.ZoneControlState;
34 import org.openhab.binding.yamahareceiver.internal.state.ZoneControlStateListener;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.w3c.dom.Node;
38
39 /**
40  * The zone protocol class is used to control one zone of a Yamaha receiver with HTTP/xml.
41  * No state will be saved in here, but in {@link ZoneControlState} instead.
42  *
43  * @author David Gräff - Refactored
44  * @author Eric Thill
45  * @author Ben Jones
46  * @author Tomasz Maruszak - Refactoring, input mapping fix, added Straight surround, volume DB fix and config
47  *         improvement.
48  */
49 public class ZoneControlXML implements ZoneControl {
50
51     protected Logger logger = LoggerFactory.getLogger(ZoneControlXML.class);
52
53     private static final String SURROUND_PROGRAM_STRAIGHT = "Straight";
54
55     private final ZoneControlStateListener observer;
56     private final Supplier<InputConverter> inputConverterSupplier;
57     private final WeakReference<AbstractConnection> comReference;
58     private final Zone zone;
59     private final YamahaZoneConfig zoneConfig;
60     private final DeviceDescriptorXML.ZoneDescriptor zoneDescriptor;
61
62     protected CommandTemplate power = new CommandTemplate("<Power_Control><Power>%s</Power></Power_Control>",
63             "Power_Control/Power");
64     protected CommandTemplate mute = new CommandTemplate("<Volume><Mute>%s</Mute></Volume>", "Volume/Mute");
65     protected CommandTemplate volume = new CommandTemplate(
66             "<Volume><Lvl><Val>%d</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume>", "Volume/Lvl/Val");
67     protected CommandTemplate inputSel = new CommandTemplate("<Input><Input_Sel>%s</Input_Sel></Input>",
68             "Input/Input_Sel");
69     protected String inputSelNamePath = "Input/Input_Sel_Item_Info/Title";
70     protected CommandTemplate surroundSelProgram = new CommandTemplate(
71             "<Surround><Program_Sel><Current><Sound_Program>%s</Sound_Program></Current></Program_Sel></Surround>",
72             "Surround/Program_Sel/Current/Sound_Program");
73     protected CommandTemplate surroundSelStraight = new CommandTemplate(
74             "<Surround><Program_Sel><Current><Straight>On</Straight></Current></Program_Sel></Surround>",
75             "Surround/Program_Sel/Current/Straight");
76     protected CommandTemplate sceneSel = new CommandTemplate("<Scene><Scene_Sel>%s</Scene_Sel></Scene>");
77     protected boolean sceneSelSupported = false;
78     protected CommandTemplate dialogueLevel = new CommandTemplate(
79             "<Sound_Video><Dialogue_Adjust><Dialogue_Lvl>%d</Dialogue_Lvl></Dialogue_Adjust></Sound_Video>",
80             "Sound_Video/Dialogue_Adjust/Dialogue_Lvl");
81     protected boolean dialogueLevelSupported = false;
82
83     public ZoneControlXML(AbstractConnection con, Zone zone, YamahaZoneConfig zoneSettings,
84             ZoneControlStateListener observer, DeviceInformationState deviceInformationState,
85             Supplier<InputConverter> inputConverterSupplier) {
86         this.comReference = new WeakReference<>(con);
87         this.zone = zone;
88         this.zoneConfig = zoneSettings;
89         this.zoneDescriptor = DeviceDescriptorXML.getAttached(deviceInformationState).zones.getOrDefault(zone, null);
90         this.observer = observer;
91         this.inputConverterSupplier = inputConverterSupplier;
92
93         this.applyModelVariations();
94     }
95
96     /**
97      * Apply command changes to ensure compatibility with all supported models
98      */
99     protected void applyModelVariations() {
100         if (zoneDescriptor == null) {
101             logger.trace("Zone {} - descriptor not available", getZone());
102             return;
103         }
104
105         logger.trace("Zone {} - compatibility detection", getZone());
106
107         // Note: Detection if scene is supported
108         sceneSelSupported = zoneDescriptor.hasCommandEnding("Scene,Scene_Sel", () -> logger
109                 .debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_SCENE));
110
111         // Note: Detection if dialogue level is supported
112         dialogueLevelSupported = zoneDescriptor.hasAnyCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lvl",
113                 "Sound_Video,Dialogue_Adjust,Dialogue_Lift");
114         if (zoneDescriptor.hasCommandEnding("Sound_Video,Dialogue_Adjust,Dialogue_Lift")) {
115             dialogueLevel = dialogueLevel.replace("Dialogue_Lvl", "Dialogue_Lift");
116             logger.debug("Zone {} - adjusting command to: {}", getZone(), dialogueLevel);
117         }
118         if (!dialogueLevelSupported) {
119             logger.debug("Zone {} - the {} channel is not supported on your model", getZone(), CHANNEL_DIALOGUE_LEVEL);
120         }
121
122         // Note: Detection for RX-V3900, which uses <Vol> instead of <Volume>
123         if (zoneDescriptor.hasCommandEnding("Vol,Lvl")) {
124             volume = volume.replace("Volume", "Vol");
125             logger.debug("Zone {} - adjusting command to: {}", getZone(), volume);
126         }
127         if (zoneDescriptor.hasCommandEnding("Vol,Mute")) {
128             mute = mute.replace("Volume", "Vol");
129             logger.debug("Zone {} - adjusting command to: {}", getZone(), mute);
130         }
131
132         try {
133             // Note: Detection for RX-V3900, which has a different XML node for surround program
134             Node basicStatusNode = getZoneResponse(comReference.get(), getZone(), ZONE_BASIC_STATUS_CMD,
135                     ZONE_BASIC_STATUS_PATH);
136             String surroundProgram = getNodeContentOrEmpty(basicStatusNode, "Surr/Pgm_Sel/Pgm");
137
138             if (StringUtils.isNotEmpty(surroundProgram)) {
139                 surroundSelProgram = new CommandTemplate(
140                         "<Surr><Pgm_Sel><Straight>Off</Straight><Pgm>%s</Pgm></Pgm_Sel></Surr>", "Surr/Pgm_Sel/Pgm");
141                 logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelProgram);
142
143                 surroundSelStraight = new CommandTemplate("<Surr><Pgm_Sel><Straight>On</Straight></Pgm_Sel></Surr>",
144                         "Surr/Pgm_Sel/Straight");
145                 logger.debug("Zone {} - adjusting command to: {}", getZone(), surroundSelStraight);
146             }
147
148         } catch (ReceivedMessageParseException | IOException e) {
149             logger.debug("Could not perform feature detection for RX-V3900");
150         }
151     }
152
153     protected void sendCommand(String message) throws IOException {
154         comReference.get().send(XMLUtils.wrZone(zone, message));
155     }
156
157     /**
158      * Return the zone
159      */
160     public Zone getZone() {
161         return zone;
162     }
163
164     @Override
165     public void setPower(boolean on) throws IOException, ReceivedMessageParseException {
166         String cmd = power.apply(on ? ON : POWER_STANDBY);
167         sendCommand(cmd);
168         update();
169     }
170
171     @Override
172     public void setMute(boolean on) throws IOException, ReceivedMessageParseException {
173         String cmd = this.mute.apply(on ? ON : OFF);
174         sendCommand(cmd);
175         update();
176     }
177
178     /**
179      * Sets the absolute volume in decibel.
180      *
181      * @param volume Absolute value in decibel ([-80,+12]).
182      * @throws IOException
183      */
184     @Override
185     public void setVolumeDB(float volume) throws IOException, ReceivedMessageParseException {
186         if (volume < zoneConfig.getVolumeDbMin()) {
187             volume = zoneConfig.getVolumeDbMin();
188         }
189         if (volume > zoneConfig.getVolumeDbMax()) {
190             volume = zoneConfig.getVolumeDbMax();
191         }
192
193         // Yamaha accepts only integer values with .0 or .5 at the end only (-20.5dB, -20.0dB) - at least on RX-S601D.
194         // The order matters here. We want to cast to integer first and then scale by 10.
195         // Effectively we're only allowing dB values with .0 at the end.
196         int vol = (int) volume * 10;
197         sendCommand(this.volume.apply(vol));
198         update();
199     }
200
201     /**
202      * Sets the volume in percent
203      *
204      * @param volume
205      * @throws IOException
206      */
207     @Override
208     public void setVolume(float volume) throws IOException, ReceivedMessageParseException {
209         if (volume < 0) {
210             volume = 0;
211         }
212         if (volume > 100) {
213             volume = 100;
214         }
215         // Compute value in db
216         setVolumeDB(zoneConfig.getVolumeDb(volume));
217     }
218
219     /**
220      * Increase or decrease the volume by the given percentage.
221      *
222      * @param percent
223      * @throws IOException
224      */
225     @Override
226     public void setVolumeRelative(ZoneControlState state, float percent)
227             throws IOException, ReceivedMessageParseException {
228         setVolume(zoneConfig.getVolumePercentage(state.volumeDB) + percent);
229     }
230
231     @Override
232     public void setInput(String name) throws IOException, ReceivedMessageParseException {
233         name = inputConverterSupplier.get().toCommandName(name);
234         String cmd = inputSel.apply(name);
235         sendCommand(cmd);
236         update();
237     }
238
239     @Override
240     public void setSurroundProgram(String name) throws IOException, ReceivedMessageParseException {
241         String cmd = name.equalsIgnoreCase(SURROUND_PROGRAM_STRAIGHT) ? surroundSelStraight.apply()
242                 : surroundSelProgram.apply(name);
243
244         sendCommand(cmd);
245         update();
246     }
247
248     @Override
249     public void setDialogueLevel(int level) throws IOException, ReceivedMessageParseException {
250         if (!dialogueLevelSupported) {
251             return;
252         }
253         sendCommand(dialogueLevel.apply(level));
254         update();
255     }
256
257     @Override
258     public void setScene(String scene) throws IOException, ReceivedMessageParseException {
259         if (!sceneSelSupported) {
260             return;
261         }
262         sendCommand(sceneSel.apply(scene));
263         update();
264     }
265
266     @Override
267     public void update() throws IOException, ReceivedMessageParseException {
268         if (observer == null) {
269             return;
270         }
271
272         Node statusNode = getZoneResponse(comReference.get(), zone, ZONE_BASIC_STATUS_CMD, ZONE_BASIC_STATUS_PATH);
273
274         String value;
275
276         ZoneControlState state = new ZoneControlState();
277
278         value = getNodeContentOrEmpty(statusNode, power.getPath());
279         state.power = ON.equalsIgnoreCase(value);
280
281         value = getNodeContentOrEmpty(statusNode, mute.getPath());
282         state.mute = ON.equalsIgnoreCase(value);
283
284         // The value comes in dB x 10, on AVR it says -30.5dB, the values comes as -305
285         value = getNodeContentOrDefault(statusNode, volume.getPath(), String.valueOf(zoneConfig.getVolumeDbMin()));
286         state.volumeDB = Float.parseFloat(value) * .1f; // in dB
287
288         value = getNodeContentOrEmpty(statusNode, inputSel.getPath());
289         state.inputID = inputConverterSupplier.get().fromStateName(value);
290         if (StringUtils.isBlank(state.inputID)) {
291             throw new ReceivedMessageParseException("Expected inputID. Failed to read Input/Input_Sel");
292         }
293
294         // Some receivers may use Src_Name instead?
295         value = getNodeContentOrEmpty(statusNode, inputSelNamePath);
296         state.inputName = value;
297
298         value = getNodeContentOrEmpty(statusNode, surroundSelStraight.getPath());
299         boolean straightOn = ON.equalsIgnoreCase(value);
300
301         value = getNodeContentOrEmpty(statusNode, surroundSelProgram.getPath());
302         // Surround is either in straight mode or sound program
303         state.surroundProgram = straightOn ? SURROUND_PROGRAM_STRAIGHT : value;
304
305         value = getNodeContentOrDefault(statusNode, dialogueLevel.getPath(), "0");
306         state.dialogueLevel = Integer.parseInt(value);
307
308         logger.debug("Zone {} state - power: {}, mute: {}, volumeDB: {}, input: {}, surroundProgram: {}", getZone(),
309                 state.power, state.mute, state.volumeDB, state.inputID, state.surroundProgram);
310
311         observer.zoneStateChanged(state);
312     }
313 }