class Daemon: running = None thread = None storage = None config = None interface = None buffer = None buffer_expiration = None def __init__(self, backend, on_receive, on_receive_interval): self.backed = backend self.on_receive = on_receive self.on_receive_interval = on_receive_interval self.storage = Storage() if self.storage.fetch_status() != "disconnected": self.storage.update_status("disconnected") self.loop = asyncio.new_event_loop() self.converter = Converter() def start(self): self.running = True if self.thread is None: self.thread = Thread(target=self.run) if not self.thread.is_alive(): self.thread.start() def stop(self): self.log("Disconnecting") self.running = False if self.interface: self.interface.disconnect() while self.thread and self.thread.is_alive(): sleep(0.1) self.emit("disconnected") self.thread = None def run(self): self.storage = Storage() self.config = Config() self.interface = Wrapper() try: self.log("Connecting") self.retry(self.interface.connect) self.emit("connected") self.log("Connected") name = self.config.read("name") interval = float(self.config.read("rate")) version = self.config.read("version") session_id = self.storage.create_session(name, version) while self.running: begin = timer() data = self.retry(self.interface.read) if isinstance(data, str): if data in ["disconnected", "connected"]: self.disconnect() return raise Exception(data) else: self.log(json.dumps(data)) if data: data["session_id"] = session_id self.update(data, version) self.storage.store_measurement(data) measurement_runtime = timer() - begin sleep_time = interval - measurement_runtime if sleep_time > 0: sleep(sleep_time) except (KeyboardInterrupt, SystemExit): raise except: logging.exception(sys.exc_info()[0]) self.emit("log", traceback.format_exc()) self.emit("log-error") finally: self.disconnect() def disconnect(self): self.interface.disconnect() self.emit("disconnected") self.log("Disconnected") self.thread = None def update(self, data, version): format = Format(version) table = [] for name in format.table_fields: callback = getattr(format, name) table.append(callback(data)) graph = {} for name in format.graph_fields: if name == "timestamp": callback = getattr(format, name) value = callback(data) else: value = data[name] graph[name] = value graph = self.converter.convert(graph) if self.on_receive: if not self.buffer: self.buffer = [] data["timestamp"] = int(data["timestamp"]) self.buffer.append(data) execute = True if self.on_receive_interval: execute = False if not self.buffer_expiration or self.buffer_expiration <= time( ): execute = True self.buffer_expiration = time() + self.on_receive_interval if execute: payload = json.dumps(self.buffer) self.buffer = None payload_file = os.path.join( os.getcwd(), "on-receive-payload-%s.json") % time() with open(payload_file, "w") as file: file.write(payload) command = self.on_receive + " \"" + payload_file + "\"" subprocess.Popen(command, shell=True, env={}) self.emit("update", json.dumps({ "table": table, "graph": graph, })) def retry(self, callback): timeout = time() + 60 count = 10 reconnect = False while True: try: if reconnect: self.interface.disconnect() self.interface.connect() # noinspection PyUnusedLocal reconnect = False return callback() except (KeyboardInterrupt, SystemExit): raise except: count -= 1 logging.exception(sys.exc_info()[0]) if timeout <= time() or count <= 0: raise else: self.log("operation failed, retrying") self.emit("log", traceback.format_exc()) reconnect = True def emit(self, event, data=None): if event == "log": self.storage.log(data) elif event in [ "connecting", "connected", "disconnecting", "disconnected" ]: self.storage.update_status(event) self.backed.emit(event, data) def log(self, message): prefix = pendulum.now().format("YYYY-MM-DD HH:mm:ss") + " - " self.emit("log", prefix + message + "\n")
class Storage: sqlite = None schema_version = 2 def __init__(self): self.parameters = { "database": data_path + "/data.db", "isolation_level": None, } self.converter = Converter() def connect(self): connection = sqlite3.connect(**self.parameters) connection.row_factory = self.row_factory return connection def row_factory(self, cursor, row): dictionary = {} for index, column in enumerate(cursor.description): dictionary[column[0]] = row[index] return dictionary def init(self): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "SELECT name FROM sqlite_master WHERE type = 'table'") tables = [] for row in cursor.fetchall(): tables.append(row["name"]) schema_version = self.schema_version if "version" not in tables: cursor.execute("CREATE TABLE version (version INTEGER)") cursor.execute("INSERT INTO version VALUES (%s)" % self.schema_version) else: schema_version = int( cursor.execute("SELECT version FROM version").fetchone() ["version"]) if "status" not in tables: cursor.execute("CREATE TABLE status (status TEXT)") cursor.execute("INSERT INTO status VALUES ('disconnected')") if "logs" not in tables: cursor.execute(("CREATE TABLE logs (" "id INTEGER PRIMARY KEY," "message TEXT" ")")) if "measurements" not in tables: cursor.execute(("CREATE TABLE measurements (" "id INTEGER PRIMARY KEY," "name TEXT," "timestamp INTEGER," "voltage REAL," "current REAL," "power REAL," "temperature REAL," "data_plus REAL," "data_minus REAL," "mode_id INTEGER," "mode_name TEXT," "accumulated_current INTEGER," "accumulated_power INTEGER," "accumulated_time INTEGER," "resistance REAL," "session_id INTEGER" ")")) if "sessions" not in tables: cursor.execute(("CREATE TABLE sessions (" "id INTEGER PRIMARY KEY," "version TEXT," "name TEXT," "timestamp INTEGER" ")")) if schema_version == 1: logging.info( "migrating database to new version, this may take a while..." ) self.backup() cursor.execute( ("ALTER TABLE measurements ADD session_id INTEGER")) cursor.execute( "DELETE FROM measurements WHERE name = '' OR name IS NULL") query = cursor.execute( "SELECT name, MIN(timestamp) AS timestamp FROM measurements WHERE session_id IS NULL GROUP BY name ORDER BY MIN(id)" ) rows = query.fetchall() for row in rows: session_name = row["name"] cursor.execute( "INSERT INTO sessions (name, timestamp) VALUES (:name, :timestamp)", (session_name, row["timestamp"])) session_id = cursor.lastrowid cursor.execute( "UPDATE measurements SET session_id = :session_id WHERE name = :name", (session_id, session_name)) cursor.execute("UPDATE version SET version = 2") def store_measurement(self, data): if data is None: return columns = [] placeholders = [] values = [] for name, value in data.items(): columns.append(name) placeholders.append(":" + name) values.append(value) columns = ", ".join(columns) placeholders = ", ".join(placeholders) values = tuple(values) with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "INSERT INTO measurements (" + columns + ") VALUES (" + placeholders + ")", values) def destroy_measurements(self, session): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute("DELETE FROM measurements WHERE session_id = ?", (session, )) cursor.execute("DELETE FROM sessions WHERE id = ?", (session, )) def fetch_sessions(self): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() return cursor.execute( "SELECT * FROM sessions ORDER BY timestamp DESC").fetchall() def fetch_measurements_count(self, session): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "SELECT COUNT(id) AS count FROM measurements WHERE session_id = ?", (session, )) return int(cursor.fetchone()["count"]) def fetch_measurements(self, session, limit=None, offset=None): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() sql = "SELECT * FROM measurements WHERE session_id = ? ORDER BY timestamp ASC" if limit is None or offset is None: cursor.execute(sql, (session, )) else: cursor.execute(sql + " LIMIT ?, ?", (session, offset, limit)) items = cursor.fetchall() for index, item in enumerate(items): items[index] = self.converter.convert(item) return items def fetch_last_measurement_by_name(self, name): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "SELECT * FROM measurements WHERE name = ? ORDER BY timestamp DESC LIMIT 1", (name, )) return cursor.fetchone() def fetch_last_measurement(self): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "SELECT * FROM measurements ORDER BY timestamp DESC LIMIT 1") return cursor.fetchone() def get_selected_session(self, selected): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() if selected == "": session = cursor.execute( "SELECT * FROM sessions ORDER BY timestamp DESC LIMIT 1" ).fetchone() else: session = cursor.execute("SELECT * FROM sessions WHERE id = ?", (selected, )).fetchone() return session def log(self, message): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute("INSERT INTO logs (message) VALUES (?)", (message, )) def fetch_log(self): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute("SELECT message FROM logs") log = "" for row in cursor.fetchall(): log += row["message"] return log def clear_log(self): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "DELETE FROM logs WHERE id NOT IN (SELECT id FROM logs ORDER BY id DESC LIMIT 250)" ) def update_status(self, status): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute("UPDATE status SET status = ?", (status, )) def fetch_status(self): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute("SELECT status FROM status") return cursor.fetchone()["status"] def create_session(self, name, version): with closing(self.connect()) as sqlite: cursor = sqlite.cursor() cursor.execute( "INSERT INTO sessions (name, version, timestamp) VALUES (?, ?, ?)", (name, version, time())) return cursor.lastrowid def backup(self): path = self.parameters["database"] backup_path = "%s.backup-%s" % ( path, pendulum.now().format("YYYY-MM-DD_HH-mm-ss")) if os.path.exists(path): shutil.copy(path, backup_path)