Browse Source

Initial commit

Phyks (Lucas Verney) 4 years ago
commit
386a688296
No known key found for this signature in database
11 changed files with 339 additions and 0 deletions
  1. 2
    0
      .gitignore
  2. 21
    0
      LICENSE.md
  3. 10
    0
      README.md
  4. 0
    0
      lircweb/__init__.py
  5. 8
    0
      lircweb/__main__.py
  6. 63
    0
      lircweb/app.py
  7. 19
    0
      lircweb/exceptions.py
  8. 115
    0
      lircweb/macro.py
  9. 100
    0
      lircweb/remote.py
  10. 0
    0
      macros/.gitkeep
  11. 1
    0
      requirements.txt

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+macros
2
+*.pyc

+ 21
- 0
LICENSE.md View File

@@ -0,0 +1,21 @@
1
+The MIT License (MIT)
2
+
3
+Copyright (c) 2017 Phyks (Lucas Verney)
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 10
- 0
README.md View File

@@ -0,0 +1,10 @@
1
+LIRCWeb
2
+=======
3
+
4
+Some code to run macros based on LIRC, from the Web.
5
+
6
+
7
+## License
8
+
9
+The content of this repository is licensed under an MIT license, unless
10
+explicitly mentionned otherwise.

+ 0
- 0
lircweb/__init__.py View File


+ 8
- 0
lircweb/__main__.py View File

@@ -0,0 +1,8 @@
1
+from __future__ import absolute_import, unicode_literals
2
+
3
+import lircweb.app
4
+
5
+if __name__ == "__main__":
6
+    app = lircweb.app.get_app()
7
+    app.run(host="0.0.0.0", port=8080)
8
+     

+ 63
- 0
lircweb/app.py View File

@@ -0,0 +1,63 @@
1
+from __future__ import absolute_import, unicode_literals
2
+
3
+import bottle
4
+
5
+import lircweb.macro
6
+import lircweb.remote
7
+
8
+
9
+app = bottle.Bottle()
10
+
11
+
12
+@app.get("/api/v1/remotes")
13
+def list_available_remotes():
14
+    """
15
+    Return the list of all available remotes.
16
+    """
17
+    return {
18
+        "remotes": {
19
+            remote: "/api/v1/remotes/{}".format(remote)
20
+            for remote in lircweb.remote.list_remotes()
21
+        }
22
+    }
23
+
24
+
25
+@app.get("/api/v1/remotes/:remote")
26
+@app.get("/api/v1/remotes/:remote/macros")
27
+def list_available_macros_for_remote(remote):
28
+    """
29
+    List available macros for the given remote.
30
+    """
31
+    return {
32
+        "macros": {
33
+            macro.replace(".json", ""): "/api/v1/remotes/{}/macros/{}".format(remote, macro.replace(".json", ""))
34
+            for macro in lircweb.macro.list_macros(remote)
35
+        }
36
+    }
37
+
38
+
39
+@app.get("/api/v1/remotes/:remote/macros/:macro")
40
+def show_macro(remote, macro):
41
+    """
42
+    Show specified macro.
43
+    """
44
+    return {
45
+        "macro": lircweb.macro.parse_macro(remote, macro)
46
+    }
47
+
48
+
49
+@app.get("/api/v1/remotes/:remote/macros/:macro/run")
50
+def run_macro(remote, macro):
51
+    """
52
+    Run the specified macro.
53
+    """
54
+    parsed_macro = lircweb.macro.parse_macro(remote, macro)
55
+    lircweb.macro.process_macro(parsed_macro, remote)
56
+    return "OK"
57
+
58
+
59
+def get_app():
60
+    """
61
+    Get Bottle app object.
62
+    """
63
+    return app

+ 19
- 0
lircweb/exceptions.py View File

@@ -0,0 +1,19 @@
1
+# coding: utf-8
2
+"""
3
+Definition of the exceptions raised by lircweb.
4
+"""
5
+from __future__ import absolute_import, unicode_literals
6
+
7
+
8
+class LIRCSocketNotFound(Exception):
9
+    """
10
+    Exception raised when lircweb is unable to connect to LIRC socket.
11
+    """
12
+    pass
13
+
14
+
15
+class LIRCException(Exception):
16
+    """
17
+    Exception raised when LIRC encountered an error.
18
+    """
19
+    pass

+ 115
- 0
lircweb/macro.py View File

