188 lines
6.3 KiB
Python
188 lines
6.3 KiB
Python
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):
|
||
# 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 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
|
||
try:
|
||
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
|
||
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 |