2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.denonmarantz.internal.connector.telnet;
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;
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;
31 * This class makes the connection to the receiver and manages it.
32 * It is also responsible for sending commands to the receiver.
34 * @author Jeroen Idserda - Initial Contribution (1.x Binding)
35 * @author Jan-Willem Veldhuis - Refactored for 2.x
37 public class DenonMarantzTelnetConnector extends DenonMarantzConnector implements DenonMarantzTelnetListener {
39 private final Logger logger = LoggerFactory.getLogger(DenonMarantzTelnetConnector.class);
41 // All regular commands. Example: PW, SICD, SITV, Z2MU
42 private static final Pattern COMMAND_PATTERN = Pattern.compile("^([A-Z0-9]{2})(.+)$");
44 // Example: E2Counting Crows
45 private static final Pattern DISPLAY_PATTERN = Pattern.compile("^(E|A)([0-9]{1})(.+)$");
47 private static final BigDecimal NINETYNINE = new BigDecimal("99");
49 private DenonMarantzTelnetClient telnetClient;
51 private boolean displayNowplaying = false;
53 protected boolean disposing = false;
55 private Future<?> telnetStateRequest;
57 private Future<?> telnetRunnable;
59 public DenonMarantzTelnetConnector(DenonMarantzConfiguration config, DenonMarantzState state,
60 ScheduledExecutorService scheduler) {
62 this.scheduler = scheduler;
67 * Set up the connection to the receiver. Either using Telnet or by polling the HTTP API.
70 public void connect() {
71 telnetClient = new DenonMarantzTelnetClient(config, this);
72 telnetRunnable = scheduler.submit(telnetClient);
76 public void telnetClientConnected(boolean 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.");
89 * Shutdown the telnet client (if initialized) and the http client
92 public void dispose() {
93 logger.debug("disposing connector");
96 if (telnetStateRequest != null) {
97 telnetStateRequest.cancel(true);
98 telnetStateRequest = null;
101 if (telnetClient != null && !telnetRunnable.isDone()) {
102 telnetRunnable.cancel(true);
103 telnetClient.shutdown();
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) {
115 if (config.getZoneCount() > 2) {
119 for (String cmd : cmds) {
120 internalSendCommand(cmd);
123 } catch (InterruptedException e) {
124 logger.trace("requestStateOverTelnet() - Interrupted while requesting state.");
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.
135 * @param line The received command (one line)
138 public void receivedLine(String line) {
139 if (COMMAND_PATTERN.matcher(line).matches()) {
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.
144 String command = line.substring(0, 2);
145 String value = line.substring(2, line.length()).trim();
147 logger.debug("Received Command: {}, value: {}", command, value);
149 // use received command (event) from telnet to update state
151 case "SI": // Switch Input
152 state.setInput(value);
155 if (value.equals("ON") || value.equals("STANDBY")) {
156 state.setPower(value.equals("ON"));
159 case "MS": // Main zone surround program
160 state.setSurroundProgram(value);
162 case "MV": // Main zone volume
163 if (StringUtils.isNumeric(value)) {
164 state.setMainVolume(fromDenonValue(value));
167 case "MU": // Main zone mute
168 if (value.equals("ON") || value.equals("OFF")) {
169 state.setMute(value.equals("ON"));
172 case "NS": // Now playing information
173 processTitleCommand(value);
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));
183 state.setZone2Input(value);
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));
194 state.setZone3Input(value);
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));
205 state.setZone4Input(value);
208 case "ZM": // Main zone
209 if (value.equals("ON") || value.equals("OFF")) {
210 state.setMainZonePower(value.equals("ON"));
215 logger.trace("Ignoring received line: '{}'", line);
219 private BigDecimal fromDenonValue(String string) {
226 BigDecimal value = new BigDecimal(string);
227 if (value.compareTo(NINETYNINE) == 1 || (string.startsWith("0") && string.length() > 2)) {
228 value = value.divide(BigDecimal.TEN);
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);
238 if (commandNo == 0) {
239 displayNowplaying = titleValue.contains("Now Playing");
242 String nowPlaying = displayNowplaying ? cleanupDisplayInfo(titleValue) : "";
246 state.setNowPlayingTrack(nowPlaying);
249 state.setNowPlayingArtist(nowPlaying);
252 state.setNowPlayingAlbum(nowPlaying);
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");
265 telnetClient.sendCommand(command);
269 * Display info could contain some garbled text, attempt to clean it up.
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]", "");