LIRC libraries
Linux Infrared Remote Control
client.py
1 ''' Top-level python bindings for the lircd socket interface. '''
2 
7 
8 
22 
23 # pylint: disable=W0613
24 
25 
27 
28 from abc import ABCMeta, abstractmethod
29 from enum import Enum
30 import configparser
31 import os
32 import os.path
33 import selectors
34 import socket
35 import sys
36 import time
37 
38 import lirc.config
39 import _client
40 
41 _DEFAULT_PROG = 'lircd-client'
42 
43 
44 def get_default_socket_path() -> str:
45  ''' Get default value for the lircd socket path, using (falling priority):
46 
47  - The environment variable LIRC_SOCKET_PATH.
48  - The 'output' value in the lirc_options.conf file if value and the
49  corresponding file exists.
50  - A hardcoded default lirc.config.VARRUNDIR/lirc/lircd, possibly
51  non-existing.
52  '''
53 
54  if 'LIRC_SOCKET_PATH' in os.environ:
55  return os.environ['LIRC_SOCKET_PATH']
56  path = lirc.config.SYSCONFDIR + '/lirc/lirc_options.conf'
57  if sys.version_info < (3, 2):
58  parser = configparser.SafeConfigParser()
59  else:
60  parser = configparser.ConfigParser()
61  try:
62  parser.read(path)
63  except configparser.Error:
64  pass
65  else:
66  if parser.has_section('lircd'):
67  try:
68  path = str(parser.get('lircd', 'output'))
69  if os.path.exists(path):
70  return path
71  except configparser.NoOptionError:
72  pass
73  return lirc.config.VARRUNDIR + '/lirc/lircd'
74 
75 
76 def get_default_lircrc_path() -> str:
77  ''' Get default path to the lircrc file according to (falling priority):
78 
79  - $XDG_CONFIG_HOME/lircrc if environment variable and file exists.
80  - ~/.config/lircrc if it exists.
81  - ~/.lircrc if it exists
82  - A hardcoded default lirc.config.SYSCONFDIR/lirc/lircrc, whether
83  it exists or not.
84  '''
85  if 'XDG_CONFIG_HOME' in os.environ:
86  path = os.path.join(os.environ['XDG_CONFIG_HOME'], 'lircrc')
87  if os.path.exists(path):
88  return path
89  path = os.path.join(os.path.expanduser('~'), '.config' 'lircrc')
90  if os.path.exists(path):
91  return path
92  path = os.path.join(os.path.expanduser('~'), '.lircrc')
93  if os.path.exists(path):
94  return path
95  return os.path.join(lirc.config.SYSCONFDIR, 'lirc', 'lircrc')
96 
97 
98 class BadPacketException(Exception):
99  ''' Malformed or otherwise unparsable packet received. '''
100  pass
101 
102 
103 class TimeoutException(Exception):
104  ''' Timeout receiving data from remote host.'''
105  pass
106 
107 
108 
156 
157 
158 class AbstractConnection(metaclass=ABCMeta):
159  ''' Abstract interface for all connections. '''
160 
161  def __enter__(self):
162  return self
163 
164  def __exit__(self, exc_type, exc, traceback):
165  self.close()
166 
167  @abstractmethod
168  def readline(self, timeout: float = None) -> str:
169  ''' Read a buffered line
170 
171  Parameters:
172  - timeout: seconds.
173  - If set to 0 immediately return either a line or None.
174  - If set to None (default mode) use blocking read.
175 
176  Returns: code string as described in lircd(8) without trailing
177  newline or None.
178 
179  Raises: TimeoutException if timeout > 0 expires.
180  '''
181  pass
182 
183  @abstractmethod
184  def fileno(self) -> int:
185  ''' Return the file nr used for IO, suitable for select() etc. '''
186  pass
187 
188  @abstractmethod
189  def has_data(self) -> bool:
190  ''' Return true if next readline(None) won't block . '''
191  pass
192 
193  @abstractmethod
194  def close(self):
195  ''' Close/release all resources '''
196  pass
197 
198 
199 class RawConnection(AbstractConnection):
200  ''' Interface to receive code strings as described in lircd(8).
201 
202  Parameters:
203  - socket_path: lircd output socket path, see get_default_socket_path()
204  for defaults.
205  - prog: Program name used in lircrc decoding, see ircat(1). Could be
206  omitted if only raw keypresses should be read.
207 
208  '''
209  # pylint: disable=no-member
210 
211  def __init__(self, socket_path: str = None, prog: str = _DEFAULT_PROG):
212  if socket_path:
213  os.environ['LIRC_SOCKET_PATH'] = socket_path
214  else:
215  os.environ['LIRC_SOCKET_PATH'] = get_default_socket_path()
216  _client.lirc_deinit()
217  fd = _client.lirc_init(prog)
218  self._socket = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
219  self._select = selectors.DefaultSelector()
220  self._select.register(self._socket, selectors.EVENT_READ)
221  self._buffer = bytearray(0)
222 
223  def readline(self, timeout: float = None) -> str:
224  ''' Implements AbstractConnection.readline(). '''
225  if timeout:
226  start = time.perf_counter()
227  while b'\n' not in self._buffer:
228  ready = self._select.select(
229  start + timeout - time.perf_counter() if timeout else timeout)
230  if ready == []:
231  if timeout:
232  raise TimeoutException(
233  "readline: no data within %f seconds" % timeout)
234  else:
235  return None
236  recv = self._socket.recv(4096)
237  if len(recv) == 0:
238  raise ConnectionResetError('Connection lost')
239  self._buffer += recv
240  line, self._buffer = self._buffer.split(b'\n', 1)
241  return line.decode('ascii', 'ignore')
242 
243  def fileno(self) -> int:
244  ''' Implements AbstractConnection.fileno(). '''
245  return self._socket.fileno()
246 
247  def has_data(self) -> bool:
248  ''' Implements AbstractConnection.has_data() '''
249  return b'\n' in self._buffer
250 
251  def close(self):
252  ''' Implements AbstractConnection.close() '''
253  self._socket.close()
254  _client.lirc_deinit()
255 
256 
257 AbstractConnection.register(RawConnection) # pylint:disable=no-member
258 
259 
260 class LircdConnection(AbstractConnection):
261  ''' Interface to receive lircrc-translated keypresses. This is basically
262  built on top of lirc_code2char() and as such supporting centralized
263  translations using lircrc_class. See lircrcd(8).
264 
265  Parameters:
266  - program: string, used to identify client. See ircat(1)
267  - lircrc: lircrc file path. See get_default_lircrc_path() for defaults.
268  - socket_path: lircd output socket path, see get_default_socket_path()
269  for defaults.
270  '''
271  # pylint: disable=no-member
272 
273  def __init__(self, program: str,
274  lircrc_path: str = None,
275  socket_path: str = None):
276  if not lircrc_path:
277  lircrc_path = get_default_lircrc_path()
278  if not lircrc_path:
279  raise FileNotFoundError('Cannot find lircrc config file.')
280  self._connection = RawConnection(socket_path, program)
281  self._lircrc = _client.lirc_readconfig(lircrc_path)
282  self._program = program
283  self._buffer = []
284 
285  def readline(self, timeout: float = None):
286  ''' Implements AbstractConnection.readline(). '''
287  while len(self._buffer) <= 0:
288  code = self._connection.readline(timeout)
289  if code is None:
290  return None
291  strings = \
292  _client.lirc_code2char(self._lircrc, self._program, code)
293  if not strings or len(strings) == 0:
294  if timeout == 0:
295  return None
296  continue
297  self._buffer.extend(strings)
298  return self._buffer.pop(0)
299 
300  def has_data(self) -> bool:
301  ''' Implements AbstractConnection.has_data() '''
302  return len(self._buffer) > 0
303 
304  def fileno(self) -> int:
305  ''' Implements AbstractConnection.fileno(). '''
306  return self._connection.fileno()
307 
308  def close(self):
309  ''' Implements AbstractConnection.close() '''
310  self._connection.close()
311  _client.lirc_freeconfig(self._lircrc)
312 
313 
314 AbstractConnection.register(LircdConnection) # pylint: disable=no-member
315 
316 
317 
318 
319 
368 
369 
370 class CommandConnection(RawConnection):
371  ''' Extends the parent with a send() method. '''
372 
373  def __init__(self, socket_path: str = None):
374  RawConnection.__init__(self, socket_path)
375 
376  def send(self, command: (bytearray, str)):
377  ''' Send single line over socket '''
378  if not isinstance(command, bytearray):
379  command = command.encode('ascii')
380  while len(command) > 0:
381  sent = self._socket.send(command)
382  command = command[sent:]
383 
384 
385 class Result(Enum):
386  ''' Public reply parser result, available when completed. '''
387  OK = 1
388  FAIL = 2
389  INCOMPLETE = 3
390 
391 
392 class Command(object):
393  ''' Command, parser and connection container with a run() method. '''
394 
395  def __init__(self, cmd: str,
396  connection: AbstractConnection,
397  timeout: float = 0.4):
398  self._conn = connection
399  self._cmd_string = cmd
400  self._parser = ReplyParser()
401 
402  def run(self, timeout: float = None):
403  ''' Run the command and return a Reply. Timeout as of
404  AbstractConnection.readline()
405  '''
406  self._conn.send(self._cmd_string)
407  while not self._parser.is_completed():
408  line = self._conn.readline(timeout)
409  if not line:
410  raise TimeoutException('No data from lircd host.')
411  self._parser.feed(line)
412  return self._parser
413 
414 
415 class Reply(object):
416  ''' The status/result from parsing a command reply.
417 
418  Attributes:
419  result: Enum Result, reflects parser state.
420  success: bool, reflects SUCCESS/ERROR.
421  data: List of lines, the command DATA payload.
422  sighup: bool, reflects if a SIGHUP package has been received
423  (these are otherwise ignored).
424  last_line: str, last input line (for error messages).
425  '''
426  def __init__(self):
427  self.result = Result.INCOMPLETE
428  self.success = None
429  self.data = []
430  self.sighup = False
431  self.last_line = ''
432 
433 
434 class ReplyParser(Reply):
435  ''' Handles the actual parsing of a command reply. '''
436 
437  def __init__(self):
438  Reply.__init__(self)
439  self._state = self._State.BEGIN
440  self._lines_expected = None
441  self._buffer = bytearray(0)
442 
443  def is_completed(self) -> bool:
444  ''' Returns true if no more reply input is required. '''
445  return self.result != Result.INCOMPLETE
446 
447  def feed(self, line: str):
448  ''' Enter a line of data into parsing FSM, update state. '''
449 
450  fsm = {
451  self._State.BEGIN: self._begin,
452  self._State.COMMAND: self._command,
453  self._State.RESULT: self._result,
454  self._State.DATA: self._data,
455  self._State.LINE_COUNT: self._line_count,
456  self._State.LINES: self._lines,
457  self._State.END: self._end,
458  self._State.SIGHUP_END: self._sighup_end
459  }
460  line = line.strip()
461  if not line:
462  return
463  self.last_line = line
464  fsm[self._state](line)
465  if self._state == self._State.DONE:
466  self.result = Result.OK
467 
468 
473 
474  class _State(Enum):
475  ''' Internal FSM state. '''
476  BEGIN = 1
477  COMMAND = 2
478  RESULT = 3
479  DATA = 4
480  LINE_COUNT = 5
481  LINES = 6
482  END = 7
483  DONE = 8
484  NO_DATA = 9
485  SIGHUP_END = 10
486 
487  def _bad_packet_exception(self, line):
488  self.result = Result.FAIL
489  raise BadPacketException(
490  'Cannot parse: %s\nat state: %s\n' % (line, self._state))
491 
492  def _begin(self, line):
493  if line == 'BEGIN':
494  self._state = self._State.COMMAND
495 
496  def _command(self, line):
497  if not line:
498  self._bad_packet_exception(line)
499  elif line == 'SIGHUP':
500  self._state = self._State.SIGHUP_END
501  self.sighup = True
502  else:
503  self._state = self._State.RESULT
504 
505  def _result(self, line):
506  if line in ['SUCCESS', 'ERROR']:
507  self.success = line == 'SUCCESS'
508  self._state = self._State.DATA
509  else:
510  self._bad_packet_exception(line)
511 
512  def _data(self, line):
513  if line == 'END':
514  self._state = self._State.DONE
515  elif line == 'DATA':
516  self._state = self._State.LINE_COUNT
517  else:
518  self._bad_packet_exception(line)
519 
520  def _line_count(self, line):
521  try:
522  self._lines_expected = int(line)
523  except ValueError:
524  self._bad_packet_exception(line)
525  if self._lines_expected == 0:
526  self._state = self._State.END
527  else:
528  self._state = self._State.LINES
529 
530  def _lines(self, line):
531  self.data.append(line)
532  if len(self.data) >= self._lines_expected:
533  self._state = self._State.END
534 
535  def _end(self, line):
536  if line != 'END':
537  self._bad_packet_exception(line)
538  self._state = self._State.DONE
539 
540  def _sighup_end(self, line):
541  if line == 'END':
542  ReplyParser.__init__(self)
543  self.sighup = True
544  else:
545  self._bad_packet_exception(line)
546 
547 
550 
551 
552 
553 
554 
560 
561 
562 class SimulateCommand(Command):
563  ''' Simulate a button press, see SIMULATE in lircd(8) manpage. '''
564  # pylint: disable=too-many-arguments
565 
566  def __init__(self, connection: AbstractConnection,
567  remote: str, key: str, repeat: int = 1, keycode: int = 0):
568  cmd = 'SIMULATE %016d %02d %s %s\n' % \
569  (int(keycode), int(repeat), key, remote)
570  Command.__init__(self, cmd, connection)
571 
572 
573 class ListRemotesCommand(Command):
574  ''' List available remotes, see LIST in lircd(8) manpage. '''
575 
576  def __init__(self, connection: AbstractConnection):
577  Command.__init__(self, 'LIST\n', connection)
578 
579 
580 class ListKeysCommand(Command):
581  ''' List available keys in given remote, see LIST in lircd(8) manpage. '''
582 
583  def __init__(self, connection: AbstractConnection, remote: str):
584  Command.__init__(self, 'LIST %s\n' % remote, connection)
585 
586 
587 class StartRepeatCommand(Command):
588  ''' Start repeating given key, see SEND_START in lircd(8) manpage. '''
589 
590  def __init__(self, connection: AbstractConnection,
591  remote: str, key: str):
592  cmd = 'SEND_START %s %s\n' % (remote, key)
593  Command.__init__(self, cmd, connection)
594 
595 
596 class StopRepeatCommand(Command):
597  ''' Stop repeating given key, see SEND_STOP in lircd(8) manpage. '''
598 
599  def __init__(self, connection: AbstractConnection,
600  remote: str, key: str):
601  cmd = 'SEND_STOP %s %s\n' % (remote, key)
602  Command.__init__(self, cmd, connection)
603 
604 
605 class SendCommand(Command):
606  ''' Send given key, see SEND_ONCE in lircd(8) manpage. '''
607 
608  def __init__(self, connection: AbstractConnection,
609  remote: str, keys: str):
610  if not len(keys):
611  raise ValueError('No keys to send given')
612  cmd = 'SEND_ONCE %s %s\n' % (remote, ' '.join(keys))
613  Command.__init__(self, cmd, connection)
614 
615 
616 class SetTransmittersCommand(Command):
617  ''' Set transmitters to use, see SET_TRANSMITTERS in lircd(8) manpage.
618 
619  Arguments:
620  transmitter: Either a bitmask or a list of int describing active
621  transmitter numbers.
622  '''
623 
624  def __init__(self, connection: AbstractConnection,
625  transmitters: (int, list)):
626  if isinstance(transmitters, list):
627  mask = 0
628  for transmitter in transmitters:
629  mask |= (1 << (int(transmitter) - 1))
630  else:
631  mask = transmitters
632  cmd = 'SET_TRANSMITTERS %d\n' % mask
633  Command.__init__(self, cmd, connection)
634 
635 
636 class VersionCommand(Command):
637  ''' Get lircd version, see VERSION in lircd(8) manpage. '''
638 
639  def __init__(self, connection: AbstractConnection):
640  Command.__init__(self, 'VERSION\n', connection)
641 
642 
643 class DrvOptionCommand(Command):
644  ''' Set a driver option value, see DRV_OPTION in lircd(8) manpage. '''
645 
646  def __init__(self, connection: AbstractConnection,
647  option: str, value: str):
648  cmd = 'DRV_OPTION %s %s\n' % (option, value)
649  Command.__init__(self, cmd, connection)
650 
651 
652 class SetLogCommand(Command):
653  ''' Start/stop logging lircd output , see SET_INPUTLOG in lircd(8)
654  manpage.
655  '''
656 
657  def __init__(self, connection: AbstractConnection,
658  logfile: str = None):
659  cmd = 'SET_INPUTLOG' + (' ' + logfile if logfile else '') + '\n'
660  Command.__init__(self, cmd, connection)
661 
662 
663 
664 
665 
671 
672 
673 class IdentCommand(Command):
674  ''' Identify client using the prog token, see IDENT in lircrcd(8) '''
675 
676  def __init__(self, connection: AbstractConnection,
677  prog: str = None):
678  if not prog:
679  raise ValueError('The prog argument cannot be None')
680  cmd = 'IDENT {}\n'.format(prog)
681  Command.__init__(self, cmd, connection)
682 
683 
684 class CodeCommand(Command):
685  '''Translate a keypress to application string, see CODE in lircrcd(8) '''
686 
687  def __init__(self, connection: AbstractConnection,
688  code: str = None):
689  if not code:
690  raise ValueError('The prog argument cannot be None')
691  Command.__init__(self, 'CODE {}\n'.format(code), connection)
692 
693 
694 class GetModeCommand(Command):
695  '''Get current translation mode, see GETMODE in lircrcd(8) '''
696 
697  def __init__(self, connection: AbstractConnection):
698  Command.__init__(self, "GETMODE\n", connection)
699 
700 
701 class SetModeCommand(Command):
702  '''Set current translation mode, see SETMODE in lircrcd(8) '''
703 
704  def __init__(self, connection: AbstractConnection,
705  mode: str = None):
706  if not mode:
707  raise ValueError('The mode argument cannot be None')
708  Command.__init__(self, 'SETMODE {}\n'.format(mode), connection)
709 
710 
711 
712 
713