1 ''' Top-level python bindings for the lircd socket interface. '''
28 from abc
import ABCMeta, abstractmethod
40 _DEFAULT_PROG =
'lircd-client'
44 ''' Get default value for the lircd socket path, using (falling priority):
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
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()
59 except configparser.Error:
62 if parser.has_section(
'lircd'):
64 path = str(parser.get(
'lircd',
'output'))
65 if os.path.exists(path):
67 except configparser.NoOptionError:
69 return lirc.config.VARRUNDIR +
'/lirc/lircd'
73 ''' Get default path to the lircrc file according to (falling priority):
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
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):
85 path = os.path.join(os.path.expanduser(
'~'),
'.config' 'lircrc')
86 if os.path.exists(path):
88 path = os.path.join(os.path.expanduser(
'~'),
'.lircrc')
89 if os.path.exists(path):
91 return os.path.join(lirc.config.SYSCONFDIR,
'lirc',
'lircrc')
94 class BadPacketException(Exception):
95 ''' Malformed or otherwise unparsable packet received. '''
99 class TimeoutException(Exception):
100 ''' Timeout receiving data from remote host.'''
155 ''' Abstract interface for all connections. '''
160 def __exit__(self, exc_type, exc, traceback):
164 def readline(self, timeout: float =
None) -> str:
165 ''' Read a buffered line
169 - If set to 0 immediately return either a line or None.
170 - If set to None (default mode) use blocking read.
172 Returns: code string as described in lircd(8) without trailing
175 Raises: TimeoutException if timeout > 0 expires.
181 ''' Return the file nr used for IO, suitable for select() etc. '''
186 ''' Return true if next readline(None) won't block . '''
191 ''' Close/release all resources '''
195 class RawConnection(AbstractConnection):
196 ''' Interface to receive code strings as described in lircd(8).
199 - socket_path: lircd output socket path, see get_default_socket_path()
201 - prog: Program name used in lircrc decoding, see ircat(1). Could be
202 omitted if only raw keypresses should be read.
207 def __init__(self, socket_path: str =
None, prog: str = _DEFAULT_PROG):
209 os.environ[
'LIRC_SOCKET_PATH'] = 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)
219 def readline(self, timeout: float =
None) -> str:
220 ''' Implements AbstractConnection.readline(). '''
224 ready = self._select.select(
225 start + timeout - time.clock()
if timeout
else timeout)
229 "readline: no data within %f seconds" % timeout)
232 self.
_buffer += self._socket.recv(4096)
233 line, self.
_buffer = self._buffer.split(b
'\n', 1)
234 return line.decode(
'ascii',
'ignore')
237 ''' Implements AbstractConnection.fileno(). '''
238 return self._socket.fileno()
241 ''' Implements AbstractConnection.has_data() '''
245 ''' Implements AbstractConnection.close() '''
247 _client.lirc_deinit()
250 AbstractConnection.register(RawConnection)
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).
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()
266 def __init__(self, program: str,
267 lircrc_path: str =
None,
268 socket_path: str =
None):
272 raise FileNotFoundError(
'Cannot find lircrc config file.')
274 self.
_lircrc = _client.lirc_readconfig(lircrc_path)
278 def readline(self, timeout: float =
None):
279 ''' Implements AbstractConnection.readline(). '''
281 code = self._connection.readline(timeout)
286 if not strings
or len(strings) == 0:
290 self._buffer.extend(strings)
291 return self._buffer.pop(0)
294 ''' Implements AbstractConnection.has_data() '''
298 ''' Implements AbstractConnection.fileno(). '''
299 return self._connection.fileno()
302 ''' Implements AbstractConnection.close() '''
303 self._connection.close()
304 _client.lirc_freeconfig(self.
_lircrc)
307 AbstractConnection.register(LircdConnection)
364 ''' Extends the parent with a send() method. '''
366 def __init__(self, socket_path: str =
None):
367 RawConnection.__init__(self, socket_path)
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:]
379 ''' Public reply parser result, available when completed. '''
386 ''' Command, parser and connection container with a run() method. '''
388 def __init__(self, cmd: str,
389 connection: AbstractConnection,
390 timeout: float = 0.4):
391 self.
_conn = connection
395 def run(self, timeout: float =
None):
396 ''' Run the command and return a Reply. Timeout as of
397 AbstractConnection.readline()
400 while not self._parser.is_completed():
401 line = self._conn.readline(timeout)
404 self._parser.feed(line)
409 ''' The status/result from parsing a command reply.
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).
420 self.
result = Result.INCOMPLETE
428 ''' Handles the actual parsing of a command reply. '''
432 self._state = self._State.BEGIN
433 self._lines_expected =
None
434 self._buffer = bytearray(0)
436 def is_completed(self) -> bool:
437 ''' Returns true if no more reply input is required. '''
440 def feed(self, line: str):
441 ''' Enter a line of data into parsing FSM, update state. '''
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
458 if self.
_state == self._State.DONE:
468 ''' Internal FSM state. '''
480 def _bad_packet_exception(self, line):
483 'Cannot parse: %s\nat state: %s\n' % (line, self.
_state))
485 def _begin(self, line):
487 self._state = self._State.COMMAND
489 def _command(self, line):
491 self._bad_packet_exception(line)
492 elif line ==
'SIGHUP':
493 self._state = self._State.SIGHUP_END
496 self._state = self._State.RESULT
498 def _result(self, line):
499 if line
in [
'SUCCESS',
'ERROR']:
500 self.success = line ==
'SUCCESS'
501 self._state = self._State.DATA
503 self._bad_packet_exception(line)
505 def _data(self, line):
507 self._state = self._State.DONE
509 self._state = self._State.LINE_COUNT
511 self._bad_packet_exception(line)
513 def _line_count(self, line):
515 self._lines_expected = int(line)
517 self._bad_packet_exception(line)
518 if self._lines_expected == 0:
519 self._state = self._State.END
521 self._state = self._State.LINES
523 def _lines(self, line):
524 self.data.append(line)
525 if len(self.data) >= self._lines_expected:
526 self._state = self._State.END
528 def _end(self, line):
530 self._bad_packet_exception(line)
531 self._state = self._State.DONE
533 def _sighup_end(self, line):
535 ReplyParser.__init__(self)
538 self._bad_packet_exception(line)
556 ''' Simulate a button press, see SIMULATE in lircd(8) manpage. '''
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)
567 ''' List available remotes, see LIST in lircd(8) manpage. '''
569 def __init__(self, connection: AbstractConnection):
570 Command.__init__(self,
'LIST\n', connection)
574 ''' List available keys in given remote, see LIST in lircd(8) manpage. '''
576 def __init__(self, connection: AbstractConnection, remote: str):
577 Command.__init__(self,
'LIST %s\n' % remote, connection)
581 ''' Start repeating given key, see SEND_START in lircd(8) manpage. '''
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)
590 ''' Stop repeating given key, see SEND_STOP in lircd(8) manpage. '''
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)
599 ''' Send given key, see SEND_ONCE in lircd(8) manpage. '''
601 def __init__(self, connection: AbstractConnection,
602 remote: str, keys: str):
604 raise ValueError(
'No keys to send given')
605 cmd =
'SEND_ONCE %s %s\n' % (remote,
' '.join(keys))
606 Command.__init__(self, cmd, connection)
610 ''' Set transmitters to use, see SET_TRANSMITTERS in lircd(8) manpage.
613 transmitter: Either a bitmask or a list of int describing active
617 def __init__(self, connection: AbstractConnection,
618 transmitters: (int, list)):
619 if isinstance(transmitters, list):
621 for transmitter
in transmitters:
622 mask |= (1 << (int(transmitter) - 1))
625 cmd =
'SET_TRANSMITTERS %d\n' % mask
626 Command.__init__(self, cmd, connection)
630 ''' Get lircd version, see VERSION in lircd(8) manpage. '''
632 def __init__(self, connection: AbstractConnection):
633 Command.__init__(self,
'VERSION\n', connection)
637 ''' Set a driver option value, see DRV_OPTION in lircd(8) manpage. '''
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)
646 ''' Start/stop logging lircd output , see SET_INPUTLOG in lircd(8)
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)
667 ''' Identify client using the prog token, see IDENT in lircrcd(8) '''
669 def __init__(self, connection: AbstractConnection,
672 raise ValueError(
'The prog argument cannot be None')
673 cmd =
'IDENT {}\n'.format(prog)
674 Command.__init__(self, cmd, connection)
678 '''Translate a keypress to application string, see CODE in lircrcd(8) '''
680 def __init__(self, connection: AbstractConnection,
683 raise ValueError(
'The prog argument cannot be None')
684 Command.__init__(self,
'CODE {}\n'.format(code), connection)
688 '''Get current translation mode, see GETMODE in lircrcd(8) '''
690 def __init__(self, connection: AbstractConnection):
691 Command.__init__(self,
"GETMODE\n", connection)
695 '''Set current translation mode, see SETMODE in lircrcd(8) '''
697 def __init__(self, connection: AbstractConnection,
700 raise ValueError(
'The mode argument cannot be None')
701 Command.__init__(self,
'SETMODE {}\n'.format(mode), connection)
def close(self)
Implements AbstractConnection.close()
Get lircd version, see VERSION in lircd(8) manpage.
Send given key, see SEND_ONCE in lircd(8) manpage.
List available keys in given remote, see LIST in lircd(8) manpage.
Simulate a button press, see SIMULATE in lircd(8) manpage.
result
Enum Result, reflects parser state.
def has_data(self)
Return true if next readline(None) won't block .
success
bool, reflects SUCCESS/ERROR.
def fileno(self)
Implements AbstractConnection.fileno().
sighup
bool, reflects if a SIGHUP package has been received
def send
Send single line over socket.
Public reply parser result, available when completed.
def readline
Implements AbstractConnection.readline().
def close(self)
Close/release all resources.
Identify client using the prog token, see IDENT in lircrcd(8)
Command, parser and connection container with a run() method.
def has_data(self)
Implements AbstractConnection.has_data()
def fileno(self)
Return the file nr used for IO, suitable for select() etc.
def run
Run the command and return a Reply.
last_line
str, last input line (for error messages).
Interface to receive code strings as described in lircd(8).
Interface to receive lircrc-translated keypresses.
Extends the parent with a send() method.
The status/result from parsing a command reply.
Abstract interface for all connections.
def readline
Implements AbstractConnection.readline().
data
List of lines, the command DATA payload.
Set transmitters to use, see SET_TRANSMITTERS in lircd(8) manpage.
def has_data(self)
Implements AbstractConnection.has_data()
def fileno(self)
Implements AbstractConnection.fileno().
def get_default_lircrc_path()
Get default path to the lircrc file according to (falling priority):
Timeout receiving data from remote host.
Malformed or otherwise unparsable packet received.
Handles the actual parsing of a command reply.
Start/stop logging lircd output , see SET_INPUTLOG in lircd(8) manpage.
Start repeating given key, see SEND_START in lircd(8) manpage.
Get current translation mode, see GETMODE in lircrcd(8)
Stop repeating given key, see SEND_STOP in lircd(8) manpage.
Set a driver option value, see DRV_OPTION in lircd(8) manpage.
Translate a keypress to application string, see CODE in lircrcd(8)
def get_default_socket_path()
Get default value for the lircd socket path, using (falling priority):
List available remotes, see LIST in lircd(8) manpage.
def readline
Read a buffered line.
def close(self)
Implements AbstractConnection.close()
Set current translation mode, see SETMODE in lircrcd(8)