Files

184 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import threading
import time
import redis
import pickle
import os
import subprocess
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # Python 3.9+
from bleak import BleakScanner, BleakClient
from typing import Set, Iterable
from config import *
from sensors import *
from ble import *
# In-memory cache + thread-safe lock
_mac_cache: Set[str] = set()
_lock = threading.Lock()
_sensors_cache = []
# ----------------------------------------------------------------------
# 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)
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", [])
print("[Scanner] Loaded state from", STATE_FILE)
for cached_mac in _mac_cache:
print(f"Found {cached_mac}")
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)
def update_redis(data):
# 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 very important function updates the MAC cache and the sensor objects
def update_cache() -> None:
# Run the async scan in a fresh loop; Bleak requires this.
# Gets the MACs
print("")
print("--- Starting update_cache loop ---")
new_macs = scan_macs()
# now that we have MACs, let's get sensor data
print("Updating sensor data...")
sensor_data = None
sensor_data = sensor_data_query(new_macs)
# print(sensor_data)
# now we update the data in the sensor objects
print("Sensor data collected:")
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())
save_state()
#def scan_macs():
# print("Scanning for MACs:")
# new_macs = asyncio.run(_scan_once())
# with _lock:
# for new_mac in new_macs:
# print(f"{new_mac}")
# new_sensor = Sensor(name=f"{new_mac}")
# if new_mac not in set(_mac_cache):
# _sensors_cache.append(new_sensor)
# _mac_cache.update(new_macs)
# print(f"Found {len(new_macs)} new MACs, total {len(_mac_cache)} MACs in cache.")
# return new_macs
# 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)
# 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}")
sensor_dict = sensor.to_dict()
new_sensor = {
"name": sensor.name,
"first_seen": first_seen,
"last_seen": last_seen,
"comment": sensor.comment,
**sensor_dict
}
sensor_list.append(new_sensor)
result = {
"sensors": sensor_list
}
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
}
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"))
return pacific_dt.strftime("%Y-%m-%d %H:%M:%S")
# subroutine to run a command, return stdout as array unless zero_only then return [0]
def run_command(cmd, zero_only=False, use_shell=True, req_check = True):
# Run the command and capture the output
result = subprocess.run(cmd, shell=use_shell, check=req_check, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Decode the byte output to a string
output = result.stdout.decode('utf-8')
# Split the output into lines and store it in an array
output_lines = [line for line in output.split('\n') if line]
# Return result
try:
return output_lines[0] if zero_only else output_lines
except:
return output_lines