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
28from abc import ABCMeta, abstractmethod
29from enum import Enum
30import configparser
31import os
32import os.path
33import selectors
34import socket
35import time
36
37import lirc.config
38import _client
39
40_DEFAULT_PROG = 'lircd-client'
41
42
43def 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
72def 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
94class BadPacketException(Exception):
95 ''' Malformed or otherwise unparsable packet received. '''
96 pass
97
98
99class TimeoutException(Exception):
100 ''' Timeout receiving data from remote host.'''
101 pass
102
103
104
152
153
154class 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
195class 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.perf_counter()
223 while b'\n' not in self._buffer:
224 ready = self._select.select(
225 start + timeout - time.perf_counter() 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
250AbstractConnection.register(RawConnection) # pylint:disable=no-member
251
252
253class 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
307AbstractConnection.register(LircdConnection) # pylint: disable=no-member
308
309
310
311
312
361
362
363class 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
378class Result(Enum):
379 ''' Public reply parser result, available when completed. '''
380 OK = 1
381 FAIL = 2
382 INCOMPLETE = 3
383
384
385class 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
408class 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
427class 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
555class 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
566class 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
573class 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
580class 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
589class 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
598class 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
609class 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
629class 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
636class 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
645class 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
666class 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
677class 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
687class 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
694class 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