]> git.basschouten.com Git - openhab-addons.git/blob
589c87ee2a98f034da335d3799496e2a528da043
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.denonmarantz.internal.connector.telnet;
14
15 import java.math.BigDecimal;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.List;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.regex.Pattern;
22
23 import org.apache.commons.lang3.StringUtils;
24 import org.openhab.binding.denonmarantz.internal.DenonMarantzState;
25 import org.openhab.binding.denonmarantz.internal.config.DenonMarantzConfiguration;
26 import org.openhab.binding.denonmarantz.internal.connector.DenonMarantzConnector;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * This class makes the connection to the receiver and manages it.
32  * It is also responsible for sending commands to the receiver.
33  *
34  * @author Jeroen Idserda - Initial Contribution (1.x Binding)
35  * @author Jan-Willem Veldhuis - Refactored for 2.x
36  */
37 public class DenonMarantzTelnetConnector extends DenonMarantzConnector implements DenonMarantzTelnetListener {
38
39     private final Logger logger = LoggerFactory.getLogger(DenonMarantzTelnetConnector.class);
40
41     // All regular commands. Example: PW, SICD, SITV, Z2MU
42     private static final Pattern COMMAND_PATTERN = Pattern.compile("^([A-Z0-9]{2})(.+)$");
43
44     // Example: E2Counting Crows
45     private static final Pattern DISPLAY_PATTERN = Pattern.compile("^(E|A)([0-9]{1})(.+)$");
46
47     private static final BigDecimal NINETYNINE = new BigDecimal("99");
48
49     private DenonMarantzTelnetClientThread telnetClientThread;
50
51     private boolean displayNowplaying = false;
52
53     protected boolean disposing = false;
54
55     private Future<?> telnetStateRequest;
56
57     private String thingUID;
58
59     public DenonMarantzTelnetConnector(DenonMarantzConfiguration config, DenonMarantzState state,
60             ScheduledExecutorService scheduler, String thingUID) {
61         this.config = config;
62         this.scheduler = scheduler;
63         this.state = state;
64         this.thingUID = thingUID;
65     }
66
67     /**
68      * Set up the connection to the receiver. Either using Telnet or by polling the HTTP API.
69      */
70     @Override
71     public void connect() {
72         telnetClientThread = new DenonMarantzTelnetClientThread(config, this);
73         telnetClientThread.setName("OH-binding-" + thingUID);
74         telnetClientThread.start();
75     }
76
77     @Override
78     public void telnetClientConnected(boolean connected) {
79         if (!connected) {
80             if (config.isTelnet() && !disposing) {
81                 logger.debug("Telnet client disconnected.");
82                 state.connectionError(
83                         "Error connecting to the telnet port. Consider disabling telnet in this Thing's configuration to use HTTP polling instead.");
84             }
85         } else {
86             refreshState();
87         }
88     }
89
90     /**
91      * Shutdown the telnet client (if initialized) and the http client
92      */
93     @Override
94     public void dispose() {
95         logger.debug("disposing connector");
96         disposing = true;
97
98         if (telnetStateRequest != null) {
99             telnetStateRequest.cancel(true);
100             telnetStateRequest = null;
101         }
102
103         if (telnetClientThread != null) {
104             telnetClientThread.interrupt();
105             // Invoke a shutdown after interrupting the thread to close the socket immediately,
106             // otherwise the client keeps running until a line was received from the telnet connection
107             telnetClientThread.shutdown();
108             telnetClientThread = null;
109         }
110     }
111
112     private void refreshState() {
113         // Sends a series of state query commands over the telnet connection
114         telnetStateRequest = scheduler.submit(() -> {
115             List<String> cmds = new ArrayList<>(Arrays.asList("PW?", "MS?", "MV?", "ZM?", "MU?", "SI?"));
116             if (config.getZoneCount() > 1) {
117                 cmds.add("Z2?");
118                 cmds.add("Z2MU?");
119             }
120             if (config.getZoneCount() > 2) {
121                 cmds.add("Z3?");
122                 cmds.add("Z3MU?");
123             }
124             for (String cmd : cmds) {
125                 internalSendCommand(cmd);
126                 try {
127                     Thread.sleep(300);
128                 } catch (InterruptedException e) {
129                     logger.trace("requestStateOverTelnet() - Interrupted while requesting state.");
130                     Thread.currentThread().interrupt();
131                 }
132             }
133         });
134     }
135
136     /**
137      * This method tries to parse information received over the telnet connection.
138      * It can be quite unreliable. Some chars go missing or turn into other chars. That's
139      * why each command is validated using a regex.
140      *
141      * @param line The received command (one line)
142      */
143     @Override
144     public void receivedLine(String line) {
145         if (COMMAND_PATTERN.matcher(line).matches()) {
146             /*
147              * This splits the commandString into the command and the parameter. SICD
148              * for example has SI as the command and CD as the parameter.
149              */
150             String command = line.substring(0, 2);
151             String value = line.substring(2, line.length()).trim();
152
153             logger.debug("Received Command: {}, value: {}", command, value);
154
155             // use received command (event) from telnet to update state
156             switch (command) {
157                 case "SI": // Switch Input
158                     state.setInput(value);
159                     break;
160                 case "PW": // Power
161                     if (value.equals("ON") || value.equals("STANDBY")) {
162                         state.setPower(value.equals("ON"));
163                     }
164                     break;
165                 case "MS": // Main zone surround program
166                     state.setSurroundProgram(value);
167                     break;
168                 case "MV": // Main zone volume
169                     if (StringUtils.isNumeric(value)) {
170                         state.setMainVolume(fromDenonValue(value));
171                     }
172                     break;
173                 case "MU": // Main zone mute
174                     if (value.equals("ON") || value.equals("OFF")) {
175                         state.setMute(value.equals("ON"));
176                     }
177                     break;
178                 case "NS": // Now playing information
179                     processTitleCommand(value);
180                     break;
181                 case "Z2": // Zone 2
182                     if (value.equals("ON") || value.equals("OFF")) {
183                         state.setZone2Power(value.equals("ON"));
184                     } else if (value.equals("MUON") || value.equals("MUOFF")) {
185                         state.setZone2Mute(value.equals("MUON"));
186                     } else if (StringUtils.isNumeric(value)) {
187                         state.setZone2Volume(fromDenonValue(value));
188                     } else {
189                         state.setZone2Input(value);
190                     }
191                     break;
192                 case "Z3": // Zone 3
193                     if (value.equals("ON") || value.equals("OFF")) {
194                         state.setZone3Power(value.equals("ON"));
195                     } else if (value.equals("MUON") || value.equals("MUOFF")) {
196                         state.setZone3Mute(value.equals("MUON"));
197                     } else if (StringUtils.isNumeric(value)) {
198                         state.setZone3Volume(fromDenonValue(value));
199                     } else {
200                         state.setZone3Input(value);
201                     }
202                     break;
203                 case "Z4": // Zone 4
204                     if (value.equals("ON") || value.equals("OFF")) {
205                         state.setZone4Power(value.equals("ON"));
206                     } else if (value.equals("MUON") || value.equals("MUOFF")) {
207                         state.setZone4Mute(value.equals("MUON"));
208                     } else if (StringUtils.isNumeric(value)) {
209                         state.setZone4Volume(fromDenonValue(value));
210                     } else {
211                         state.setZone4Input(value);
212                     }
213                     break;
214                 case "ZM": // Main zone
215                     if (value.equals("ON") || value.equals("OFF")) {
216                         state.setMainZonePower(value.equals("ON"));
217                     }
218                     break;
219             }
220         } else {
221             logger.trace("Ignoring received line: '{}'", line);
222         }
223     }
224
225     private BigDecimal fromDenonValue(String string) {
226         /*
227          * 455 = 45,5
228          * 45 = 45
229          * 045 = 4,5
230          * 04 = 4
231          */
232         BigDecimal value = new BigDecimal(string);
233         if (value.compareTo(NINETYNINE) == 1 || (string.startsWith("0") && string.length() > 2)) {
234             value = value.divide(BigDecimal.TEN);
235         }
236         return value;
237     }
238
239     private void processTitleCommand(String value) {
240         if (DISPLAY_PATTERN.matcher(value).matches()) {
241             Integer commandNo = Integer.valueOf(value.substring(1, 2));
242             String titleValue = value.substring(2);
243
244             if (commandNo == 0) {
245                 displayNowplaying = titleValue.contains("Now Playing");
246             }
247
248             String nowPlaying = displayNowplaying ? cleanupDisplayInfo(titleValue) : "";
249
250             switch (commandNo) {
251                 case 1:
252                     state.setNowPlayingTrack(nowPlaying);
253                     break;
254                 case 2:
255                     state.setNowPlayingArtist(nowPlaying);
256                     break;
257                 case 4:
258                     state.setNowPlayingAlbum(nowPlaying);
259                     break;
260             }
261         }
262     }
263
264     @Override
265     protected void internalSendCommand(String command) {
266         logger.debug("Sending command '{}'", command);
267         if (command == null || command.isBlank()) {
268             logger.warn("Trying to send empty command");
269             return;
270         }
271         telnetClientThread.sendCommand(command);
272     }
273
274     /**
275      * Display info could contain some garbled text, attempt to clean it up.
276      */
277     private String cleanupDisplayInfo(String titleValue) {
278         byte firstByteRemoved[] = Arrays.copyOfRange(titleValue.getBytes(), 1, titleValue.getBytes().length);
279         return new String(firstByteRemoved).replaceAll("[\u0000-\u001f]", "");
280     }
281 }