motion detection example home assistant

This commit is contained in:
Christos Falas 2025-05-02 01:12:05 +01:00
parent 6f454c6190
commit 66c893b50b
No known key found for this signature in database
4 changed files with 785 additions and 560 deletions

View File

@ -3,7 +3,6 @@ This example shows how to use the CSI framework to connect to a FeitCSI host and
detect changes in the environment
"""
import logging
from queue import Queue
import numpy as np
@ -16,37 +15,46 @@ from where_fi.collection.ingest import RealtimeCSIProducer
producer = RealtimeCSIProducer()
app = CSIApplication(producer)
# Store historical CSI data to compare with the current data
# This allows us to detect sudden changes in the environment,
# most likely to be cause by motion
# This is stored individually for each receiving antenna, for each subcarrier
# Stores historical data for each receiving antenna, for each subcarrier
historical: dict[tuple[int, int], Queue[np.complex64]] = {}
MAGN_THRESHOLD = 20
PHASE_THRESHOLD = 0.5
QUEUE_SIZE = 2000
@app.on_process
def _(antenna_data: npt.NDArray[np.complex64]) -> None:
for antenna in range(antenna_data.shape[1]):
for subcarrier in range(antenna_data.shape[0]):
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 = antenna_data[subcarrier, antenna, 0]
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=100)
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
historical_data = historical[(antenna, subcarrier)]
# Calculate the average of the historical data
mean: np.complex64 = np.mean(historical_data.queue)
# 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 data to the historical data
# Add the current sample to the historical data
historical_data.put(current)
# Compare the current data with the historical data
@ -54,11 +62,11 @@ def _(antenna_data: npt.NDArray[np.complex64]) -> None:
np.abs(mean - current) > MAGN_THRESHOLD
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
):
logging.info(
f"Change detected on antenna {antenna}, subcarrier {subcarrier} "
f"|{np.abs(mean - current)}| <{np.angle(mean) - np.angle(current)}>"
)
change = True
if change:
print("Motion detected!")
else:
print("No motion detected!")
# Start the application
app.start()

View File

@ -0,0 +1,109 @@
"""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()

View File

@ -15,6 +15,7 @@ dependencies = [
"pydantic>=2.10.6",
"torch",
"grpcio>=1.70.0",
"requests>=2.32.3",
]
[build-system]
@ -54,4 +55,5 @@ dev = [
"grpcio-tools>=1.70.0",
"matplotlib-stubs>=0.1.0",
"protoletariat>=3.3.9",
"types-requests>=2.32.0.20250328",
]

1190
uv.lock

File diff suppressed because it is too large Load Diff