redis beta 1
This commit is contained in:
1 parent
63102b7740
commit
b23b52ed2e
3 files changed
+34
-27
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Summary
|
## 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:
|
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.
|
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
|
* Friendly Names for Sensors
|
||||||
* There will be a variable added to the `config.py` file to pre-configure friendly names for specific MAC addresses.
|
* 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*
|
* 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/
|
ble-scanner/
|
||||||
├─ app.py # Flask + Scheduler + API
|
├─ app.py # Flask + Scheduler + API
|
||||||
├─ ble.py # BLE helper functions to read temp+humidity data
|
├─ 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
|
├─ scanner.py # Async BLE scan + cache logic
|
||||||
├─ config.py # Shared constants (prefix, interval, etc.)
|
├─ config.py # Shared constants (prefix, interval, etc.)
|
||||||
├─ requirements.txt # Python VENV requirements
|
├─ requirements.txt # Python VENV requirements
|
||||||
|
|||||||
+1
-2
@@ -6,8 +6,7 @@ from config import *
|
|||||||
####################################################
|
####################################################
|
||||||
### Redis Functions
|
### 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):
|
def update_redis(data):
|
||||||
# push data to redis
|
# push data to redis
|
||||||
|
|||||||
+32
-21
@@ -20,44 +20,60 @@ _sensors_cache = []
|
|||||||
MI_SERVICE_UUID = "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"
|
MI_SERVICE_UUID = "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"
|
||||||
TEMP_CHAR_UUID = "ebe0ccc1-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:
|
def _is_match(mac: str, name: str) -> bool:
|
||||||
"""Return True if the MAC starts with the configured prefix and is correct model."""
|
"""Return True if the MAC starts with the configured prefix and is correct model."""
|
||||||
if name == "LYWSD03MMC":
|
if name == "LYWSD03MMC":
|
||||||
return mac.upper().startswith(MAC_PREFIX.upper())
|
return mac.upper().startswith(MAC_PREFIX.upper())
|
||||||
|
|
||||||
|
# this function scans for MACs
|
||||||
async def _scan_once() -> Iterable[str]:
|
async def _scan_once() -> Iterable[str]:
|
||||||
"""Do one scan cycle and return the matching MACs."""
|
"""Do one scan cycle and return the matching MACs."""
|
||||||
devices = await BleakScanner.discover(timeout=5.0) # 5s scan
|
devices = await BleakScanner.discover(timeout=5.0) # 5s scan
|
||||||
return [dev.address for dev in devices if _is_match(dev.address, dev.name)]
|
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:
|
def update_cache() -> None:
|
||||||
"""
|
"""
|
||||||
Called by the scheduler (in a separate thread). It runs a tiny
|
Called by the scheduler (in a separate thread). It runs a tiny
|
||||||
asyncio event loop to call the Bleak scan, then updates the cache.
|
asyncio event loop to call the Bleak scan, then updates the cache.
|
||||||
"""
|
"""
|
||||||
# Run the async scan in a fresh loop; Bleak requires this.
|
# Run the async scan in a fresh loop; Bleak requires this.
|
||||||
|
# Gets the MACs
|
||||||
new_macs = scan_macs()
|
new_macs = scan_macs()
|
||||||
print(f"[Scanner] Updated cache, {len(_mac_cache)} unique MAC(s).")
|
print(f"[Scanner] Updated cache, {len(_mac_cache)} unique MAC(s).")
|
||||||
print("Updating data for current MACs...")
|
print("Updating data for current MACs...")
|
||||||
|
# now that we have MACs, let's get sensor data
|
||||||
sensor_data = None
|
sensor_data = None
|
||||||
try:
|
try:
|
||||||
#asyncio.run(update_data())
|
|
||||||
sensor_data = sensor_data_query(new_macs)
|
sensor_data = sensor_data_query(new_macs)
|
||||||
print(sensor_data)
|
print(sensor_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Aww fuck, no data :(- {e}")
|
print(f"Aww fuck, no data :(- {e}")
|
||||||
|
# now we update the data in the sensor objects
|
||||||
for sensor_datum in sensor_data:
|
for sensor_datum in sensor_data:
|
||||||
print(f"MAC - {sensor_datum['name']} | temp - {sensor_datum['temp']} | humidity - {sensor_datum['humidity']}")
|
print(f"MAC - {sensor_datum['name']} | temp - {sensor_datum['temp']} | humidity - {sensor_datum['humidity']}")
|
||||||
for sensor in _sensors_cache:
|
for sensor in _sensors_cache:
|
||||||
if sensor.name == sensor_datum['name']:
|
if sensor.name == sensor_datum['name']:
|
||||||
sensor.add_or_update(key="temp", value=sensor_datum['temp'])
|
sensor.add_or_update(key="temp", value=sensor_datum['temp'])
|
||||||
sensor.add_or_update(key="humidity", value=sensor_datum['humidity'])
|
sensor.add_or_update(key="humidity", value=sensor_datum['humidity'])
|
||||||
|
# if redis is enabled, push full details to the redis server
|
||||||
if enable_redis:
|
if enable_redis:
|
||||||
update_redis(get_sensor_details())
|
update_redis(get_sensor_details())
|
||||||
|
|
||||||
|
# this helper function scans for MACs
|
||||||
def scan_macs():
|
def scan_macs():
|
||||||
new_macs = asyncio.run(_scan_once())
|
new_macs = asyncio.run(_scan_once())
|
||||||
with _lock:
|
with _lock:
|
||||||
@@ -70,58 +86,53 @@ def scan_macs():
|
|||||||
_mac_cache.update(new_macs)
|
_mac_cache.update(new_macs)
|
||||||
return new_macs
|
return new_macs
|
||||||
|
|
||||||
def update_data():
|
# return just the MACs
|
||||||
for mac in set(_mac_cache):
|
|
||||||
print(f"Test - {mac}")
|
|
||||||
result = asyncio.run(read_data(mac))
|
|
||||||
|
|
||||||
def get_cache_snapshot() -> Set[str]:
|
def get_cache_snapshot() -> Set[str]:
|
||||||
"""Return a thread-safe snapshot of the current cache."""
|
"""Return a thread-safe snapshot of the current cache."""
|
||||||
with _lock:
|
with _lock:
|
||||||
return set(_mac_cache)
|
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 = []
|
sensor_list = []
|
||||||
|
|
||||||
for sensor in _sensors_cache:
|
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}")
|
print(f"Summary - {sensor.name}")
|
||||||
new_sensor = {
|
new_sensor = {
|
||||||
"name": sensor.name,
|
"name": sensor.name,
|
||||||
# "first_seen": ts_to_pst_string(sensor.first_seen),
|
"first_seen": first_seen,
|
||||||
# "last_seen": ts_to_pst_string(sensor.last_seen),
|
"last_seen": last_seen,
|
||||||
"first_seen": sensor.first_seen,
|
|
||||||
"last_seen": sensor.last_seen,
|
|
||||||
"comment": sensor.comment,
|
"comment": sensor.comment,
|
||||||
"data": sensor.to_dict()
|
"data": sensor.to_dict()
|
||||||
|
|
||||||
}
|
}
|
||||||
sensor_list.append(new_sensor)
|
sensor_list.append(new_sensor)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"sensors": sensor_list
|
"sensors": sensor_list
|
||||||
}
|
}
|
||||||
# print(result)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# return just sensor data without timestamps
|
||||||
def get_sensor_summary():
|
def get_sensor_summary():
|
||||||
sensor_list = []
|
sensor_list = []
|
||||||
|
|
||||||
for sensor in _sensors_cache:
|
for sensor in _sensors_cache:
|
||||||
print(f"Summary - {sensor.name}")
|
print(f"Summary - {sensor.name}")
|
||||||
new_sensor = {
|
new_sensor = {
|
||||||
"name": sensor.name,
|
"name": sensor.name,
|
||||||
"comment": sensor.comment,
|
"comment": sensor.comment,
|
||||||
"data": sensor.to_dict(simple_return=True)
|
"data": sensor.to_dict(simple_return=True)
|
||||||
|
|
||||||
}
|
}
|
||||||
sensor_list.append(new_sensor)
|
sensor_list.append(new_sensor)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"sensors": sensor_list
|
"sensors": sensor_list
|
||||||
}
|
}
|
||||||
# print(result)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# helper function for ts int to string
|
||||||
def ts_to_pst_string(ts_str):
|
def ts_to_pst_string(ts_str):
|
||||||
utc_dt = datetime.fromtimestamp(ts_str, tz=timezone.utc)
|
utc_dt = datetime.fromtimestamp(ts_str, tz=timezone.utc)
|
||||||
pacific_dt = utc_dt.astimezone(ZoneInfo("America/Los_Angeles"))
|
pacific_dt = utc_dt.astimezone(ZoneInfo("America/Los_Angeles"))
|
||||||
|
|||||||
Reference in new issue
Block a user