init commit
This commit is contained in:
commit
8567e9998a
9 files changed
+544
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
Gonna use the intern to build this temp sensing thing.
|
||||||
|
|
||||||
|
ble-scanner/
|
||||||
|
├─ app.py # Flask + Scheduler + API
|
||||||
|
├─ ble.py # BLE helper functions to read temp+humidity data
|
||||||
|
├─ scanner.py # Async BLE scan + cache logic
|
||||||
|
├─ config.py # Shared constants (prefix, interval, etc.)
|
||||||
|
├─ requirements.txt
|
||||||
|
├─ run.sh # Run API from shell
|
||||||
|
├─ scanner.py # Handles scanning for Mii sensors, other helper functions
|
||||||
|
├─ sensors.py # Object classifier
|
||||||
|
└─ README.md
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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 config import SCAN_INTERVAL_MINUTES
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
scheduler = APScheduler()
|
||||||
|
scheduler.init_app(app)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
|
||||||
|
scan_macs()
|
||||||
|
# --- Scheduler job ----------------------------------------------------
|
||||||
|
@scheduler.task(
|
||||||
|
'interval',
|
||||||
|
id='ble_scan',
|
||||||
|
minutes=SCAN_INTERVAL_MINUTES,
|
||||||
|
misfire_grace_time=30
|
||||||
|
)
|
||||||
|
def ble_scan_job():
|
||||||
|
"""Runs in the background, calls the async scanner."""
|
||||||
|
update_cache()
|
||||||
|
|
||||||
|
|
||||||
|
# --- API -------------------------------------------------------------
|
||||||
|
@app.route('/macs', methods=['GET'])
|
||||||
|
def list_macs():
|
||||||
|
"""Return the current set of MACs as JSON."""
|
||||||
|
macs = sorted(get_cache_snapshot())
|
||||||
|
return jsonify({"count": len(macs), "macs": macs})
|
||||||
|
|
||||||
|
@app.route('/details', methods=['GET'])
|
||||||
|
def sensor_detail():
|
||||||
|
"""Return the current set of MACs as JSON."""
|
||||||
|
sensors = get_sensor_details()
|
||||||
|
return jsonify(sensors)
|
||||||
|
|
||||||
|
@app.route('/summary', methods=['GET'])
|
||||||
|
def sensor_summary():
|
||||||
|
"""Return the current set of MACs as JSON."""
|
||||||
|
sensors = get_sensor_summary()
|
||||||
|
return jsonify(sensors)
|
||||||
|
|
||||||
|
# --- Convenience route -----------------------------------------------
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def hello():
|
||||||
|
return "BLE Scanner API - see /macs for the list."
|
||||||
|
|
||||||
|
# --- Run --------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The scheduler runs in its own thread; the main thread serves Flask.
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import asyncio
|
||||||
|
from bleak import BleakClient, BleakScanner
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
# Mi sensor UUIDs
|
||||||
|
MI_SERVICE_UUID = "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"
|
||||||
|
TEMP_CHAR_UUID = "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6"
|
||||||
|
|
||||||
|
# scanner constants
|
||||||
|
full_timeout = 60
|
||||||
|
attempts = 6
|
||||||
|
|
||||||
|
async def read_data(mac_address):
|
||||||
|
|
||||||
|
# Scan for the device
|
||||||
|
ratio = full_timeout / attempts
|
||||||
|
fail_count = full_timeout / ratio
|
||||||
|
device = None
|
||||||
|
print(f"Beginning read_data for {mac_address} - {full_timeout} timeout - {ratio} ratio - please stand by...")
|
||||||
|
while fail_count > 0 and not device:
|
||||||
|
# print(f"Starting discover loop {fail_count}")
|
||||||
|
devices = await BleakScanner.discover(timeout=ratio)
|
||||||
|
# print("BleakScanner discover timeout pass")
|
||||||
|
device = next((d for d in devices if d.address.lower() == mac_address.lower()), None)
|
||||||
|
if device:
|
||||||
|
# print(f"BleakScanner device variable loaded - {device.name}")
|
||||||
|
print(f"-- Found device: {device.name} ({device.address})")
|
||||||
|
fail_count = 0
|
||||||
|
# else:
|
||||||
|
# print("BleakScanner device not loaded yet")
|
||||||
|
fail_count = fail_count - 1
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
print(f"!! Device with MAC {mac_address} not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Connect to the device
|
||||||
|
mi_service = None
|
||||||
|
async with BleakClient(device) as client:
|
||||||
|
|
||||||
|
success = False
|
||||||
|
# Check if Mi service is available
|
||||||
|
|
||||||
|
print("?? Waiting on client to connect...")
|
||||||
|
while not client.is_connected:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
while not success:
|
||||||
|
try:
|
||||||
|
print("Client connected, getting services")
|
||||||
|
services = await client.get_services()
|
||||||
|
print("Services acquired, loading uuid matches")
|
||||||
|
mi_service = next((s for s in services if s.uuid.lower() == MI_SERVICE_UUID.lower()), None)
|
||||||
|
success = True
|
||||||
|
#print("uuid matches loaded")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Mi service unavailable, infinite loop looping RIP - {e}")
|
||||||
|
|
||||||
|
# Read temperature data
|
||||||
|
sensor_data = await client.read_gatt_char(TEMP_CHAR_UUID)
|
||||||
|
|
||||||
|
# Parse the temperature value (same logic as in JavaScript)
|
||||||
|
sign = sensor_data[1] & (1 << 7)
|
||||||
|
temp = ((sensor_data[1] & 0x7F) << 8 | sensor_data[0])
|
||||||
|
if sign:
|
||||||
|
temp = temp - 32767
|
||||||
|
temp = temp / 100
|
||||||
|
hum = sensor_data[2]
|
||||||
|
|
||||||
|
print(f"-- Temperature: {temp:.1f}°C, Humidity: {hum}%")
|
||||||
|
print("")
|
||||||
|
return temp, hum
|
||||||
|
|
||||||
|
def sensor_data_query(mac_list):
|
||||||
|
result = []
|
||||||
|
for mac in mac_list:
|
||||||
|
this_result = asyncio.run(read_data(mac))
|
||||||
|
result.append({"name": mac, "temp": this_result[0], "humidity": this_result[1]})
|
||||||
|
print(f"^^ Data query complete - returning result")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test():
|
||||||
|
test_result = False
|
||||||
|
sensors = []
|
||||||
|
mac_list = [
|
||||||
|
"A4:C1:38:11:F4:2A",
|
||||||
|
"A4:C1:38:24:15:B0",
|
||||||
|
"A4:C1:38:D7:8D:26"
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Starting BLE test...")
|
||||||
|
print(" Scanning MACs:")
|
||||||
|
for mac in mac_list:
|
||||||
|
print(f" {mac}")
|
||||||
|
test_list = mac_list
|
||||||
|
while not test_result:
|
||||||
|
print(" Test loop start. Initializing run...")
|
||||||
|
test_run_results = sensor_data_query(test_list)
|
||||||
|
for test_run_result in test_run_results:
|
||||||
|
sensors.append(test_run_result)
|
||||||
|
passed_list = [device['name'] for device in sensors]
|
||||||
|
print(f" Passed list - {passed_list}")
|
||||||
|
if len(mac_list) == len(passed_list):
|
||||||
|
print(" Test run passed, yee-haw")
|
||||||
|
test_result = True
|
||||||
|
else:
|
||||||
|
print(f" Test run incomplete/failed, re-running, updating test list")
|
||||||
|
mac_list_new = []
|
||||||
|
for mac in mac_list:
|
||||||
|
if mac not in passed_list:
|
||||||
|
mac_list_new.append(mac)
|
||||||
|
test_list = mac_list_new
|
||||||
|
print(f" Re-do list - {test_list}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
#print("Welp, this will keep going forever. Have fun :)")
|
||||||
|
#while True:
|
||||||
|
#try:
|
||||||
|
test()
|
||||||
|
#except Exception as e:
|
||||||
|
# print(f"Test failed brah - {e}")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# --- BLE & app config -------------------------------------------------
|
||||||
|
MAC_PREFIX = "A4:C1:38" # 3 octets (change to what you need)
|
||||||
|
SCAN_INTERVAL_MINUTES = 1 # how often to run the scan
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
anyio
|
||||||
|
APScheduler
|
||||||
|
bleak
|
||||||
|
cryptography
|
||||||
|
Flask
|
||||||
|
click
|
||||||
|
setuptools
|
||||||
|
|
||||||
|
Flask-APScheduler
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
anyio
|
||||||
|
bleak
|
||||||
|
cryptography
|
||||||
|
click
|
||||||
|
setuptools
|
||||||
|
Flask-APScheduler
|
||||||
|
Flask
|
||||||
|
APScheduler
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
# 3. Create a venv (optional but recommended)
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# 4. Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 5. Start
|
||||||
|
#python ble.py
|
||||||
|
python app.py
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
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 MAC_PREFIX
|
||||||
|
from sensors import *
|
||||||
|
from ble import *
|
||||||
|
|
||||||
|
# In-memory cache + thread-safe lock
|
||||||
|
_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"
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
||||||
|
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)]
|
||||||
|
|
||||||
|
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.
|
||||||
|
new_macs = scan_macs()
|
||||||
|
print(f"[Scanner] Updated cache, {len(_mac_cache)} unique MAC(s).")
|
||||||
|
print("Updating data for current MACs...")
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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'])
|
||||||
|
|
||||||
|
def scan_macs():
|
||||||
|
new_macs = asyncio.run(_scan_once())
|
||||||
|
with _lock:
|
||||||
|
print("MACs found:")
|
||||||
|
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)
|
||||||
|
return new_macs
|
||||||
|
|
||||||
|
def update_data():
|
||||||
|
for mac in set(_mac_cache):
|
||||||
|
print(f"Test - {mac}")
|
||||||
|
result = asyncio.run(read_data(mac))
|
||||||
|
|
||||||
|
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():
|
||||||
|
sensor_list = []
|
||||||
|
|
||||||
|
for sensor in _sensors_cache:
|
||||||
|
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,
|
||||||
|
"comment": sensor.comment,
|
||||||
|
"data": sensor.to_dict()
|
||||||
|
|
||||||
|
}
|
||||||
|
sensor_list.append(new_sensor)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"sensors": sensor_list
|
||||||
|
}
|
||||||
|
# print(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Helper utilities
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _current_timestamp() -> int:
|
||||||
|
"""Return the current UTC timestamp (seconds)."""
|
||||||
|
return int(datetime.now(timezone.utc).timestamp())
|
||||||
|
|
||||||
|
def _ensure_timestamp(ts: Optional[Union[int, datetime]]) -> int:
|
||||||
|
"""
|
||||||
|
Normalise a timestamp that can be an int (Unix seconds) or a datetime.
|
||||||
|
Raises ValueError if the value is not recognised.
|
||||||
|
"""
|
||||||
|
if ts is None:
|
||||||
|
return _current_timestamp()
|
||||||
|
if isinstance(ts, int):
|
||||||
|
return ts
|
||||||
|
if isinstance(ts, datetime):
|
||||||
|
return int(ts.replace(tzinfo=timezone.utc).timestamp())
|
||||||
|
raise ValueError(f"Unsupported timestamp type: {type(ts)}")
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Sensor implementation
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Sensor:
|
||||||
|
"""
|
||||||
|
Represents a sensor that records timestamped values under a set of keys.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
The unique identifier for the sensor itself.
|
||||||
|
first_seen : int
|
||||||
|
Unix timestamp of when the sensor was first instantiated.
|
||||||
|
last_seen : int
|
||||||
|
Unix timestamp of the most recent data update (or creation time).
|
||||||
|
data : List[Dict[str, Any]]
|
||||||
|
A list of data points, each with keys:
|
||||||
|
- name (str)
|
||||||
|
- value (Any)
|
||||||
|
- first_seen (int)
|
||||||
|
- last_seen (int)
|
||||||
|
"""
|
||||||
|
name: str
|
||||||
|
first_seen: int = field(init=False)
|
||||||
|
last_seen: int = field(init=False)
|
||||||
|
data: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
# Validity rules
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# Current rule: key must be a "word" (letters, digits, underscore)
|
||||||
|
VALID_NAME_RE = re.compile(r"^[A-Za-z0-9_]+$")
|
||||||
|
|
||||||
|
def _is_valid_key(self, key: str) -> bool:
|
||||||
|
"""Return True if the key meets current validity criteria."""
|
||||||
|
is_valid = False
|
||||||
|
if bool(self.VALID_NAME_RE.match(key)):
|
||||||
|
is_valid = True
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
# Life‑cycle hooks
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""Initialise timestamps when the object is created."""
|
||||||
|
print(f"Instantiating new sensor object - {self.name}")
|
||||||
|
ts = _current_timestamp()
|
||||||
|
self.first_seen = ts
|
||||||
|
self.last_seen = ts
|
||||||
|
self.comment = self.name
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
# Core API
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def add_or_update(self,
|
||||||
|
key: str,
|
||||||
|
value: Any,
|
||||||
|
timestamp: Optional[Union[int, datetime]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Add a new data point or update an existing one.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
key : str
|
||||||
|
The data key. Must pass :meth:`_is_valid_key`.
|
||||||
|
value : Any
|
||||||
|
The data value to store.
|
||||||
|
timestamp : int | datetime | None
|
||||||
|
When the data was recorded. If omitted, current time is used.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the key is not valid.
|
||||||
|
"""
|
||||||
|
if not self._is_valid_key(key):
|
||||||
|
raise ValueError(f"Invalid key '{key}'. Must contain only "
|
||||||
|
"letters, digits, or underscores.")
|
||||||
|
|
||||||
|
ts = _ensure_timestamp(timestamp)
|
||||||
|
|
||||||
|
# Update sensor-level timestamps
|
||||||
|
self.last_seen = max(self.last_seen, ts)
|
||||||
|
|
||||||
|
# Search for existing entry
|
||||||
|
for entry in self.data:
|
||||||
|
if entry["name"] == key:
|
||||||
|
# Update existing entry
|
||||||
|
entry["value"] = value
|
||||||
|
entry["last_seen"] = ts
|
||||||
|
if ts < entry["first_seen"]:
|
||||||
|
entry["first_seen"] = ts
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we get here, it's a new key
|
||||||
|
new_entry: Dict[str, Any] = {
|
||||||
|
"name": key,
|
||||||
|
"value": value,
|
||||||
|
"first_seen": ts,
|
||||||
|
"last_seen": ts
|
||||||
|
}
|
||||||
|
self.data.append(new_entry)
|
||||||
|
|
||||||
|
def get_value(self, key: str) -> Optional[Any]:
|
||||||
|
"""Return the most recent value for a key, or None if it doesn't exist."""
|
||||||
|
for entry in self.data:
|
||||||
|
if entry["name"] == key:
|
||||||
|
return entry["value"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_comment(self, new_comment: str):
|
||||||
|
self.comment = new_comment
|
||||||
|
|
||||||
|
def to_dict(self, simple_return = False) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
|
"""
|
||||||
|
Return the data in the exact structure requested.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
>>> sensor.to_dict()
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{"name": "temp", "value": 23, "first_seen": 1710000000, "last_seen": 1710000005},
|
||||||
|
{"name": "hum", "value": 45, "first_seen": 1710000010, "last_seen": 1710000010}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if simple_return:
|
||||||
|
simple_return = {
|
||||||
|
"temp": self.get_value(key="temp"),
|
||||||
|
"humidity": self.get_value(key="humidity")
|
||||||
|
}
|
||||||
|
return simple_return
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {"data": self.data.copy()}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
# Convenience & debugging
|
||||||
|
# ----------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (f"{self.__class__.__name__}(name={self.name!r}, "
|
||||||
|
f"first_seen={self.first_seen}, last_seen={self.last_seen}, "
|
||||||
|
f"data={self.data!r})")
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Example usage
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Instantiate a sensor
|
||||||
|
sensor = Sensor(name="weather_station_01")
|
||||||
|
|
||||||
|
# Add some data
|
||||||
|
sensor.add_or_update("temperature", 23.5)
|
||||||
|
sensor.add_or_update("humidity", 45)
|
||||||
|
|
||||||
|
# Update an existing key
|
||||||
|
sensor.add_or_update("temperature", 24.0)
|
||||||
|
|
||||||
|
# Try an invalid key (will raise ValueError)
|
||||||
|
try:
|
||||||
|
sensor.add_or_update("bad key!", 0)
|
||||||
|
except ValueError as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# Print the structured data
|
||||||
|
import json
|
||||||
|
print(json.dumps(sensor.to_dict(), indent=4))
|
||||||
Reference in new issue
Block a user