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