From b23b52ed2e1ede643ac2bed0fc6a8ee60ae2f485 Mon Sep 17 00:00:00 2001 From: matt Date: Tue, 13 Oct 2026 03:14:25 -0700 Subject: [PATCH] redis beta 1 --- README.md | 5 +---- ble_redis.py | 3 +-- scanner.py | 53 +++++++++++++++++++++++++++++++--------------------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 86270ed..27d70f6 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. Some product info can be found here at my Knowledgebase: @@ -12,8 +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. -* REDIS function for Node.js Websocket support - * In the end this will drive a web dashboard, and I want to be able to view live data on this dashboard * 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* @@ -182,7 +180,6 @@ The Python code is split across multiple files with different files having code ble-scanner/ ├─ app.py # Flask + Scheduler + API ├─ ble.py # BLE helper functions to read temp+humidity data -├─ ble-redis.py # Redis functions for Websocket support ├─ scanner.py # Async BLE scan + cache logic ├─ config.py # Shared constants (prefix, interval, etc.) ├─ requirements.txt # Python VENV requirements diff --git a/ble_redis.py b/ble_redis.py index 45de8fe..167e2b0 100644 --- a/ble_redis.py +++ b/ble_redis.py @@ -6,8 +6,7 @@ from config import * #################################################### ### Redis Functions #################################################### -if enable_redis: - r = redis.Redis(host=bind_ip, port=redis_port) +r = redis.Redis(host=bind_ip, port=redis_port) def update_redis(data): # push data to redis diff --git a/scanner.py b/scanner.py index 2397c50..536c86f 100644 --- a/scanner.py +++ b/scanner.py @@ -20,44 +20,60 @@ _sensors_cache = [] MI_SERVICE_UUID = "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6" TEMP_CHAR_UUID = "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6" +# Redis Functions +r = redis.Redis(host=bind_ip, port=redis_port) + +def update_redis(data): + # push data to redis + # Publish to the Redis channel that the WS server is listening on + r.publish('temp_hum', json.dumps(data)) + if debug_output: + print("=== Stats Redis Update ===") + print(json.dumps(data, indent=2)) + return True + +# this function filters MACs def _is_match(mac: str, name: str) -> bool: """Return True if the MAC starts with the configured prefix and is correct model.""" if name == "LYWSD03MMC": return mac.upper().startswith(MAC_PREFIX.upper()) - +# this function scans for MACs async def _scan_once() -> Iterable[str]: """Do one scan cycle and return the matching MACs.""" devices = await BleakScanner.discover(timeout=5.0) # 5s scan return [dev.address for dev in devices if _is_match(dev.address, dev.name)] +# this very important function updates the MAC cache and the sensor objects def update_cache() -> None: """ Called by the scheduler (in a separate thread). It runs a tiny asyncio event loop to call the Bleak scan, then updates the cache. """ # Run the async scan in a fresh loop; Bleak requires this. + # Gets the MACs new_macs = scan_macs() print(f"[Scanner] Updated cache, {len(_mac_cache)} unique MAC(s).") print("Updating data for current MACs...") + # now that we have MACs, let's get sensor data sensor_data = None try: - #asyncio.run(update_data()) sensor_data = sensor_data_query(new_macs) print(sensor_data) except Exception as e: print(f"Aww fuck, no data :(- {e}") - + # now we update the data in the sensor objects for sensor_datum in sensor_data: print(f"MAC - {sensor_datum['name']} | temp - {sensor_datum['temp']} | humidity - {sensor_datum['humidity']}") for sensor in _sensors_cache: if sensor.name == sensor_datum['name']: sensor.add_or_update(key="temp", value=sensor_datum['temp']) sensor.add_or_update(key="humidity", value=sensor_datum['humidity']) - + # if redis is enabled, push full details to the redis server if enable_redis: update_redis(get_sensor_details()) +# this helper function scans for MACs def scan_macs(): new_macs = asyncio.run(_scan_once()) with _lock: @@ -70,58 +86,53 @@ def scan_macs(): _mac_cache.update(new_macs) return new_macs -def update_data(): - for mac in set(_mac_cache): - print(f"Test - {mac}") - result = asyncio.run(read_data(mac)) - +# return just the MACs def get_cache_snapshot() -> Set[str]: """Return a thread-safe snapshot of the current cache.""" with _lock: return set(_mac_cache) -def get_sensor_details(): +# return sensor data and timestamps, can return int or string +def get_sensor_details(friendly_date = False): sensor_list = [] - for sensor in _sensors_cache: + first_seen = sensor.first_seen + last_seen = sensor.last_seen + if friendly_date: + first_seen = ts_to_pst_string(sensor.first_seen) + last_seen = ts_to_pst_string(sensor.last_seen) print(f"Summary - {sensor.name}") new_sensor = { "name": sensor.name, - # "first_seen": ts_to_pst_string(sensor.first_seen), - # "last_seen": ts_to_pst_string(sensor.last_seen), - "first_seen": sensor.first_seen, - "last_seen": sensor.last_seen, + "first_seen": first_seen, + "last_seen": last_seen, "comment": sensor.comment, "data": sensor.to_dict() } sensor_list.append(new_sensor) - result = { "sensors": sensor_list } - # print(result) return result +# return just sensor data without timestamps def get_sensor_summary(): sensor_list = [] - for sensor in _sensors_cache: print(f"Summary - {sensor.name}") new_sensor = { "name": sensor.name, "comment": sensor.comment, "data": sensor.to_dict(simple_return=True) - } sensor_list.append(new_sensor) - result = { "sensors": sensor_list } - # print(result) return result +# helper function for ts int to string def ts_to_pst_string(ts_str): utc_dt = datetime.fromtimestamp(ts_str, tz=timezone.utc) pacific_dt = utc_dt.astimezone(ZoneInfo("America/Los_Angeles"))