@@ -0,0 +1,115 @@
1
+# coding: utf-8
2
+"""
3
+Handle macros, parse and run them.
4
+"""
5
+from __future__ import absolute_import, unicode_literals
6
+
7
+import json
8
+import os
9
+import time
10
+
11
+from lircweb.remote import Remote
12
+
13
+
14
+def get_macros_dir():
15
+    """
16
+    Get the macros directory.
17
+    """
18
+    return os.getenv("LIRCWEB_MACROS_DIR", "macros")
19
+
20
+
21
+def list_macros(remote=None):
22
+    """
23
+    List all available macros.
24
+
25
+    Params:
26
+        - remote (str): Optional remote to restrict macros to.
27
+    """
28
+    macros_dir = get_macros_dir()
29
+    if remote:
30
+        try:
31
+            macros = os.listdir(os.path.join(macros_dir, remote))
32
+        except FileNotFoundError:
33
+            macros = []
34
+    else:
35
+        macros = {}
36
+        try:
37
+            remotes = [d for f in os.listdir(macros_dir) if os.path.isdir(d)]
38
+        except FileNotFoundError:
39
+            remotes = []
40
+        for remote in remotes:
41
+            macros[remote] = list_macros(remote=remote)
42
+    return macros
43
+
44
+
45
+def parse_macro_string(macro_string):
46
+    """
47
+    Parse a JSON string describing a macro.
48
+
49
+    Params:
50
+        - macro_string (str): The macro string to parse.
51
+    Returns:
52
+        dict: The parsed macro dict.
53
+    """
54
+    return json.loads(macro_string)
55
+
56
+
57
+def parse_macro_file(macro_file):
58
+    """
59
+    Parse a file containing a macro definition.
60
+
61
+    Params:
62
+        - macro_file (str): Path to the macro file to parse. If not an absolute path, consider it relative to LIRCWEB_MACROS_DIR.
63
+    Returns:
64
+        dict: The parsed macro dict.
65
+    """
66
+    if not macro_file.startswith("/"):
67
+        macro_file = os.path.join(get_macros_dir(), macro_file)
68
+    with open(macro_file, 'r') as fh:
69
+        return parse_macro_string(fh.read())
70
+
71
+
72
+def parse_macro(remote, macro):
73
+    """
74
+    Parse a macro identified by the remote and its name.
75
+
76
+    Params:
77
+        - remote (str): Name of the remote.
78
+        - macro (str): Name of the macro.
79
+    Returns:
80
+        dict: The parsed macro dict.
81
+    """
82
+    return parse_macro_file("{}/{}.json".format(remote, macro))
83
+
84
+
85
+def process_macro(macro, remote):
86
+    """
87
+    Process a given macro on a given remote.
88
+
89
+    Params:
90
+        - macro (dict): A macro to run.
91
+        - remote (str): The remote to process macro on.
92
+    Returns: None
93
+    """
94
+    remote = Remote(remote)
95
+    for cmd in macro["codes"]:
96
+        process_macro_cmd(remote, cmd)
97
+
98
+
99
+def process_macro_cmd(remote, cmd):
100
+    """
101
+    Process a given command in a macro list of commands.
102
+
103
+    Params:
104
+        - remote (Remote): a Remote object to execute command on.
105
+        - cmd (str): The code to send.
106
+    Returns: None
107
+    """
108
+    if cmd.startwith("delay"):
109
+        delay = float(cmd.split()[1])
110
+        time.sleep(delay)
111
+    elif cmd.smartswith("macro"):
112
+        macro = parse_macro(remote, cmd.split()[1])
113
+        process_macro(macro, remote)
114
+    else:
115
+        remote.send_once(cmd)

+ 100
- 0
lircweb/remote.py View File

@@ -0,0 +1,100 @@
1
+"""
2
+A Remote class to abstract on top of a LIRC remote and send codes.
3
+"""
4
+from __future__ import absolute_import, unicode_literals
5
+
6
+import socket
7
+
8
+import lircweb.exceptions
9
+
10
+
11
+LIRC_SOCKET = "/var/run/lirc/lircd"
12
+
13
+
14
+def irsend(directive, remote, code, lirc_socket=LIRC_SOCKET):
15
+    """
16
+    Send some instruction to LIRC using irsend.
17
+
18
+    Params:
19
+        directive (str): See irsend doc.
20
+        remote (str): See irsend doc.
21
+        code (str): See irsend doc.
22
+        lirc_socket (str): Path to the Unix LIRC socket to use.
23
+    Returns:
24
+        str: The output of the LIRC call.
25
+
26
+    May raise CalledProcessError if irsend fails.
27
+    """
28
+    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
29
+    try:
30
+        sock.connect(lirc_socket)
31
+    except socket.error:
32
+        raise lircweb.exceptions.LIRCSocketNotFound
33
+
34
+    received = ""
35
+    try:
36
+        message = "{} {} {}\n".format(directive, remote, code)
37
+        sock.sendall(message)
38
+
39
+        while not received.strip().endswith("END"):
40
+            received += sock.recv(16)
41
+    finally:
42
+        sock.close()
43
+
44
+    try:
45
+        received = received.splitlines()
46
+        assert received[0].strip() == "BEGIN"
47
+        assert received[1].strip() == message.strip()
48
+        assert received[2].strip() == "SUCCESS"
49
+        output = received[3:-1]
50
+        assert received[-1].strip() == "END"
51
+    except AssertionError:
52
+        raise lircweb.exceptions.LIRCException
53
+
54
+    return output
55
+
56
+
57
+def list_remotes():
58
+    """
59
+    List all available LIRC remotes.
60
+    """
61
+    remotes = irsend("LIST", "", "")[2:]
62
+    return remotes
63
+
64
+
65
+class Remote(object):
66
+    """
67
+    Abstract on top of LIRC remotes.
68
+    """
69
+    def __init__(self, remote):
70
+        """
71
+        Params:
72
+            - remote (str): A valid LIRC remote.
73
+        """
74
+        self.remote = remote
75
+
76
+    def send_once(self, code):
77
+        """
78
+        Send the code to the remote once. For available codes, see LIRC doc.
79
+        """
80
+        return irsend("SEND_ONCE", self.remote, code)
81
+
82
+    def send_start(self, code):
83
+        """
84
+        Send the code continuously to the remote. For available codes, see LIRC
85
+        doc.
86
+        """
87
+        return irsend("SEND_START", self.remote, code)
88
+
89
+    def send_stop(self, code):
90
+        """
91
+        Stop sending the code continuously to the remote. For available codes,
92
+        see LIRC doc.
93
+        """
94
+        return irsend("SEND_STOP", self.remote, code)
95
+
96
+    def list_codes(self, code=""):
97
+        """
98
+        List available codes for this remote.
99
+        """
100
+        return irsend("LIST", self.remote, code)

+ 0
- 0
macros/.gitkeep View File


+ 1
- 0
requirements.txt View File

@@ -0,0 +1 @@
1
+bottle==0.12.13