dissertation/examples/motion_detector_home_assistant.py
2025-05-12 01:44:49 +01:00

111 lines
3.3 KiB
Python

"""Motion Detection example
This example shows how to use the CSI framework to connect to a FeitCSI host and
detect changes in the environment
Once motion is detected, the application will log the change to Home Assistant
through an HTTP request.
"""
import os
from queue import Queue
import numpy as np
import numpy.typing as npt
import requests
from where_fi.application import CSIApplication
from where_fi.collection.ingest import RealtimeCSIProducer
# Connect to a FeitCSI host
producer = RealtimeCSIProducer()
app = CSIApplication(producer)
class HomeAssistantBinarySensor:
"""
Represents a binary sensor in Home Assistant.
Uses the HTTP API [1] to update the state of the sensor.
[1] - https://www.home-assistant.io/integrations/http/#binary-sensor
"""
def __init__(self, id: str, name: str) -> None:
self.id = id
self.name = name
BASE_URL = os.getenv("HOME_ASSISTANT_URL")
API_KEY = os.getenv("HOME_ASSISTANT_API_KEY")
self.url = f"{BASE_URL}/api/states/binary_sensor.{self.id}"
self.headers = {"Authorization": f"Bearer {API_KEY}"}
self.state = False
def update(self, state: bool) -> None:
if self.state == state:
return
self.state = state
data = {
"state": "on" if state else "off",
"attributes": {"friendly_name": self.name, "device_class": "motion"},
}
print(f"Updating sensor {self.name} to {data}")
requests.post(self.url, json=data, headers=self.headers)
# Stores historical data for each receiving antenna, for each subcarrier
historical: dict[tuple[int, int], Queue[np.complex64]] = {}
sensor = HomeAssistantBinarySensor("motion_detector", "Room Motion Detector")
MAGN_THRESHOLD = 20
PHASE_THRESHOLD = 0.5
QUEUE_SIZE = 2000
@app.on_process
def _(sample: npt.NDArray[np.complex64]) -> None:
"""
Process the CSI data and detect changes in the environment.
This function is called by the framework at a fixed interval, with the latest CSI
sample received.
It is used to detect changes in the environment caused by motion, by comparing each
entry in the matrix with a moving average
"""
change = False
for antenna in range(sample.shape[1]):
for subcarrier in range(sample.shape[0]):
# Get the current subcarrier data
current = sample[subcarrier, antenna, 0]
# Get the historical data for this antenna and subcarrier
if (antenna, subcarrier) not in historical:
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
historical_data = historical[(antenna, subcarrier)]
# Calculate the average of the historical data for this antenna and
# subcarrier
mean = np.mean(historical_data.queue)
# If we have enough historical data, compare it with the current data
if historical_data.full():
historical_data.get()
# Add the current sample to the historical data
historical_data.put(current)
# Compare the current data with the historical data
if (
np.abs(mean - current) > MAGN_THRESHOLD
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
):
change = True
sensor.update(change)
app.start()