LIRC libraries Linux Infrared Remote Control

Loading...
Searching...
No Matches
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 sys
36 import time
37
38 import lirc.config
39 import _client
40
41_DEFAULT_PROG = 'lircd-client'
42
43
44 def 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
76 def 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
98 class BadPacketException(Exception):
99 ''' Malformed or otherwise unparsable packet received. '''
100 pass
101
102
103 class TimeoutException(Exception):
104 ''' Timeout receiving data from remote host.'''
105 pass
106
107
108
156
157
158 class 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
198
199 class 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()
255
256
257AbstractConnection.register(RawConnection) # pylint:disable=no-member
258
259
260 class LircdConnection(AbstractConnection):
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)
312
313
314AbstractConnection.register(LircdConnection) # pylint: disable=no-member
315
316
317
318
319
368
369
370 class CommandConnection(RawConnection):
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
385 class Result(Enum):
386 ''' Public reply parser result, available when completed. '''
387 OK = 1
388 FAIL = 2
389 INCOMPLETE = 3
390
391
392 class 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
415 class 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
434 class 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)
442
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. '''
449
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))
491
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
562 class SimulateCommand(Command):
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
573 class ListRemotesCommand(Command):
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
579
580 class ListKeysCommand(Command):
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
587 class StartRepeatCommand(Command):
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
596 class StopRepeatCommand(Command):
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
604
605 class 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
616 class SetTransmittersCommand(Command):
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
636 class VersionCommand(Command):
637 ''' Get lircd version, see VERSION in lircd(8) manpage. '''
638
639 def __init__(self, connection: AbstractConnection):
640 Command.__init__(self, 'VERSION\n', connection)
641
642
643 class DrvOptionCommand(Command):
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
652 class SetLogCommand(Command):
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
671
672
673 class 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
684 class 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
694 class GetModeCommand(Command):
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
701 class SetModeCommand(Command):
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
711
712
713