| LIRC libraries LinuxInfraredRemoteControl

client.py
Go to the documentation of this file.
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.client.RawConnection.close
def close(self)
Implements AbstractConnection.close()
Definition: client.py:248
lirc.client.VersionCommand
Get lircd version, see VERSION in lircd(8) manpage.
Definition: client.py:646
lirc.client.SendCommand
Send given key, see SEND_ONCE in lircd(8) manpage.
Definition: client.py:615
lirc.client.ListKeysCommand
List available keys in given remote, see LIST in lircd(8) manpage.
Definition: client.py:590
lirc.client.SimulateCommand
Simulate a button press, see SIMULATE in lircd(8) manpage.
Definition: client.py:572
lirc.client.Reply.result
result
Enum Result, reflects parser state.
Definition: client.py:434
lirc.client.AbstractConnection.has_data
def has_data(self)
Return true if next readline(None) won&#39;t block .
Definition: client.py:189
lirc.client.RawConnection._socket
_socket
Definition: client.py:217
lirc.client.Reply.success
success
bool, reflects SUCCESS/ERROR.
Definition: client.py:435
lirc.client.LircdConnection.fileno
def fileno(self)
Implements AbstractConnection.fileno().
Definition: client.py:301
lirc.client.RawConnection._select
_select
Definition: client.py:218
lirc.client.Command._parser
_parser
Definition: client.py:396
lirc.config
Definition: config.py:1
lirc.client.LircdConnection._lircrc
_lircrc
Definition: client.py:277
lirc.client.Reply.sighup
sighup
bool, reflects if a SIGHUP package has been received
Definition: client.py:437
lirc.client.CommandConnection.send
def send
Send single line over socket.
Definition: client.py:373
lirc.client.Result
Public reply parser result, available when completed.
Definition: client.py:382
lirc.client.LircdConnection.readline
def readline
Implements AbstractConnection.readline().
Definition: client.py:282
lirc.client.AbstractConnection.close
def close(self)
Close/release all resources.
Definition: client.py:194
lirc.client.RawConnection._buffer
_buffer
Definition: client.py:220
lirc.client.IdentCommand
Identify client using the prog token, see IDENT in lircrcd(8)
Definition: client.py:683
lirc.client.Command
Command, parser and connection container with a run() method.
Definition: client.py:389
lirc.client.RawConnection.has_data
def has_data(self)
Implements AbstractConnection.has_data()
Definition: client.py:244
lirc.client.AbstractConnection.fileno
def fileno(self)
Return the file nr used for IO, suitable for select() etc.
Definition: client.py:184
lirc.client.Command.run
def run
Run the command and return a Reply.
Definition: client.py:401
lirc.client.Reply.last_line
last_line
str, last input line (for error messages).
Definition: client.py:438
lirc.client.RawConnection
Interface to receive code strings as described in lircd(8).
Definition: client.py:207
lirc.client.LircdConnection
Interface to receive lircrc-translated keypresses.
Definition: client.py:266
lirc.client.CommandConnection
Extends the parent with a send() method.
Definition: client.py:367
lirc.client.Reply
The status/result from parsing a command reply.
Definition: client.py:415
lirc.client.AbstractConnection
Abstract interface for all connections.
Definition: client.py:158
lirc.client.LircdConnection._connection
_connection
Definition: client.py:276
lirc.client.ReplyParser._state
_state
Definition: client.py:446
lirc.client.RawConnection.readline
def readline
Implements AbstractConnection.readline().
Definition: client.py:223
lirc.client.Reply.data
data
List of lines, the command DATA payload.
Definition: client.py:436
lirc.client.LircdConnection._program
_program
Definition: client.py:278
lirc.client.SetTransmittersCommand
Set transmitters to use, see SET_TRANSMITTERS in lircd(8) manpage.
Definition: client.py:631
lirc.client.LircdConnection.has_data
def has_data(self)
Implements AbstractConnection.has_data()
Definition: client.py:297
lirc.client.RawConnection.fileno
def fileno(self)
Implements AbstractConnection.fileno().
Definition: client.py:240
lirc.client.Command._conn
_conn
Definition: client.py:394
lirc.client.LircdConnection._buffer
_buffer
Definition: client.py:279
lirc.client.get_default_lircrc_path
def get_default_lircrc_path()
Get default path to the lircrc file according to (falling priority):
Definition: client.py:83
lirc.client.TimeoutException
Timeout receiving data from remote host.
Definition: client.py:103
lirc.client.BadPacketException
Malformed or otherwise unparsable packet received.
Definition: client.py:98
lirc.client.ReplyParser
Handles the actual parsing of a command reply.
Definition: client.py:442
lirc.client.SetLogCommand
Start/stop logging lircd output , see SET_INPUTLOG in lircd(8) manpage.
Definition: client.py:664
lirc.client.StartRepeatCommand
Start repeating given key, see SEND_START in lircd(8) manpage.
Definition: client.py:597
lirc.client.Command._cmd_string
_cmd_string
Definition: client.py:395
lirc.client.GetModeCommand
Get current translation mode, see GETMODE in lircrcd(8)
Definition: client.py:704
lirc.client.StopRepeatCommand
Stop repeating given key, see SEND_STOP in lircd(8) manpage.
Definition: client.py:606
lirc.client.DrvOptionCommand
Set a driver option value, see DRV_OPTION in lircd(8) manpage.
Definition: client.py:653
lirc.client.CodeCommand
Translate a keypress to application string, see CODE in lircrcd(8)
Definition: client.py:694
lirc.client.get_default_socket_path
def get_default_socket_path()
Get default value for the lircd socket path, using (falling priority):
Definition: client.py:54
lirc.client.ListRemotesCommand
List available remotes, see LIST in lircd(8) manpage.
Definition: client.py:583
lirc.client.AbstractConnection.readline
def readline
Read a buffered line.
Definition: client.py:179
lirc.client.LircdConnection.close
def close(self)
Implements AbstractConnection.close()
Definition: client.py:305
lirc.client._State
Internal FSM state.
Definition: client.py:484
lirc.client.SetModeCommand
Set current translation mode, see SETMODE in lircrcd(8)
Definition: client.py:711