LIRC libraries
LinuxInfraredRemoteControl
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  self._buffer += self._socket.recv(4096)
233  line, self._buffer = self._buffer.split(b'\n', 1)
234  return line.decode('ascii', 'ignore')
235 
236  def fileno(self) -> int:
237  ''' Implements AbstractConnection.fileno(). '''
238  return self._socket.fileno()
239 
240  def has_data(self) -> bool:
241  ''' Implements AbstractConnection.has_data() '''
242  return b'\n' in self._buffer
243 
244  def close(self):
245  ''' Implements AbstractConnection.close() '''
246  self._socket.close()
247  _client.lirc_deinit()
248 
249 
250 AbstractConnection.register(RawConnection) # pylint:disable=no-member
251 
252 
253 class LircdConnection(AbstractConnection):
254  ''' Interface to receive lircrc-translated keypresses. This is basically
255  built on top of lirc_code2char() and as such supporting centralized
256  translations using lircrc_class. See lircrcd(8).
257 
258  Parameters:
259  - program: string, used to identify client. See ircat(1)
260  - lircrc: lircrc file path. See get_default_lircrc_path() for defaults.
261  - socket_path: lircd output socket path, see get_default_socket_path()
262  for defaults.
263  '''
264  # pylint: disable=no-member
265 
266  def __init__(self, program: str,
267  lircrc_path: str = None,
268  socket_path: str = None):
269  if not lircrc_path:
270  lircrc_path = get_default_lircrc_path()
271  if not lircrc_path:
272  raise FileNotFoundError('Cannot find lircrc config file.')
273  self._connection = RawConnection(socket_path, program)
274  self._lircrc = _client.lirc_readconfig(lircrc_path)
275  self._program = program
276  self._buffer = []
277 
278  def readline(self, timeout: float = None):
279  ''' Implements AbstractConnection.readline(). '''
280  while len(self._buffer) <= 0:
281  code = self._connection.readline(timeout)
282  if code is None:
283  return None
284  strings = \
285  _client.lirc_code2char(self._lircrc, self._program, code)
286  if not strings or len(strings) == 0:
287  if timeout == 0:
288  return None
289  continue
290  self._buffer.extend(strings)
291  return self._buffer.pop(0)
292 
293  def has_data(self) -> bool:
294  ''' Implements AbstractConnection.has_data() '''
295  return len(self._buffer) > 0
296 
297  def fileno(self) -> int:
298  ''' Implements AbstractConnection.fileno(). '''
299  return self._connection.fileno()
300 
301  def close(self):
302  ''' Implements AbstractConnection.close() '''
303  self._connection.close()
304  _client.lirc_freeconfig(self._lircrc)
305 
306 
307 AbstractConnection.register(LircdConnection) # pylint: disable=no-member
308 
309 
310 
311 
312 
361 
362 
363 class CommandConnection(RawConnection):
364  ''' Extends the parent with a send() method. '''
365 
366  def __init__(self, socket_path: str = None):
367  RawConnection.__init__(self, socket_path)
368 
369  def send(self, command: (bytearray, str)):
370  ''' Send single line over socket '''
371  if not isinstance(command, bytearray):
372  command = command.encode('ascii')
373  while len(command) > 0:
374  sent = self._socket.send(command)
375  command = command[sent:]
376 
377 
378 class Result(Enum):
379  ''' Public reply parser result, available when completed. '''
380  OK = 1
381  FAIL = 2
382  INCOMPLETE = 3
383 
384 
385 class Command(object):
386  ''' Command, parser and connection container with a run() method. '''
387 
388  def __init__(self, cmd: str,
389  connection: AbstractConnection,
390  timeout: float = 0.4):
391  self._conn = connection
392  self._cmd_string = cmd
393  self._parser = ReplyParser()
394 
395  def run(self, timeout: float = None):
396  ''' Run the command and return a Reply. Timeout as of
397  AbstractConnection.readline()
398  '''
399  self._conn.send(self._cmd_string)
400  while not self._parser.is_completed():
401  line = self._conn.readline(timeout)
402  if not line:
403  raise TimeoutException('No data from lircd host.')
404  self._parser.feed(line)
405  return self._parser
406 
407 
408 class Reply(object):
409  ''' The status/result from parsing a command reply.
410 
411  Attributes:
412  result: Enum Result, reflects parser state.
413  success: bool, reflects SUCCESS/ERROR.
414  data: List of lines, the command DATA payload.
415  sighup: bool, reflects if a SIGHUP package has been received
416  (these are otherwise ignored).
417  last_line: str, last input line (for error messages).
418  '''
419  def __init__(self):
420  self.result = Result.INCOMPLETE
421  self.success = None
422  self.data = []
423  self.sighup = False
424  self.last_line = ''
425 
426 
427 class ReplyParser(Reply):
428  ''' Handles the actual parsing of a command reply. '''
429 
430  def __init__(self):
431  Reply.__init__(self)
432  self._state = self._State.BEGIN
433  self._lines_expected = None
434  self._buffer = bytearray(0)
435 
436  def is_completed(self) -> bool:
437  ''' Returns true if no more reply input is required. '''
438  return self.result != Result.INCOMPLETE
439 
440  def feed(self, line: str):
441  ''' Enter a line of data into parsing FSM, update state. '''
442 
443  fsm = {
444  self._State.BEGIN: self._begin,
445  self._State.COMMAND: self._command,
446  self._State.RESULT: self._result,
447  self._State.DATA: self._data,
448  self._State.LINE_COUNT: self._line_count,
449  self._State.LINES: self._lines,
450  self._State.END: self._end,
451  self._State.SIGHUP_END: self._sighup_end
452  }
453  line = line.strip()
454  if not line:
455  return
456  self.last_line = line
457  fsm[self._state](line)
458  if self._state == self._State.DONE:
459  self.result = Result.OK
460 
461 
466 
467  class _State(Enum):
468  ''' Internal FSM state. '''
469  BEGIN = 1
470  COMMAND = 2
471  RESULT = 3
472  DATA = 4
473  LINE_COUNT = 5
474  LINES = 6
475  END = 7
476  DONE = 8
477  NO_DATA = 9
478  SIGHUP_END = 10
479 
480  def _bad_packet_exception(self, line):
481  self.result = Result.FAIL
482  raise BadPacketException(
483  'Cannot parse: %s\nat state: %s\n' % (line, self._state))
484 
485  def _begin(self, line):
486  if line == 'BEGIN':
487  self._state = self._State.COMMAND
488 
489  def _command(self, line):
490  if not line:
491  self._bad_packet_exception(line)
492  elif line == 'SIGHUP':
493  self._state = self._State.SIGHUP_END
494  self.sighup = True
495  else:
496  self._state = self._State.RESULT
497 
498  def _result(self, line):
499  if line in ['SUCCESS', 'ERROR']:
500  self.success = line == 'SUCCESS'
501  self._state = self._State.DATA
502  else:
503  self._bad_packet_exception(line)
504 
505  def _data(self, line):
506  if line == 'END':
507  self._state = self._State.DONE
508  elif line == 'DATA':
509  self._state = self._State.LINE_COUNT
510  else:
511  self._bad_packet_exception(line)
512 
513  def _line_count(self, line):
514  try:
515  self._lines_expected = int(line)
516  except ValueError:
517  self._bad_packet_exception(line)
518  if self._lines_expected == 0:
519  self._state = self._State.END
520  else:
521  self._state = self._State.LINES
522 
523  def _lines(self, line):
524  self.data.append(line)
525  if len(self.data) >= self._lines_expected:
526  self._state = self._State.END
527 
528  def _end(self, line):
529  if line != 'END':
530  self._bad_packet_exception(line)
531  self._state = self._State.DONE
532 
533  def _sighup_end(self, line):
534  if line == 'END':
535  ReplyParser.__init__(self)
536  self.sighup = True
537  else:
538  self._bad_packet_exception(line)
539 
540 
543 
544 
545 
546 
547 
553 
554 
555 class SimulateCommand(Command):
556  ''' Simulate a button press, see SIMULATE in lircd(8) manpage. '''
557  # pylint: disable=too-many-arguments
558 
559  def __init__(self, connection: AbstractConnection,
560  remote: str, key: str, repeat: int = 1, keycode: int = 0):
561  cmd = 'SIMULATE %016d %02d %s %s\n' % \
562  (int(keycode), int(repeat), key, remote)
563  Command.__init__(self, cmd, connection)
564 
565 
566 class ListRemotesCommand(Command):
567  ''' List available remotes, see LIST in lircd(8) manpage. '''
568 
569  def __init__(self, connection: AbstractConnection):
570  Command.__init__(self, 'LIST\n', connection)
571 
572 
573 class ListKeysCommand(Command):
574  ''' List available keys in given remote, see LIST in lircd(8) manpage. '''
575 
576  def __init__(self, connection: AbstractConnection, remote: str):
577  Command.__init__(self, 'LIST %s\n' % remote, connection)
578 
579 
580 class StartRepeatCommand(Command):
581  ''' Start repeating given key, see SEND_START in lircd(8) manpage. '''
582 
583  def __init__(self, connection: AbstractConnection,
584  remote: str, key: str):
585  cmd = 'SEND_START %s %s\n' % (remote, key)
586  Command.__init__(self, cmd, connection)
587 
588 
589 class StopRepeatCommand(Command):
590  ''' Stop repeating given key, see SEND_STOP in lircd(8) manpage. '''
591 
592  def __init__(self, connection: AbstractConnection,
593  remote: str, key: str):
594  cmd = 'SEND_STOP %s %s\n' % (remote, key)
595  Command.__init__(self, cmd, connection)
596 
597 
598 class SendCommand(Command):
599  ''' Send given key, see SEND_ONCE in lircd(8) manpage. '''
600 
601  def __init__(self, connection: AbstractConnection,
602  remote: str, keys: str):
603  if not len(keys):
604  raise ValueError('No keys to send given')
605  cmd = 'SEND_ONCE %s %s\n' % (remote, ' '.join(keys))
606  Command.__init__(self, cmd, connection)
607 
608 
609 class SetTransmittersCommand(Command):
610  ''' Set transmitters to use, see SET_TRANSMITTERS in lircd(8) manpage.
611 
612  Arguments:
613  transmitter: Either a bitmask or a list of int describing active
614  transmitter numbers.
615  '''
616 
617  def __init__(self, connection: AbstractConnection,
618  transmitters: (int, list)):
619  if isinstance(transmitters, list):
620  mask = 0
621  for transmitter in transmitters:
622  mask |= (1 << (int(transmitter) - 1))
623  else:
624  mask = transmitters
625  cmd = 'SET_TRANSMITTERS %d\n' % mask
626  Command.__init__(self, cmd, connection)
627 
628 
629 class VersionCommand(Command):
630  ''' Get lircd version, see VERSION in lircd(8) manpage. '''
631 
632  def __init__(self, connection: AbstractConnection):
633  Command.__init__(self, 'VERSION\n', connection)
634 
635 
636 class DrvOptionCommand(Command):
637  ''' Set a driver option value, see DRV_OPTION in lircd(8) manpage. '''
638 
639  def __init__(self, connection: AbstractConnection,
640  option: str, value: str):
641  cmd = 'DRV_OPTION %s %s\n' % (option, value)
642  Command.__init__(self, cmd, connection)
643 
644 
645 class SetLogCommand(Command):
646  ''' Start/stop logging lircd output , see SET_INPUTLOG in lircd(8)
647  manpage.
648  '''
649 
650  def __init__(self, connection: AbstractConnection,
651  logfile: str = None):
652  cmd = 'SET_INPUTLOG' + (' ' + logfile if logfile else '') + '\n'
653  Command.__init__(self, cmd, connection)
654 
655 
656 
657 
658 
664 
665 
666 class IdentCommand(Command):
667  ''' Identify client using the prog token, see IDENT in lircrcd(8) '''
668 
669  def __init__(self, connection: AbstractConnection,
670  prog: str = None):
671  if not prog:
672  raise ValueError('The prog argument cannot be None')
673  cmd = 'IDENT {}\n'.format(prog)
674  Command.__init__(self, cmd, connection)
675 
676 
677 class CodeCommand(Command):
678  '''Translate a keypress to application string, see CODE in lircrcd(8) '''
679 
680  def __init__(self, connection: AbstractConnection,
681  code: str = None):
682  if not code:
683  raise ValueError('The prog argument cannot be None')
684  Command.__init__(self, 'CODE {}\n'.format(code), connection)
685 
686 
687 class GetModeCommand(Command):
688  '''Get current translation mode, see GETMODE in lircrcd(8) '''
689 
690  def __init__(self, connection: AbstractConnection):
691  Command.__init__(self, "GETMODE\n", connection)
692 
693 
694 class SetModeCommand(Command):
695  '''Set current translation mode, see SETMODE in lircrcd(8) '''
696 
697  def __init__(self, connection: AbstractConnection,
698  mode: str = None):
699  if not mode:
700  raise ValueError('The mode argument cannot be None')
701  Command.__init__(self, 'SETMODE {}\n'.format(mode), connection)
702 
703 
704 
705 
706 
lirc.config
Definition: config.py:1