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