Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyvdr for openSUSE:Factory checked in at 2021-03-18 22:55:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyvdr (Old) and /work/SRC/openSUSE:Factory/.python-pyvdr.new.2401 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyvdr" Thu Mar 18 22:55:10 2021 rev:5 rq:879833 version:0.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyvdr/python-pyvdr.changes 2020-07-31 15:59:38.792493506 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyvdr.new.2401/python-pyvdr.changes 2021-03-18 22:55:12.679548388 +0100 @@ -1,0 +2,9 @@ +Thu Mar 18 07:24:38 UTC 2021 - Martin Hauke <[email protected]> + +- Update to version 0.3.0 + * refactored svdrp socket handling. + * changed behaviour of get_channel_epg_info which now expects + the channel no as parameter instead of getting the channel + itself. + +------------------------------------------------------------------- Old: ---- pyvdr-0.2.3.tar.gz New: ---- pyvdr-0.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyvdr.spec ++++++ --- /var/tmp/diff_new_pack.ORJn3d/_old 2021-03-18 22:55:14.751550626 +0100 +++ /var/tmp/diff_new_pack.ORJn3d/_new 2021-03-18 22:55:14.755550630 +0100 @@ -1,8 +1,8 @@ # # spec file for package python-pyvdr # -# Copyright (c) 2020 SUSE LLC -# Copyright (c) 2020, Martin Hauke <[email protected]> +# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2020-2021, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -20,7 +20,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-pyvdr -Version: 0.2.3 +Version: 0.3.0 Release: 0 Summary: Python library for accessing a Linux VDR via SVDRP License: MIT ++++++ pyvdr-0.2.3.tar.gz -> pyvdr-0.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyvdr-0.2.3/pyvdr/pyvdr.py new/pyvdr-0.3.0/pyvdr/pyvdr.py --- old/pyvdr-0.2.3/pyvdr/pyvdr.py 2020-07-29 23:00:01.000000000 +0200 +++ new/pyvdr-0.3.0/pyvdr/pyvdr.py 2021-03-17 18:01:00.000000000 +0100 @@ -1,21 +1,20 @@ #!/usr/bin/env python3 from .svdrp import SVDRP +from .svdrp import SVDRP_COMMANDS +from .svdrp import SVDRP_RESULT_CODE + import logging import re from collections import namedtuple -EPG_DATA_RECORD = '215' epg_info = namedtuple('EPGDATA', 'Channel Title Description') -# timer_info = namedtuple('TIMER', 'Status Name Date Description') -# channel_info = namedtuple('CHANNEL', 'Number Name') FLAG_TIMER_ACTIVE = 1 FLAG_TIMER_INSTANT_RECORDING = 2 FLAG_TIMER_VPS = 4 FLAG_TIMER_RECORDING = 8 - _LOGGER = logging.getLogger(__name__) @@ -26,12 +25,8 @@ self.svdrp = SVDRP(hostname=self.hostname, timeout=timeout) self.timers = None - def sensors(self): - return ['Vdrinfo'] - def stat(self): - self.svdrp.connect() - self.svdrp.send_cmd("STAT DISK") + self.svdrp.send_cmd(SVDRP_COMMANDS.DISK_INFO) disk_stat_response = self.svdrp.get_response()[1:][0] if disk_stat_response.Code != SVDRP.SVDRP_STATUS_OK: @@ -40,7 +35,6 @@ disk_stat_parts = re.match( r'(\d*)\w. (\d*)\w. (\d*)', disk_stat_response.Value, re.M | re.I) - self.svdrp.disconnect() if disk_stat_parts: return [disk_stat_parts.group(1), disk_stat_parts.group(2), @@ -48,19 +42,24 @@ else: return None + """ + Gets the channel info and returns the channel number and the channel name. + """ def get_channel(self): - self.svdrp.connect() - if not self.svdrp.is_connected(): + self.svdrp.send_cmd(SVDRP_COMMANDS.GET_CHANNEL) + responses = self.svdrp.get_response() + _LOGGER.debug("Response of get channel cmd: '%s'" % responses) + if len(responses) < 1: return None - - self.svdrp.send_cmd("CHAN") - generic_response = self.svdrp.get_response()[-1] + # get 2nd element (1. welcome, 2. response, 3. quit msg) + generic_response = responses[-2] channel = self._parse_channel_response(generic_response) - self.svdrp.disconnect() + _LOGGER.debug("Returned Chan: '%s'" % channel) return channel @staticmethod def _parse_channel_response(channel_data): + _LOGGER.debug("Parsing Channel response to fields: %s" % channel_data.Value) channel_info = {} channel_parts = re.match( @@ -75,16 +74,16 @@ @staticmethod def _parse_timer_response(response): timer = {} - m = re.match( + timer_match = re.match( r'^(\d) (\d{1,2}):(\d{1,2}):(\d{4}-\d{2}-\d{2}):(\d{4}):(\d{4}):(\d+):(\d+):(.*):(.*)$', response.Value, re.M | re.I) - if m: - timer['status'] = m.group(2) - timer['channel'] = m.group(3) - timer['date'] = m.group(4) - timer['name'] = m.group(9) + if timer_match: + timer['status'] = timer_match.group(2) + timer['channel'] = timer_match.group(3) + timer['date'] = timer_match.group(4) + timer['name'] = timer_match.group(9) timer['description'] = "" timer['series'] = timer['name'].find('~') != -1 timer['instant'] = False @@ -96,28 +95,21 @@ def get_timers(self): timers = [] - self.svdrp.connect() - self.svdrp.send_cmd("LSTT") + self.svdrp.send_cmd(SVDRP_COMMANDS.LIST_TIMERS) responses = self.svdrp.get_response() - self.svdrp.disconnect() for response in responses: - if response.Code != '250': + if response.Code != SVDRP_RESULT_CODE.SUCCESS: continue timers.append(self._parse_timer_response(response)) return timers def is_recording(self): - self.svdrp.connect() - if not self.svdrp.is_connected(): - return None - - self.svdrp.send_cmd("LSTT") + self.svdrp.send_cmd(SVDRP_COMMANDS.LIST_TIMERS) responses = self.svdrp.get_response() for response in responses: - if response.Code != '250': + if response.Code != SVDRP_RESULT_CODE.SUCCESS: continue timer = self._parse_timer_response(response) - self.svdrp.disconnect() if len(timer) <= 0: _LOGGER.debug("No output from timer parsing.") return None @@ -129,17 +121,13 @@ return None - def get_channel_epg_info(self): - self.svdrp.connect() - self.svdrp.send_cmd("CHAN") - chan = self.svdrp.get_response()[-1] - channel = self._parse_channel_response(chan) - self.svdrp.send_cmd("LSTE {} now".format(channel['number'])) + def get_channel_epg_info(self, channel_no=1): + epg_title = epg_channel = epg_description = None + self.svdrp.send_cmd(f"{SVDRP_COMMANDS.LIST_EPG} {channel_no} now") epg_data = self.svdrp.get_response()[1:] - self.svdrp.disconnect() - for d in epg_data: - if d[0] == EPG_DATA_RECORD: - epg = re.match(r'^(\S)\s(.*)$', d[2], re.M | re.I) + for data in epg_data: + if data[0] == SVDRP_RESULT_CODE.EPG_DATA_RECORD: + epg = re.match(r'^(\S)\s(.*)$', data[2], re.M | re.I) if epg is not None: epg_field_type = epg.group(1) epg_field_value = epg.group(2) @@ -151,28 +139,23 @@ if epg_field_type == 'D': epg_description = epg_field_value - return channel, epg_info( + return epg_info( Channel=epg_channel, Title=epg_title, Description=epg_description) def channel_up(self): - self.svdrp.connect() - self.svdrp.send_cmd("CHAN +") - reponse_text = self.svdrp.get_response_as_text() - self.svdrp.disconnect() - return reponse_text + self.svdrp.send_cmd(SVDRP_COMMANDS.CHANNEL_UP) + response_text = self.svdrp.get_response_as_text() + return response_text def channel_down(self): - self.svdrp.connect() - self.svdrp.send_cmd("CHAN -") - reponse_text = self.svdrp.get_response_as_text() - self.svdrp.disconnect() - return reponse_text + self.svdrp.send_cmd(SVDRP_COMMANDS.CHANNEL_DOWN) + response_text = self.svdrp.get_response_as_text() + return response_text def list_recordings(self): - self.svdrp.connect() - self.svdrp.send_cmd("LSTR") + self.svdrp.send_cmd(SVDRP_COMMANDS.LIST_RECORDINGS) return self.svdrp.get_response()[1:] @staticmethod @@ -181,14 +164,3 @@ if isinstance(timer_status, str): return int(timer_status) & flag return timer_status & flag - - def test(self): - self.svdrp.connect() - self.svdrp.send_cmd("LSTE 6 now") - return self.svdrp.get_response_text() - - def finish(self): - self.svdrp.shutdown() - - def mypyvdr(self): - return (u'blubb') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyvdr-0.2.3/pyvdr/svdrp.py new/pyvdr-0.3.0/pyvdr/svdrp.py --- old/pyvdr-0.2.3/pyvdr/svdrp.py 2020-07-29 23:00:01.000000000 +0200 +++ new/pyvdr-0.3.0/pyvdr/svdrp.py 2021-03-17 18:01:00.000000000 +0100 @@ -1,16 +1,35 @@ #!/usr/bin/env python3 +from enum import Enum import re import socket import logging from collections import namedtuple + +SVDRP_CMD_LF = '\r\n' response_data = namedtuple('ResponseData', 'Code Separator Value') SVDRP_EMPTY_RESPONSE = "" _LOGGER = logging.getLogger(__name__) +class SVDRP_COMMANDS(str, Enum): + QUIT = 'quit' + LIST_TIMERS = "LSTT" + GET_CHANNEL = "CHAN" + DISK_INFO = "STAT DISK" + LIST_RECORDINGS = "LSTR" + LIST_EPG = "LSTE" + CHANNEL_UP = "CHAN +" + CHANNEL_DOWN = "CHAN -" + + +class SVDRP_RESULT_CODE(str, Enum): + SUCCESS = '250' + EPG_DATA_RECORD = '215' + + class SVDRP(object): SVDRP_STATUS_OK = '250' @@ -19,60 +38,75 @@ self.port = port self.timeout = timeout self.socket = None - self.socket_file = None self.responses = [] - def connect(self): + def _connect(self): if self.socket is None: try: _LOGGER.debug("Setting up connection to {}".format(self.hostname)) self.socket = socket.create_connection((self.hostname, self.port), timeout=self.timeout) - self.socket_file = self.socket.makefile('r') + self.responses = [] except socket.error as se: _LOGGER.info('Unable to connect. Not powered on? {}'.format(se)) - def is_connected(self): - return self.socket is not None - - def disconnect(self): + def _disconnect(self, send_quit=False): _LOGGER.debug("Closing communication with server.") if self.socket is not None: - self.send_cmd("quit") - self.socket_file.close() + if send_quit: + self.socket.sendall(SVDRP_COMMANDS.QUIT.join(SVDRP_CMD_LF).encode()) self.socket.close() + self.socket = None - self.responses = [] - self.socket = None + def is_connected(self): + return self.socket is not None + """ + Sends a SVDRP command to the VDR instance, by default the connection will be created and also be closed at the end. + If the connection should be kept open in the end (e.g. for sending multi-commands) + the param auto_disconnect needs to be set to False on invoking. + The result will be stored in the internal responses array for later content handling. + :return void / nothing + """ def send_cmd(self, cmd): - _LOGGER.debug("Send cmd: {}".format(cmd)) - if not self.is_connected(): - return + self._connect() - cmd += '\r\n' - - if isinstance(cmd, str): - cmd = cmd.encode("utf-8") + command_list = [cmd] + command_list.extend([SVDRP_COMMANDS.QUIT]) + _LOGGER.debug("Send commands: {}".format(command_list)) try: - self.socket.sendall(cmd) + data = list() + [self.socket.sendall(s.join(SVDRP_CMD_LF).encode()) for s in command_list] + while True: + data.append(self.socket.recv(16)) + if not data[-1]: + break except IOError as e: _LOGGER.debug("IOError e {}, closing connection".format(e)) - self.socket.close() + finally: + _LOGGER.debug('Decoding data into responses: %s' % data) + response_raw = b''.join(data) + [self.responses.append(self._parse_response_item(s.decode())) for s in response_raw.splitlines()] - def _parse_response(self, resp): + self._disconnect() + """ + Parses a single response item into data set + :return response_data object + """ + def _parse_response_item(self, resp): # <Reply code:3><-|Space><Text><Newline> - matchobj = re.match(r'^(\d{3})(.)(.*)', resp, re.M | re.I) + match_obj = re.match(r'^(\d{3})(.)(.*)', resp, re.M | re.I) - return response_data(Code=matchobj.group(1), Separator=matchobj.group(2), Value=matchobj.group(3)) + return response_data(Code=match_obj.group(1), Separator=match_obj.group(2), Value=match_obj.group(3)) """ Gets the response from the last CMD and puts it in the internal list. :return Namedtuple (Code, Separator, Value) """ - def _read_response(self): - for line in self.socket_file: - response_entry = self._parse_response(line) + def _read_response_(self): + + for line in self.responses: + response_entry = self._parse_response_item(line) self.responses.append(response_entry) # The first and last row are separated simply by ' ', other with '-'. @@ -85,7 +119,6 @@ :return response as plain text """ def get_response_as_text(self): - self._read_response() return "".join(str(self.responses)) """ @@ -95,20 +128,12 @@ :return List of Namedtuple (Code, Separator, Value) """ def get_response(self, single_line=False): - if not self.is_connected(): + if len(self.responses) == 0: return SVDRP_EMPTY_RESPONSE - self._read_response() if single_line: _LOGGER.debug("Returning single item") return self.responses[2] else: _LOGGER.debug("Returning {} items".format(len(self.responses))) return self.responses - - def shutdown(self): - if self.socket is not None: - self.send_cmd("quit") - self.socket_file.close() - self.socket.close() - self.responses = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyvdr-0.2.3/setup.py new/pyvdr-0.3.0/setup.py --- old/pyvdr-0.2.3/setup.py 2020-07-29 23:00:01.000000000 +0200 +++ new/pyvdr-0.3.0/setup.py 2021-03-17 18:01:00.000000000 +0100 @@ -4,7 +4,7 @@ long_description = fh.read() setup(name='pyvdr', - version='0.2.3', + version='0.3.0', description='Python library for accessing a Linux VDR via SVDRP', long_description=long_description, long_description_content_type="text/markdown", @@ -15,4 +15,4 @@ packages=['pyvdr'], zip_safe=False, python_requires='>=3.6', -) + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyvdr-0.2.3/test/test_PYVDR.py new/pyvdr-0.3.0/test/test_PYVDR.py --- old/pyvdr-0.2.3/test/test_PYVDR.py 2020-07-29 23:00:01.000000000 +0200 +++ new/pyvdr-0.3.0/test/test_PYVDR.py 2021-03-17 18:01:00.000000000 +0100 @@ -15,11 +15,11 @@ ) def test__parse_channel_response(self): - chan_ard = self.func._parse_channel_response(["", "", "1 ARD"]) + chan_ard = self.func._parse_channel_response(response_data("", "", "1 ARD")) self.assertEqual(chan_ard['number'], "1") self.assertEqual(chan_ard['name'], "ARD") - chan_prosieben = self.func._parse_channel_response(["", "", "11 Pro Sieben"]) + chan_prosieben = self.func._parse_channel_response(response_data("", "", "11 Pro Sieben")) self.assertEqual(chan_prosieben['number'], "11") self.assertEqual(chan_prosieben['name'], "Pro Sieben") @@ -58,17 +58,44 @@ }) # timer active, not yet recording - self.assertTrue(self.func._check_timer_recording_flag(t_active, pyvdr.FLAG_TIMER_ACTIVE), "Timer should be active") - self.assertFalse(self.func._check_timer_recording_flag(t_active, pyvdr.FLAG_TIMER_RECORDING), "Timer should not be recording") + self.assertTrue( + self.func._check_timer_recording_flag( + t_active, + pyvdr.FLAG_TIMER_ACTIVE + ), "Timer should be active") + self.assertFalse( + self.func._check_timer_recording_flag( + t_active, + pyvdr.FLAG_TIMER_RECORDING + ), "Timer should not be recording") # timer active, recording - self.assertTrue(self.func._check_timer_recording_flag(t_active_and_recording, pyvdr.FLAG_TIMER_ACTIVE), "Timer should be active") - self.assertTrue(self.func._check_timer_recording_flag(t_active_and_recording, pyvdr.FLAG_TIMER_RECORDING), "Timer should be recording") + self.assertTrue( + self.func._check_timer_recording_flag( + t_active_and_recording, + pyvdr.FLAG_TIMER_ACTIVE + ), "Timer should be active") + self.assertTrue( + self.func._check_timer_recording_flag( + t_active_and_recording, + pyvdr.FLAG_TIMER_RECORDING + ), "Timer should be recording") # instant recording - self.assertTrue(self.func._check_timer_recording_flag(t_active_and_instant_recording, pyvdr.FLAG_TIMER_RECORDING), "Timer active") - self.assertTrue(self.func._check_timer_recording_flag(t_active_and_instant_recording, pyvdr.FLAG_TIMER_RECORDING), "Timer recording") - self.assertTrue(self.func._check_timer_recording_flag(t_active_and_instant_recording, pyvdr.FLAG_TIMER_INSTANT_RECORDING), "Timer instant recording") + self.assertTrue( + self.func._check_timer_recording_flag( + t_active_and_instant_recording, + pyvdr.FLAG_TIMER_RECORDING + ), "Timer active") + self.assertTrue( + self.func._check_timer_recording_flag( + t_active_and_instant_recording, + pyvdr.FLAG_TIMER_RECORDING + ), "Timer recording") + self.assertTrue( + self.func._check_timer_recording_flag( + t_active_and_instant_recording, pyvdr.FLAG_TIMER_INSTANT_RECORDING + ), "Timer instant recording") def test__parse_timer_responses(self): parseable_responses = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyvdr-0.2.3/test/test_Real.py new/pyvdr-0.3.0/test/test_Real.py --- old/pyvdr-0.2.3/test/test_Real.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pyvdr-0.3.0/test/test_Real.py 2021-03-17 18:01:00.000000000 +0100 @@ -0,0 +1,18 @@ +import unittest +from pyvdr import PYVDR + +class MyTestCase(unittest.TestCase): + def test_something(self): + + # pyvdr = PYVDR(hostname='easyvdr.fritz.box') + # channel = pyvdr.get_channel() + # epg_info = pyvdr.get_channel_epg_info() + # diskstat = pyvdr.stat() + # list_of_timers = pyvdr.get_timers() + # is_recording = pyvdr.is_recording() + # list_of_recordings = pyvdr.list_recordings() + + pass + +if __name__ == '__main__': + unittest.main()
