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)
 
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 fileno
Return the file nr used for IO, suitable for select() etc. 
 
success
bool, reflects SUCCESS/ERROR. 
 
def fileno
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 has_data
Implements AbstractConnection.has_data() 
 
Identify client using the prog token, see IDENT in lircrcd(8) 
 
def get_default_socket_path
Get default value for the lircd socket path, using (falling priority): 
 
Command, parser and connection container with a run() method. 
 
def run
Run the command and return a Reply. 
 
last_line
str, last input line (for error messages). 
 
def get_default_lircrc_path
Get default path to the lircrc file according to (falling priority): 
 
Interface to receive code strings as described in lircd(8). 
 
Interface to receive lircrc-translated keypresses. 
 
def fileno
Implements AbstractConnection.fileno(). 
 
Extends the parent with a send() method. 
 
The status/result from parsing a command reply. 
 
Abstract interface for all connections. 
 
def close
Implements AbstractConnection.close() 
 
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 close
Implements AbstractConnection.close() 
 
Timeout receiving data from remote host. 
 
Malformed or otherwise unparsable packet received. 
 
def has_data
Implements AbstractConnection.has_data() 
 
def close
Close/release all resources. 
 
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) 
 
List available remotes, see LIST in lircd(8) manpage. 
 
def readline
Read a buffered line. 
 
def has_data
Return true if next readline(None) won't block . 
 
Set current translation mode, see SETMODE in lircrcd(8)