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