2 * Copyright (c) 2010-2023 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.vdr.internal.svdrp;
15 import java.io.BufferedReader;
16 import java.io.BufferedWriter;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.io.OutputStreamWriter;
20 import java.net.InetSocketAddress;
21 import java.net.Socket;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
29 * The {@link SVDRPClientImpl} encapsulates all calls to the SVDRP interface of a VDR
31 * @author Matthias Klocke - Initial contribution
34 public class SVDRPClientImpl implements SVDRPClient {
37 private int port = 6419;
38 private String charset = "UTF-8";
39 private String version = "";
41 private static final String WELCOME_MESSAGE = "([0-9]{3})([ -])(.*)";
42 private static final Pattern PATTERN_WELCOME = Pattern.compile(WELCOME_MESSAGE);
44 private static final int TIMEOUT_MS = 3000;
46 private @Nullable Socket socket = null;
47 private @Nullable BufferedWriter out = null;
48 private @Nullable BufferedReader in = null;
50 public SVDRPClientImpl(String host, int port) {
56 public SVDRPClientImpl(String host, int port, String charset) {
60 this.charset = charset;
65 * Open VDR Socket Connection
67 * @throws SVDRPConnectionException if an IO Error occurs
70 public void openConnection() throws SVDRPConnectionException, SVDRPParseResponseException {
71 Socket localSocket = socket;
72 BufferedWriter localOut = out;
73 BufferedReader localIn = in;
75 if (localSocket == null || localSocket.isClosed()) {
76 localSocket = new Socket();
80 InetSocketAddress isa = new InetSocketAddress(host, port);
81 localSocket.connect(isa, TIMEOUT_MS);
82 localSocket.setSoTimeout(TIMEOUT_MS);
84 localOut = new BufferedWriter(new OutputStreamWriter(localSocket.getOutputStream(), charset), 8192);
86 localIn = new BufferedReader(new InputStreamReader(localSocket.getInputStream(), charset), 8192);
89 // read welcome message and init version & charset
90 SVDRPResponse res = null;
94 if (res.getCode() == 220) {
95 SVDRPWelcome welcome = SVDRPWelcome.parse(res.getMessage());
96 this.charset = welcome.getCharset();
97 this.version = welcome.getVersion();
99 throw new SVDRPParseResponseException(res);
101 } catch (IOException e) {
102 // cleanup after timeout
104 if (localOut != null) {
107 if (localIn != null) {
111 } catch (IOException ex) {
113 throw new SVDRPConnectionException(e.getMessage(), e);
118 * Close VDR Socket Connection
120 * @throws SVDRPConnectionException if an IO Error occurs
123 public void closeConnection() throws SVDRPConnectionException, SVDRPParseResponseException {
124 Socket localSocket = socket;
125 BufferedWriter localOut = out;
126 BufferedReader localIn = in;
128 * socket on vdr stays in FIN_WAIT2 without closing connection
131 if (localOut != null) {
132 localOut.write("QUIT");
137 if (localIn != null) {
140 if (localSocket != null) {
143 } catch (IOException e) {
144 throw new SVDRPConnectionException(e.getMessage(), e);
152 * @param command SVDRP command to execute
153 * @return response of SVDRPCall
154 * @throws SVDRPException exception from SVDRP call
156 private SVDRPResponse execute(@Nullable String command)
157 throws SVDRPConnectionException, SVDRPParseResponseException {
158 BufferedWriter localOut = out;
159 BufferedReader localIn = in;
161 StringBuilder message = new StringBuilder();
162 Matcher matcher = null;
166 if (command != null) {
167 if (localOut == null) {
168 throw new SVDRPConnectionException("OutputStream is null!");
170 localOut.write(command);
176 if (localIn != null) {
180 while (cont && (line = localIn.readLine()) != null) {
181 matcher = PATTERN_WELCOME.matcher(line);
182 if (matcher.matches() && matcher.groupCount() > 2) {
184 code = Integer.parseInt(matcher.group(1));
186 if (" ".equals(matcher.group(2))) {
189 message.append(matcher.group(3));
191 message.append(System.lineSeparator());
197 return new SVDRPResponse(code, message.toString());
199 throw new SVDRPConnectionException("SVDRP Input Stream is Null");
201 } catch (IOException ioe) {
202 throw new SVDRPConnectionException(ioe.getMessage(), ioe);
203 } catch (NumberFormatException ne) {
204 throw new SVDRPParseResponseException(ne.getMessage(), ne);
209 * Retrieve Disk Status from SVDRP Client
211 * @return SVDRP Disk Status
212 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
213 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
216 public SVDRPDiskStatus getDiskStatus() throws SVDRPConnectionException, SVDRPParseResponseException {
217 SVDRPResponse res = null;
219 res = execute("STAT disk");
221 if (res.getCode() == 250) {
222 return SVDRPDiskStatus.parse(res.getMessage());
224 throw new SVDRPParseResponseException(res);
229 * Retrieve EPG Event from SVDRPClient
231 * @param type Type of EPG Event (now, next)
232 * @return SVDRP EPG Event
233 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
234 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
237 public SVDRPEpgEvent getEpgEvent(SVDRPEpgEvent.TYPE type)
238 throws SVDRPConnectionException, SVDRPParseResponseException {
239 SVDRPResponse res = null;
240 SVDRPChannel channel = this.getCurrentSVDRPChannel();
243 res = execute(String.format("LSTE %s %s", channel.getNumber(), "now"));
246 res = execute(String.format("LSTE %s %s", channel.getNumber(), "next"));
250 if (res != null && res.getCode() == 215) {
251 return SVDRPEpgEvent.parse(res.getMessage());
252 } else if (res != null) {
253 throw new SVDRPParseResponseException(res);
255 throw new SVDRPConnectionException("SVDRPResponse is Null");
260 * Retrieve current volume from SVDRP Client
262 * @return SVDRP Volume Object
263 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
264 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
267 public SVDRPVolume getSVDRPVolume() throws SVDRPConnectionException, SVDRPParseResponseException {
268 SVDRPResponse res = null;
270 res = execute("VOLU");
272 if (res.getCode() == 250) {
273 return SVDRPVolume.parse(res.getMessage());
275 throw new SVDRPParseResponseException(res);
280 * Set volume on SVDRP Client
282 * @param newVolume Volume in Percent
283 * @return SVDRP Volume Object
284 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
285 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
288 public SVDRPVolume setSVDRPVolume(int newVolume) throws SVDRPConnectionException, SVDRPParseResponseException {
289 SVDRPResponse res = null;
291 double newVolumeDouble = newVolume * 255 / 100;
292 res = execute(String.format("VOLU %s", String.valueOf(Math.round(newVolumeDouble))));
294 if (res.getCode() == 250) {
295 return SVDRPVolume.parse(res.getMessage());
297 throw new SVDRPParseResponseException(res);
302 * Send Key command to SVDRP Client
304 * @param key Key Command to send
305 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
306 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
309 public void sendSVDRPKey(String key) throws SVDRPConnectionException, SVDRPParseResponseException {
310 SVDRPResponse res = null;
312 res = execute(String.format("HITK %s", key));
314 if (res.getCode() != 250) {
315 throw new SVDRPParseResponseException(res);
320 * Send Message to SVDRP Client
322 * @param message Message to send
323 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
324 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
327 public void sendSVDRPMessage(String message) throws SVDRPConnectionException, SVDRPParseResponseException {
328 SVDRPResponse res = null;
330 res = execute(String.format("MESG %s", message));
332 if (res.getCode() != 250) {
333 throw new SVDRPParseResponseException(res);
338 * Retrieve current Channel from SVDRP Client
340 * @return SVDRPChannel object
341 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
342 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
345 public SVDRPChannel getCurrentSVDRPChannel() throws SVDRPConnectionException, SVDRPParseResponseException {
346 SVDRPResponse res = null;
348 res = execute("CHAN");
350 if (res.getCode() == 250) {
351 return SVDRPChannel.parse(res.getMessage());
353 throw new SVDRPParseResponseException(res);
358 * Change current Channel on SVDRP Client
360 * @param number Channel to be set
361 * @return SVDRPChannel object
362 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
363 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
366 public SVDRPChannel setSVDRPChannel(int number) throws SVDRPConnectionException, SVDRPParseResponseException {
367 SVDRPResponse res = null;
369 res = execute(String.format("CHAN %s", number));
371 if (res.getCode() == 250) {
372 return SVDRPChannel.parse(res.getMessage());
374 throw new SVDRPParseResponseException(res);
379 * Retrieve from SVDRP Client if a recording is currently active
381 * @return is currently a recording active
382 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
383 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
386 public boolean isRecordingActive() throws SVDRPConnectionException, SVDRPParseResponseException {
387 SVDRPResponse res = null;
389 res = execute("LSTT");
391 if (res.getCode() == 250) {
392 SVDRPTimerList timers = SVDRPTimerList.parse(res.getMessage());
393 return timers.isRecordingActive();
394 } else if (res.getCode() == 550) {
395 // Error 550 is "No timers defined". Therefore there cannot be an active recording
398 throw new SVDRPParseResponseException(res);
403 * Retrieve VDR Version from SVDRP Client
405 * @return VDR Version
408 public String getSVDRPVersion() {