Learn occupancy zones from radar coordinates. No zones to draw, no walls to configure by hand.
I write software for a living, so the YAML never bothered me. What did bother me was the step before that: deciding where the sensor goes, then drawing zones I had to redraw every time someone moved a chair.
So I stopped configuring it. I let a 24 GHz radar log target coordinates in my living room for four days and looked at the density. With no input at all, the sofa, my chair and my wife's chair came out as separate zones — and the two sofa seats 0.5 m apart resolved on their own.
204,796 coordinate pairs, four days, zero configuration:
zone share centre x centre y
1 23.8% +0.03 1.21 desk chair (mine)
2 18.0% -0.81 3.88 sofa, near end
3 10.5% -0.66 1.81 desk chair (my wife's)
4 3.2% -1.26 4.38 sofa, far corner
Zones 2 and 4 are the same sofa. They stay separate down to a 3% threshold because there is a real density valley between the cushions.
4.5m .. .---.
4.2m .-++.
4.0m . ..---.. ..
3.8m . ...---.##... ..-.. <- sofa
3.5m ......-. ...
3.0m .-..
2.0m ....
1.8m .-++-.. <- her chair
1.2m ..-+#-..
1.0m ...##-.... <- my chair
0.5m ..--..
0.0m .-.
^ sensor
Nobody drew these.
Radar goes through drywall. A sensor near a wall reports the neighbours, and no sensitivity setting removes them cleanly — I tried. What does work is three measurements:
python3 zonelearn.py --csv coords.csv --x-min -3000 --x-max 1000
# 204,796 coordinate pairs — 12,214 (6.0%) outside the room, droppedThat is the whole configuration. Zones stay automatic; only the walls are told once.
# straight from Home Assistant's own database — nothing to export first
python3 zonelearn.py --ha-db ~/.homeassistant/home-assistant_v2.db --days 3
# from a CSV with x,y columns (millimetres by default; use --scale for other units)
python3 zonelearn.py --csv coords.csv
# a PostgreSQL recorder instead of the default SQLite one
python3 zonelearn.py --ha-recorder sensor.living_target_1 --days 3 --dsn "$DSN"With --ha-db the sensors are found for you — the first <prefix>_x / <prefix>_y
target pair in the database wins, and --entity overrides it. (0, 0) means the radar
saw nobody, so those readings are dropped before anything else; on a real recorder that
is most of the day.
| flag | what it does |
|---|---|
--cell |
grid size in mm (default 250) |
--frac |
keep cells above this share of the busiest cell (default 0.10) |
--x-min --x-max --y-max |
wall distances from the sensor, in mm |
--scale |
multiply input by this to get millimetres |
--days |
only the last N days |
--entity |
entity prefix for --ha-db, when auto-detection picks wrong |
--keep-origin |
keep (0,0) readings instead of discarding them |
--emit |
also print zone config: esphome, ha, or both |
No dependencies for CSV or SQLite — that is the standard library. psycopg2 only for
--ha-recorder. Home Assistant OS keeps the database at /config/home-assistant_v2.db;
copy it out before reading if you like, though opening it read-only is safe.
Finding the rectangle is only half of it. --emit writes the other half:
python3 zonelearn.py --ha-db /config/home-assistant_v2.db --emit both# --- Home Assistant: Developer Tools > Actions, YAML mode ---
- action: number.set_value
target:
entity_id: number.ld2450_zone_1_x1
data:
value: -1750ESPHome's ld2450 component takes three zones, so if more are found the three busiest
are written and the rest are left for you to merge with a higher --frac.
Anything that reports 2-D target coordinates. Coordinates are normalised to millimetres before anything else runs, so the sensor is an input, not the design.
Tested with HLK-LD2450 (24 GHz, 3 targets, x/y/speed) via ESPHome. Should work with any source that gives per-target x and y — LD6002B, exports from commercial sensors, or your own.
- Two dimensions. A sitting person and a standing person at the same spot are one zone.
- A flat threshold does not separate everything. At 10% it finds four zones here. Drop it to 3% and a chair 0.7 m from another chair merges into it, while the two sofa seats still stay apart. A density-valley method (watershed) would do better; this does not do that yet.
- Zones have no names. It finds where people stop, not what the furniture is.
- Walls are not learned. They cannot be — a wall is where points should never appear, and that is not visible in the data. Hence the three numbers.
- Four days of one room is one sample. If you run it, I would like to see what comes out.
MIT.