From 0e58f6b73a41a9c8922723091993948e37431972 Mon Sep 17 00:00:00 2001 From: matt Date: Tue, 13 Oct 2026 03:54:51 -0700 Subject: [PATCH] pickle test 2 from intern --- README.md | 6 +----- app.py | 8 ++++++-- scanner.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++--- sensors.py | 2 +- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 27d70f6..491ff99 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Summary -This is an API for monitoring Temperature and Humidity data for Xiaomii LYWSD03MMC BLE sensors. The app will scan for any of these devices by looking for the Bluetooth MAC octet and then filtering by model. Once it has a list of MACs, it will then attempt to read the Temp/Humidity data for each MAC and record it. This data can be interacted with through an API. +This is an API for monitoring Temperature and Humidity data for Xiaomii LYWSD03MMC BLE sensors. The app will scan for any of these devices by looking for the Bluetooth MAC octet and then filtering by model. Once it has a list of MACs, it will then attempt to read the Temp/Humidity data for each MAC and record it. This data can be interacted with through an API. The app has a Redis publishing function for use with websockets. The comment field for sensors can be configured using the appropriate variable in the `config.py` as well. Some product info can be found here at my Knowledgebase: @@ -12,10 +12,6 @@ https://kb.matt-cloud.com/books/07-saved-articles/page/xiaomii-temperaturehumidi Aside from a REST API, there are a few more features which need to be added to this app. -* Friendly Names for Sensors - * There will be a variable added to the `config.py` file to pre-configure friendly names for specific MAC addresses. - * For example, *Living Room* or *Main Bedroom* - * This data will also be integrated into the API to replace the **comment** field * Data persistence across app launches * I will do this with the **Pickle** package diff --git a/app.py b/app.py index e6d6191..2ae86fc 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,15 @@ import redis +import atexit from flask import Flask, jsonify from flask_apscheduler import APScheduler from datetime import datetime, timedelta -from scanner import update_cache, get_cache_snapshot, get_sensor_summary, get_sensor_details, scan_macs +from scanner import * from config import * from ble_redis import * import logging logging.getLogger('apscheduler').setLevel(logging.ERROR) + app = Flask(__name__) scheduler = APScheduler() scheduler.init_app(app) @@ -54,4 +56,6 @@ def hello(): # --- Run -------------------------------------------------------------- if __name__ == '__main__': # The scheduler runs in its own thread; the main thread serves Flask. - app.run(host=bind_ip, port=api_port, debug=False) \ No newline at end of file + app.run(host=bind_ip, port=api_port, debug=False) + # Register a graceful shutdown hook so the scanner state is persisted + atexit.register(save_state) \ No newline at end of file diff --git a/scanner.py b/scanner.py index b45fc5b..50288d5 100644 --- a/scanner.py +++ b/scanner.py @@ -2,6 +2,8 @@ import asyncio import threading import time import redis +import pickle +import os from datetime import datetime, timezone from zoneinfo import ZoneInfo # Python 3.9+ from bleak import BleakScanner, BleakClient @@ -16,9 +18,53 @@ _mac_cache: Set[str] = set() _lock = threading.Lock() _sensors_cache = [] -# other vars -MI_SERVICE_UUID = "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6" -TEMP_CHAR_UUID = "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6" +# ---------------------------------------------------------------------- +# Pickle persistence helpers +# ---------------------------------------------------------------------- +STATE_FILE = "scanner_state.pkl" + +def save_state() -> None: + """ + Persist the MAC cache and sensor objects to disk. + Called automatically after each cache refresh and at program exit. + """ + with _lock: + state = { + "mac_cache": _mac_cache, + "sensors": _sensors_cache, + } + try: + with open(STATE_FILE, "wb") as f: + pickle.dump(state, f, protocol=pickle.HIGHEST_PROTOCOL) + if debug_output: + print("[Scanner] State persisted to", STATE_FILE) + except Exception as exc: + # Don't crash the app – just log the error + print("[Scanner] Warning: could not persist state:", exc) + +def load_state() -> None: + """ + Load the MAC cache and sensor objects from disk if the file exists. + Called automatically on import. + """ + if not os.path.exists(STATE_FILE): + return + try: + with open(STATE_FILE, "rb") as f: + state = pickle.load(f) + with _lock: + # NOTE: we replace the lists/sets to avoid mutating old references + _mac_cache.clear() + _mac_cache.update(state.get("mac_cache", [])) + global _sensors_cache + _sensors_cache = state.get("sensors", []) + if debug_output: + print("[Scanner] Loaded state from", STATE_FILE) + except Exception as exc: + print("[Scanner] Warning: could not load state:", exc) + +# Load any persisted state on module import +load_state() # Redis Functions r = redis.Redis(host=bind_ip, port=redis_port) @@ -72,6 +118,7 @@ def update_cache() -> None: # if redis is enabled, push full details to the redis server if enable_redis: update_redis(get_sensor_details()) + save_state() # this helper function scans for MACs def scan_macs(): diff --git a/sensors.py b/sensors.py index 2fcba38..ef790d3 100644 --- a/sensors.py +++ b/sensors.py @@ -71,7 +71,7 @@ class Sensor: return is_valid # ----------------------------------------------------------------------- # - # Life‑cycle hooks + # Life-cycle hooks # ----------------------------------------------------------------------- # def __post_init__(self) -> None: