]> git.basschouten.com Git - openhab-addons.git/blob
b5ae903f13639e5380a78a0fa519d2de3a45f95d
[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.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.lang.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 DenonMarantzTelnetClient telnetClient;
50
51     private boolean displayNowplaying = false;
52
53     protected boolean disposing = false;
54
55     private Future<?> telnetStateRequest;
56
57     private Future<?> telnetRunnable;
58
59     public DenonMarantzTelnetConnector(DenonMarantzConfiguration config, DenonMarantzState state,
60             ScheduledExecutorService scheduler) {
61         this.config = config;
62         this.scheduler = scheduler;
63         this.state = state;
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         telnetClient = new DenonMarantzTelnetClient(config, this);
72         telnetRunnable = scheduler.submit(telnetClient);
73     }
74
75     @Override
76     public void telnetClientConnected(boolean connected) {
77         if (!connected) {
78             if (config.isTelnet() && !disposing) {
79                 logger.debug("Telnet client disconnected.");
80                 state.connectionError(
81                         "Error connecting to the telnet port. Consider disabling telnet in this Thing's configuration to use HTTP polling instead.");
82             }
83         } else {
84             refreshState();
85         }
86     }
87
88     /**
89      * Shutdown the telnet client (if initialized) and the http client
90      */
91     @Override
92     public void dispose() {
93         logger.debug("disposing connector");
94         disposing = true;
95
96         if (telnetStateRequest != null) {
97             telnetStateRequest.cancel(true);
98             telnetStateRequest = null;
99         }
100
101         if (telnetClient != null && !telnetRunnable.isDone()) {
102             telnetRunnable.cancel(true);
103             telnetClient.shutdown();
104         }
105     }
106
107     private void refreshState() {
108         // Sends a series of state query commands over the telnet connection
109         telnetStateRequest = scheduler.submit(() -> {
110             List<String> cmds = new ArrayList<>(Arrays.asList("PW?", "MS?", "MV?", "ZM?", "MU?", "SI?"));
111             if (config.getZoneCount() > 1) {
112                 cmds.add("Z2?");
113                 cmds.add("Z2MU?");
114             }
115             if (config.getZoneCount() > 2) {
116                 cmds.add("Z3?");
117                 cmds.add("Z3MU?");
118             }
119             for (String cmd : cmds) {
120                 internalSendCommand(cmd);
121                 try {
122                     Thread.sleep(300);
123                 } catch (InterruptedException e) {
124                     logger.trace("requestStateOverTelnet() - Interrupted while requesting state.");
125                 }
126             }
127         });
128     }
129
130     /**
131      * This method tries to parse information received over the telnet connection.
132      * It can be quite unreliable. Some chars go missing or turn into other chars. That's
133      * why each command is validated using a regex.
134      *
135      * @param line The received command (one line)
136      */
137     @Override
138     public void receivedLine(String line) {
139         if (COMMAND_PATTERN.matcher(line).matches()) {
140             /*
141              * This splits the commandString into the command and the parameter. SICD
142              * for example has SI as the command and CD as the parameter.
143              */
144             String command = line.substring(0, 2);
145             String value = line.substring(2, line.length()).trim();
146
147             logger.debug("Received Command: {}, value: {}", command, value);
148
149             // use received command (event) from telnet to update state
150             switch (command) {
151                 case "SI": // Switch Input
152                     state.setInput(value);
153                     break;
154                 case "PW": // Power
155                     if (value.equals("ON") || value.equals("STANDBY")) {
156                         state.setPower(value.equals("ON"));
157                     }
158                     break;
159                 case "MS": // Main zone surround program
160                     state.setSurroundProgram(value);
161                     break;
162                 case "MV": // Main zone volume
163                     if (StringUtils.isNumeric(value)) {
164                         state.setMainVolume(fromDenonValue(value));
165                     }
166                     break;
167                 case "MU": // Main zone mute
168                     if (value.equals("ON") || value.equals("OFF")) {
169                         state.setMute(value.equals("ON"));
170                     }
171                     break;
172                 case "NS": // Now playing information
173                     processTitleCommand(value);
174                     break;
175                 case "Z2": // Zone 2
176                     if (value.equals("ON") || value.equals("OFF")) {
177                         state.setZone2Power(value.equals("ON"));
178                     } else if (value.equals("MUON") || value.equals("MUOFF")) {
179                         state.setZone2Mute(value.equals("MUON"));
180                     } else if (StringUtils.isNumeric(value)) {
181                         state.setZone2Volume(fromDenonValue(value));
182                     } else {
183                         state.setZone2Input(value);
184                     }
185                     break;
186                 case "Z3": // Zone 3
187                     if (value.equals("ON") || value.equals("OFF")) {
188                         state.setZone3Power(value.equals("ON"));
189                     } else if (value.equals("MUON") || value.equals("MUOFF")) {
190                         state.setZone3Mute(value.equals("MUON"));
191                     } else if (StringUtils.isNumeric(value)) {
192                         state.setZone3Volume(fromDenonValue(value));
193                     } else {
194                         state.setZone3Input(value);
195                     }
196                     break;
197                 case "Z4": // Zone 4
198                     if (value.equals("ON") || value.equals("OFF")) {
199                         state.setZone4Power(value.equals("ON"));
200                     } else if (value.equals("MUON") || value.equals("MUOFF")) {
201                         state.setZone4Mute(value.equals("MUON"));
202                     } else if (StringUtils.isNumeric(value)) {
203                         state.setZone4Volume(fromDenonValue(value));
204                     } else {
205                         state.setZone4Input(value);
206                     }
207                     break;
208                 case "ZM": // Main zone
209                     if (value.equals("ON") || value.equals("OFF")) {
210                         state.setMainZonePower(value.equals("ON"));
211                     }
212                     break;
213             }
214         } else {
215             logger.trace("Ignoring received line: '{}'", line);
216         }
217     }
218
219     private BigDecimal fromDenonValue(String string) {
220         /*
221          * 455 = 45,5
222          * 45 = 45
223          * 045 = 4,5
224          * 04 = 4
225          */
226         BigDecimal value = new BigDecimal(string);
227         if (value.compareTo(NINETYNINE) == 1 || (string.startsWith("0") && string.length() > 2)) {
228             value = value.divide(BigDecimal.TEN);
229         }
230         return value;
231     }
232
233     private void processTitleCommand(String value) {
234         if (DISPLAY_PATTERN.matcher(value).matches()) {
235             Integer commandNo = Integer.valueOf(value.substring(1, 2));
236             String titleValue = value.substring(2);
237
238             if (commandNo == 0) {
239                 displayNowplaying = titleValue.contains("Now Playing");
240             }
241
242             String nowPlaying = displayNowplaying ? cleanupDisplayInfo(titleValue) : "";
243
244             switch (commandNo) {
245                 case 1:
246                     state.setNowPlayingTrack(nowPlaying);
247                     break;
248                 case 2:
249                     state.setNowPlayingArtist(nowPlaying);
250                     break;
251                 case 4:
252                     state.setNowPlayingAlbum(nowPlaying);
253                     break;
254             }
255         }
256     }
257
258     @Override
259     protected void internalSendCommand(String command) {
260         logger.debug("Sending command '{}'", command);
261         if (StringUtils.isBlank(command)) {
262             logger.warn("Trying to send empty command");
263             return;
264         }
265         telnetClient.sendCommand(command);
266     }
267
268     /**
269      * Display info could contain some garbled text, attempt to clean it up.
270      */
271     private String cleanupDisplayInfo(String titleValue) {
272         byte firstByteRemoved[] = Arrays.copyOfRange(titleValue.getBytes(), 1, titleValue.getBytes().length);
273         return new String(firstByteRemoved).replaceAll("[\u0000-\u001f]", "");
274     }
275 }