"""Motion Detection example This example shows how to use the CSI framework to connect to a FeitCSI host and detect changes in the environment """ import numpy as np import numpy.typing as npt import requests from where_fi.application import CSIApplication from where_fi.collection.ingest import RealtimeCSIProducer from where_fi.config import config from where_fi.visualise import server as visualise # Connect to a FeitCSI host app = CSIApplication(visualise_raw=True) # Register visualisations app.register_figure( "magn-diff", visualise.figures.PerAntennaFigure( [ visualise.figures.SimpleLineChart( "Magnitude diff", "Subcarrier", "Magnitude" ), ], [lambda x: x], ), ) app.register_figure( "phase-diff", visualise.figures.PerAntennaFigure( [ visualise.figures.SimpleLineChart("Phase diff", "Subcarrier", "Phase"), ], [lambda x: x], ), ) MAGN_THRESHOLD = 0.2 PHASE_THRESHOLD = 0.02 QUEUE_SIZE = 20 # Stores historical data for each receiving antenna, for each subcarrier historical = np.zeros( (QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64 ) sample_position = 0 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) sensor = HomeAssistantBinarySensor("motion_detector", "Room Motion Detector") @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 """ global sample_position historical[sample_position] = sample sample_position = (sample_position + 1) % QUEUE_SIZE mean = np.mean(historical, axis=0) magn_diff = np.abs(mean - sample) phase_diff = np.abs(np.angle(mean) - np.angle(sample)) app.visualise_data(magn_diff, "magn-diff") app.visualise_data(phase_diff, "phase-diff") if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any(): print("Motion detected!") sensor.update(True) else: print("No motion detected!") sensor.update(False) if __name__ == "__main__": # Start the application print("Starting app") app.set_producer(RealtimeCSIProducer()) app.start()