motion detection example home assistant
This commit is contained in:
parent
6f454c6190
commit
66c893b50b
@ -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
|
detect changes in the environment
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -16,37 +15,46 @@ from where_fi.collection.ingest import RealtimeCSIProducer
|
|||||||
producer = RealtimeCSIProducer()
|
producer = RealtimeCSIProducer()
|
||||||
app = CSIApplication(producer)
|
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]] = {}
|
historical: dict[tuple[int, int], Queue[np.complex64]] = {}
|
||||||
|
|
||||||
MAGN_THRESHOLD = 20
|
MAGN_THRESHOLD = 20
|
||||||
PHASE_THRESHOLD = 0.5
|
PHASE_THRESHOLD = 0.5
|
||||||
|
QUEUE_SIZE = 2000
|
||||||
|
|
||||||
|
|
||||||
@app.on_process
|
@app.on_process
|
||||||
def _(antenna_data: npt.NDArray[np.complex64]) -> None:
|
def _(sample: npt.NDArray[np.complex64]) -> None:
|
||||||
for antenna in range(antenna_data.shape[1]):
|
"""
|
||||||
for subcarrier in range(antenna_data.shape[0]):
|
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
|
# 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
|
# Get the historical data for this antenna and subcarrier
|
||||||
if (antenna, subcarrier) not in historical:
|
if (antenna, subcarrier) not in historical:
|
||||||
historical[(antenna, subcarrier)] = Queue(maxsize=100)
|
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
|
||||||
historical_data = historical[(antenna, subcarrier)]
|
historical_data = historical[(antenna, subcarrier)]
|
||||||
|
|
||||||
# Calculate the average of the historical data
|
# Calculate the average of the historical data for this antenna and
|
||||||
mean: np.complex64 = np.mean(historical_data.queue)
|
# subcarrier
|
||||||
|
mean = np.mean(historical_data.queue)
|
||||||
|
|
||||||
# If we have enough historical data, compare it with the current data
|
# If we have enough historical data, compare it with the current data
|
||||||
if historical_data.full():
|
if historical_data.full():
|
||||||
historical_data.get()
|
historical_data.get()
|
||||||
|
|
||||||
# Add the current data to the historical data
|
# Add the current sample to the historical data
|
||||||
historical_data.put(current)
|
historical_data.put(current)
|
||||||
|
|
||||||
# Compare the current data with the historical data
|
# 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
|
np.abs(mean - current) > MAGN_THRESHOLD
|
||||||
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
|
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
|
||||||
):
|
):
|
||||||
logging.info(
|
change = True
|
||||||
f"Change detected on antenna {antenna}, subcarrier {subcarrier} "
|
if change:
|
||||||
f"|{np.abs(mean - current)}| <{np.angle(mean) - np.angle(current)}>"
|
print("Motion detected!")
|
||||||
)
|
else:
|
||||||
|
print("No motion detected!")
|
||||||
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
app.start()
|
app.start()
|
||||||
|
|||||||
109
examples/motion_detector_home_assistant.py
Normal file
109
examples/motion_detector_home_assistant.py
Normal 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()
|
||||||
@ -15,6 +15,7 @@ dependencies = [
|
|||||||
"pydantic>=2.10.6",
|
"pydantic>=2.10.6",
|
||||||
"torch",
|
"torch",
|
||||||
"grpcio>=1.70.0",
|
"grpcio>=1.70.0",
|
||||||
|
"requests>=2.32.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
@ -54,4 +55,5 @@ dev = [
|
|||||||
"grpcio-tools>=1.70.0",
|
"grpcio-tools>=1.70.0",
|
||||||
"matplotlib-stubs>=0.1.0",
|
"matplotlib-stubs>=0.1.0",
|
||||||
"protoletariat>=3.3.9",
|
"protoletariat>=3.3.9",
|
||||||
|
"types-requests>=2.32.0.20250328",
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user