110 lines
3.3 KiB
Python
110 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},
|
|
}
|
|
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", "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()
|