2 * Copyright (c) 2010-2022 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.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;
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 DenonMarantzTelnetClientThread telnetClientThread;
51 private boolean displayNowplaying = false;
53 protected boolean disposing = false;
55 private Future<?> telnetStateRequest;
57 private String thingUID;
59 public DenonMarantzTelnetConnector(DenonMarantzConfiguration config, DenonMarantzState state,
60 ScheduledExecutorService scheduler, String thingUID) {
62 this.scheduler = scheduler;
64 this.thingUID = thingUID;
68 * Set up the connection to the receiver. Either using Telnet or by polling the HTTP API.
71 public void connect() {
72 telnetClientThread = new DenonMarantzTelnetClientThread(config, this);
73 telnetClientThread.setName("OH-binding-" + thingUID);
74 telnetClientThread.start();
78 public void telnetClientConnected(boolean 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.");
91 * Shutdown the telnet client (if initialized) and the http client
94 public void dispose() {
95 logger.debug("disposing connector");
98 if (telnetStateRequest != null) {
99 telnetStateRequest.cancel(true);
100 telnetStateRequest = null;
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;
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) {
120 if (config.getZoneCount() > 2) {
124 for (String cmd : cmds) {
125 internalSendCommand(cmd);
128 } catch (InterruptedException e) {
129 logger.trace("requestStateOverTelnet() - Interrupted while requesting state.");
130 Thread.currentThread().interrupt();
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.
141 * @param line The received command (one line)
144 public void receivedLine(String line) {
145 if (COMMAND_PATTERN.matcher(line).matches()) {
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.
150 String command = line.substring(0, 2);
151 String value = line.substring(2, line.length()).trim();
153 logger.debug("Received Command: {}, value: {}", command, value);
155 // use received command (event) from telnet to update state
157 case "SI": // Switch Input
158 state.setInput(value);
161 if (value.equals("ON") || value.equals("STANDBY")) {
162 state.setPower(value.equals("ON"));
165 case "MS": // Main zone surround program
166 state.setSurroundProgram(value);
168 case "MV": // Main zone volume
169 if (StringUtils.isNumeric(value)) {
170 state.setMainVolume(fromDenonValue(value));
173 case "MU": // Main zone mute
174 if (value.equals("ON") || value.equals("OFF")) {
175 state.setMute(value.equals("ON"));
178 case "NS": // Now playing information
179 processTitleCommand(value);
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));
189 state.setZone2Input(value);
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));
200 state.setZone3Input(value);
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));
211 state.setZone4Input(value);
214 case "ZM": // Main zone
215 if (value.equals("ON") || value.equals("OFF")) {
216 state.setMainZonePower(value.equals("ON"));
221 logger.trace("Ignoring received line: '{}'", line);
225 private BigDecimal fromDenonValue(String string) {
232 BigDecimal value = new BigDecimal(string);
233 if (value.compareTo(NINETYNINE) == 1 || (string.startsWith("0") && string.length() > 2)) {
234 value = value.divide(BigDecimal.TEN);
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);
244 if (commandNo == 0) {
245 displayNowplaying = titleValue.contains("Now Playing");
248 String nowPlaying = displayNowplaying ? cleanupDisplayInfo(titleValue) : "";
252 state.setNowPlayingTrack(nowPlaying);
255 state.setNowPlayingArtist(nowPlaying);
258 state.setNowPlayingAlbum(nowPlaying);
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");
271 telnetClientThread.sendCommand(command);
275 * Display info could contain some garbled text, attempt to clean it up.
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]", "");