diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b247bc3 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# API server for Tion breezer +## Installation +```shell script +git clone https://github.com/TionAPI/python-server.git +cd python-server +pip3 install -r requirements.txt +``` +## Usage +```shell script +python3 main.py +curl -XGET 'http://127.0.0.1/s3/mac_of_you_breezer' +curl -XPOST 'http://127.0.0.1/s3/mac_of_you_breezer --data {"fan_speed": "1"}' +``` diff --git a/apiServer.py b/apiServer.py new file mode 100644 index 0000000..d7719ce --- /dev/null +++ b/apiServer.py @@ -0,0 +1,132 @@ +from http.server import BaseHTTPRequestHandler +from pprint import pprint +import json +import sys +import os +import time + +from tion_btle.tion import tion +from tion_btle.s3 import S3 +from tion_btle.lite import Lite + + +class TionApiServer(BaseHTTPRequestHandler): + allowed_devices = [] + cache_valid = 600 # seconds + cache_expire = 0 + cache_response = {} + + @classmethod + def _is_cache_valid(cls, ts) -> bool: + return not cls.cache_expire < ts + + @classmethod + def _invalidate_cache(cls): + cls.cache_expire = 0 + + @classmethod + def _get_cache(cls) -> dict: + return cls.cache_response + + @classmethod + def _set_cache(cls, response, ts): + cls.cache_expire = ts + cls.cache_valid + cls.cache_response = dict(response) + cls.cache_response["code"] = 304 + cls.cache_response["last_update"] = ts + + def __init__(self, request, client_addr, server): + self.allowed_devices = self._get_allowed_devices() + BaseHTTPRequestHandler.__init__(self, request, client_addr, server) + + def _get_allowed_devices(self): + result = [] + for c in tion.__subclasses__(): + result.append(c.__name__) + return result + + def _set_headers(self, code): + self.send_response(code) + self.send_header('Content-type', 'application/json') + self.end_headers() + + def _send_response(self, code, message: dict, error=""): + self._set_headers(200) + + response = {"code": code, "error": error, } + response = {**response, **message} + response = json.dumps(response) + self.log_message("Response is: " + response) + self.wfile.write((response + "\n").encode()) + + def _create_device(self, device_name: str, device_mac: str) -> tion: + if device_name in self.allowed_devices: + device = getattr(sys.modules[__name__], device_name)(device_mac) + else: + raise AttributeError + return device + + def _send_bad_request(self, message: str): + self._send_response(400, {"message": "request is {}".format(self.path)}, message) + + def _get_device_from_request(self, path: str) -> tion: + + s = path.split("?") + r = s[0].split("/") + try: + device_name = r[1] + device_mac = r[2] + except IndexError as e: + self._send_bad_request("Use requests like '/model/mac_addres'") + return + + if len(s) > 1: + p = s[1].split("&") + + try: + device = self._create_device(device_name, device_mac) + except AttributeError as e: + self._send_response(422, {}, "Device {} is not supported".format(device_name)) + return + + self.log_message(path) + self.log_message("Process device {0} with mac {1}".format(device_name, device_mac)) + + return device + + def do_GET(self): + now = time.time() + if not self._is_cache_valid(now): + try: + device = self._get_device_from_request(self.path) + response = device.get() + except Exception as e: + self._invalidate_cache() # drop cache + self._send_response(400, {}, str(e)) + else: + if response["code"] == 200: + self._set_cache(response, now) + self._send_response(response["code"], response) + else: + response = self._get_cache() + self._send_response(response["code"], response) + + def do_POST(self): + content_len = int(self.headers.get('content-length', 0)) + post_body = self.rfile.read(content_len).decode() + device = self._get_device_from_request(self.path) + + i = 0 + done = False + exception = None + self.log_message(post_body) + self._invalidate_cache() # drop cache + try: + device.set(json.loads(post_body)) + except Exception as e: + self._send_response(400, {}, str(e)) + else: + self._send_response(200, {"message": "Data {} were sent to device {}".format(post_body, device.mac)}) + + def do_PUT(self): + self.do_POST() diff --git a/main.py b/main.py new file mode 100644 index 0000000..f5927c5 --- /dev/null +++ b/main.py @@ -0,0 +1,35 @@ +from http.server import HTTPServer +from pprint import pprint +import json +import sys +import signal + +from apiServer import TionApiServer + +PORT = 8000 +ADDRESS = "127.0.0.1" + +server_address = (ADDRESS, PORT) + +handler = TionApiServer + + +class limitedHttpServer(HTTPServer): + def _signal_handler(self, signum, frame): + raise Exception("Timed out!") + + def serve_forever(self): + """Handle one request at a time until doomsday.""" + while 1: + signal.signal(signal.SIGALRM, self._signal_handler) + signal.alarm(4) + try: + self.handle_request() + except Exception as e: + print("Got exceptin {}".format(str(e))) + finally: + signal.alarm(0) + + +httpd = HTTPServer(server_address, TionApiServer) +httpd.serve_forever() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6ed592c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +tion-btle==1.0.0 \ No newline at end of file