"""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()