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 IOException 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 IOException 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 SVDRPDiskStatus status = SVDRPDiskStatus.parse(res.getMessage());
225 throw new SVDRPParseResponseException(res);
230 * Retrieve EPG Event from SVDRPClient
232 * @param type Type of EPG Event (now, next)
233 * @return SVDRP EPG Event
234 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
235 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
238 public SVDRPEpgEvent getEpgEvent(SVDRPEpgEvent.TYPE type)
239 throws SVDRPConnectionException, SVDRPParseResponseException {
240 SVDRPResponse res = null;
241 SVDRPChannel channel = this.getCurrentSVDRPChannel();
244 res = execute(String.format("LSTE %s %s", channel.getNumber(), "now"));
247 res = execute(String.format("LSTE %s %s", channel.getNumber(), "next"));
251 if (res != null && res.getCode() == 215) {
252 SVDRPEpgEvent entry = SVDRPEpgEvent.parse(res.getMessage());
254 } else if (res != null) {
255 throw new SVDRPParseResponseException(res);
257 throw new SVDRPConnectionException("SVDRPResponse is Null");
262 * Retrieve current volume from SVDRP Client
264 * @return SVDRP Volume Object
265 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
266 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
269 public SVDRPVolume getSVDRPVolume() throws SVDRPConnectionException, SVDRPParseResponseException {
270 SVDRPResponse res = null;
272 res = execute("VOLU");
274 if (res.getCode() == 250) {
275 SVDRPVolume volume = SVDRPVolume.parse(res.getMessage());
278 throw new SVDRPParseResponseException(res);
283 * Set volume on SVDRP Client
285 * @param newVolume Volume in Percent
286 * @return SVDRP Volume Object
287 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
288 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
291 public SVDRPVolume setSVDRPVolume(int newVolume) throws SVDRPConnectionException, SVDRPParseResponseException {
292 SVDRPResponse res = null;
294 double newVolumeDouble = newVolume * 255 / 100;
295 res = execute(String.format("VOLU %s", String.valueOf(Math.round(newVolumeDouble))));
297 if (res.getCode() == 250) {
298 SVDRPVolume volume = SVDRPVolume.parse(res.getMessage());
301 throw new SVDRPParseResponseException(res);
306 * Send Key command to SVDRP Client
308 * @param key Key Command to send
309 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
310 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
313 public void sendSVDRPKey(String key) throws SVDRPConnectionException, SVDRPParseResponseException {
314 SVDRPResponse res = null;
316 res = execute(String.format("HITK %s", key));
318 if (res.getCode() != 250) {
319 throw new SVDRPParseResponseException(res);
324 * Send Message to SVDRP Client
326 * @param message Message to send
327 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
328 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
331 public void sendSVDRPMessage(String message) throws SVDRPConnectionException, SVDRPParseResponseException {
332 SVDRPResponse res = null;
334 res = execute(String.format("MESG %s", message));
336 if (res.getCode() != 250) {
337 throw new SVDRPParseResponseException(res);
342 * Retrieve current Channel from SVDRP Client
344 * @return SVDRPChannel object
345 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
346 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
349 public SVDRPChannel getCurrentSVDRPChannel() throws SVDRPConnectionException, SVDRPParseResponseException {
350 SVDRPResponse res = null;
352 res = execute("CHAN");
354 if (res.getCode() == 250) {
355 SVDRPChannel channel = SVDRPChannel.parse(res.getMessage());
358 throw new SVDRPParseResponseException(res);
363 * Change current Channel on SVDRP Client
365 * @param number Channel to be set
366 * @return SVDRPChannel object
367 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
368 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
371 public SVDRPChannel setSVDRPChannel(int number) throws SVDRPConnectionException, SVDRPParseResponseException {
372 SVDRPResponse res = null;
374 res = execute(String.format("CHAN %s", number));
376 if (res.getCode() == 250) {
377 SVDRPChannel channel = SVDRPChannel.parse(res.getMessage());
380 throw new SVDRPParseResponseException(res);
385 * Retrieve from SVDRP Client if a recording is currently active
387 * @return is currently a recording active
388 * @throws SVDRPConnectionException thrown if connection to VDR failed or was not possible
389 * @throws SVDRPParseResponseException thrown if something's not OK with SVDRP response
392 public boolean isRecordingActive() throws SVDRPConnectionException, SVDRPParseResponseException {
393 SVDRPResponse res = null;
395 res = execute("LSTT");
397 if (res.getCode() == 250) {
398 SVDRPTimerList timers = SVDRPTimerList.parse(res.getMessage());
399 return timers.isRecordingActive();
400 } else if (res.getCode() == 550) {
401 // Error 550 is "No timers defined". Therefore there cannot be an active recording
404 throw new SVDRPParseResponseException(res);
409 * Retrieve VDR Version from SVDRP Client
411 * @return VDR Version
412 * @throws SVDRPException thrown if something's not OK with SVDRP call
415 public String getSVDRPVersion() {