forked from stefs/home-sensor
/
api.py
174 lines (148 loc) · 4.64 KB
/
api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import http.client
import http.server
import json
import logging
import pickle
import socketserver
import ssl
import threading
import time
CERT = 'server.crt'
CONTENT_TYPE = 'application/json'
HOST = 'kaloix.de'
INTERVAL = 10
KEY = 'server.key'
PORT = 64918
TIMEOUT = 60
TOKEN_FILE = 'api_token'
class ApiClient(object):
def __init__(self):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(CERT)
self.conn = http.client.HTTPSConnection(HOST, PORT, timeout=TIMEOUT,
context=context)
try:
with open('buffer.pickle', 'rb') as file:
self.buffer = pickle.load(file)
except FileNotFoundError:
self.buffer = list()
self.buffer_send = threading.Event()
self.buffer_mutex = threading.Lock()
with open(TOKEN_FILE) as token_file:
self.token = token_file.readline().strip()
def __enter__(self):
self.shutdown = False
self.sender = threading.Thread(target=self._sender)
self.sender.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
logging.info('wait for empty buffer')
self.shutdown = True
self.buffer_send.set()
self.sender.join()
def _sender(self):
self.buffer_send.wait()
while not self.shutdown or self.buffer:
time.sleep(INTERVAL)
with self.buffer_mutex:
self._send_buffer()
self._backup_buffer()
if not self.shutdown:
self.buffer_send.wait()
def _send_buffer(self):
start = time.perf_counter()
count = int()
try:
self.conn.connect()
for item in self.buffer:
try:
self._send(**item)
except ApiError as err:
logging.error('unable to send {}: {}'.format(item, err))
count += 1
except (http.client.HTTPException, OSError) as err:
logging.warning('postpone send: {}'.format(type(err).__name__))
self.buffer = self.buffer[count:]
if not self.buffer:
self.buffer_send.clear()
self.conn.close()
if count:
logging.info('sent {} item{} in {:.1f}s'.format(
count, '' if count==1 else 's', time.perf_counter()-start))
def _send(self, **kwargs):
kwargs['_token'] = self.token
try:
body = json.dumps(kwargs)
except TypeError as err:
raise ApiError(str(err))
headers = {'Content-type': CONTENT_TYPE, 'Accept': 'text/plain'}
self.conn.request('POST', '', body, headers)
resp = self.conn.getresponse()
resp.read()
if resp.status != 201:
raise ApiError('{} {}'.format(resp.status, resp.reason))
def _backup_buffer(self):
with open('buffer.pickle', 'wb') as file:
pickle.dump(self.buffer, file)
def send(self, **kwargs):
with self.buffer_mutex:
self.buffer.append(kwargs)
self.buffer_send.set()
class ApiServer(object):
def __init__(self, handle_function):
self.httpd = ThreadedHTTPServer(('', PORT), HTTPRequestHandler)
self.httpd.socket = ssl.wrap_socket(self.httpd.socket,
keyfile=KEY, certfile=CERT,
server_side=True,
do_handshake_on_connect=False)
self.httpd.handle = handle_function
with open(TOKEN_FILE) as token_file:
self.httpd.token = [t.strip() for t in token_file]
def __enter__(self):
self.server = threading.Thread(target=self.httpd.serve_forever)
self.server.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
logging.info('shutdown api server')
self.httpd.shutdown()
self.server.join()
class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
if self.headers['content-type'] != CONTENT_TYPE:
self.send_error(400, 'bad content type')
self.end_headers()
return
try:
content_length = int(self.headers['content-length'])
content = self.rfile.read(content_length).decode()
data = json.loads(content)
except ValueError as err:
self.send_error(400, 'bad json', str(err))
self.end_headers()
return
if type(data) is not dict or '_token' not in data:
self.send_error(401, 'missing api token')
self.end_headers()
return
if data.pop('_token') not in self.server.token:
self.send_error(401, 'invalid api token')
self.end_headers()
return
try:
self.server.handle(**data)
except Exception as err:
logging.error('{}: {}'.format(type(err).__name__, err))
self.send_error(400, 'bad parameters')
else:
self.send_response(201, 'value received')
self.end_headers()
def log_error(self, format_, *args):
logging.warning('ip {}, {}'.format(self.address_string(),
format_ % args))
def log_message(self, format_, *args):
pass
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
pass
class ApiError(Exception):
pass