HouseFly runs a rate model of 4,724 identified Drosophila neurons, wired together by 126,108 measured synapses, inside Home Assistant. Your sensors go to the neurons that carry that kind of information in the animal. It walks across your dashboard, it startles when something comes at the camera, and โ because that is what a mushroom body is for โ it tells you when your house stops looking like your house.
A real Home Assistant with a fake house in it, signed in for you as a guest. Everything is a template on top of a toggle, so switch things on and off freely โ then drag the approach slider down and watch it bolt. Four of the switches exist only to be refused.
There is a tempting way to build something like this, and it is worth naming because avoiding it
is the only design decision that matters. You take a recurrent network, seed it with random weights,
call the shape "fly-inspired", and then hang the behaviour you actually want off the side of it in
plain if statements. It looks alive. The network could be deleted without changing a
single thing the user sees.
So the rule here is: every behaviour has to come out of the measured wiring, or be labelled as not coming out of it. No third option.
That rule is why this page is full of numbers, and why some of them are failures. The ring attractor is not asserted, it is measured out of the synapse counts. The escape threshold is not a constant someone picked, it is where the network sits. Where something genuinely is a controller and not biology โ goal seeking, wall avoidance, estimating dawn from the light โ it says so in the code and it says so here. And where the model does not do what it looked like it did, that is written down too, with the measurement that caught it:
Each of those is now a check that would fail if it were ever quietly fixed or quietly broken. The alternative โ a model that always agrees with its own documentation โ is the failure mode, not the goal.
The replacement is not a bigger network. It is a set of circuits that have a known function, each receiving the kind of signal it receives in the animal.
| Circuit | What it does in the fly | What it gets from your house |
|---|---|---|
| EPG / PEN / PEG / ฮ7 | Ring attractor holding heading | Integrates the fly's own turns into a compass |
| ER ring neurons | Visual bearing into the ellipsoid body | Bearing to every card on your dashboard |
| PFL3 | Compares heading to goal, commands a turn | Steering (see the caveat below) |
| KC โ MBON | Associative memory, real plasticity | Learns which parts of the house are good |
| PAM / PPL1 | Reward and punishment teaching signals | Feeding it, and swatting at it |
| LPLC2 โ DNp09/10 | Looming detection โ escape | Motion sensors firing; your cursor |
| Projection neurons | Odour identity | The state of your house, as a smell |
| s-LNv / LNd / DN1 | Circadian morning + evening oscillators | Real local time |
There is no rule anywhere that says be active at dawn. The morning cells are driven by morning, the evening cells by evening, and arousal is read off their firing rate. The fly is crepuscular because the circuit is.
This is the part worth showing people. Nobody wrote a ring attractor. The connectome contains one.
Take the effective EPG โ ฮ7 โ EPG coupling โ using only measured synapse counts and the protocerebral bridge glomerulus labels โ and bin it by heading offset:
python3 tools/build_connectome.py and it prints this from the raw data.The first version of this gave both halves of the protocerebral bridge the same angular order. That produced a much weaker 2.7:1 profile, and a compass that held a heading perfectly well but would only ever turn one way.
The real bridge is a mirror-symmetric double map. Correcting it sharpened the signature from 2.7:1 to 23:1. The connectome said the anatomy was wrong before a single neuron was simulated.
You do not have to assume. It is readable straight out of the synapse counts:
EPG โ LEFT PEN1 โ EPG loop shift: +12.5ยฐ
EPG โ RIGHT PEN1 โ EPG loop shift: โ11.5ยฐ
EPG โ LEFT PEG โ EPG loop shift: โ5.6ยฐ
EPG โ RIGHT PEG โ EPG loop shift: +0.9ยฐ
PEN loops shift the bump in opposite directions per hemisphere. PEG loops do not shift it at all. That is the textbook split between the loop that moves the bump and the loop that holds it โ and it fell out of the data.
Learning in Drosophila happens at Kenyon cell โ MBON synapses. A Kenyon cell active at the same moment as a dopaminergic neuron in that MBON's compartment gets depressed. That is the whole rule. It is anti-Hebbian, and it is why a fly stops approaching things that turned out to be bad.
HouseFly implements exactly that, on 20,391 real KCโMBON connections. Each MBON's compartment valence is derived from which dopaminergic class dominates its input, rather than hard-coded. Those gains are what persists across restarts โ the fly does not forget your house when Home Assistant updates.
Two things were expected to fall out of the wiring and did not. Both are worth stating plainly, because โwe tried to derive it and could notโ is a result.
The plan was to read the landmark-bearing โ heading mapping off the ER โ EPG connectivity. It isn't there: that projection is near-uniform, about 7% modulation depth, with no consistent phase.
This is not a gap in the data. In the real animal that map is learned, in plastic ERโEPG synapses (Fisher et al. 2019; Kim et al. 2019). A naive fly does not have one. So HouseFly doesn't fake it โ the compass integrates the fly's own turns, exactly as a real one does in the dark, and it drifts when it cannot see.
In the animal, PFL3's left-right imbalance is the turn command, because each cell's fan-shaped-body arbor sits about a quarter turn from its bridge arbor, in opposite directions per hemisphere.
We tried it both ways โ feeding the goal into the fan-shaped body and letting the connectome do the rest, and imposing the quarter-turn offset explicitly. Neither gave reliable goal-following: the correlation between turn command and the sine of the heading error ranged from +0.03 to โ0.54 depending on the trajectory, and the closed loop never converged. That offset is where the arbors physically sit, and synapse counts between cell types do not carry it.
So the goal-seeking controller lives in coordinator.py โ four lines of proportional
control, labelled as a controller. The fly holds a heading using a connectome-derived ring
attractor, which is real, and chooses which heading to hold using arithmetic, which is not.
Keeping those visibly separate matters more than having one more thing to claim.
The fake ten-room testbed was generated, tidy and metric. Pointed at a live installation instead โ Swedish, 30 lights, Bluetooth room-presence sensors, a power meter and an electricity price โ the sensory front end turned out to be destroying most of its input before a single neuron saw it.
Any state that wasn't numeric returned a constant 0.35. So
Allrum, Loft and Kitchen all arrived as
the same smell, and the mushroom body could not learn that one room differed
from another. The information was gone before it reached a neuron.
Words now pick a glomerulus rather than a magnitude โ which is how odour identity actually works: it is carried by which neurons respond, not how hard one does. With 131 glomeruli some states inevitably collide, as they do in a real fly with about 50 for an unbounded number of odours. That is resolved where the animal resolves it: two colliding inputs still produce clearly different Kenyon cell codes โ 67 and 39 cells active with 12% overlap โ because the projection-neuron-to-Kenyon-cell divergence in the connectome pulls them apart.
tanh(value / 60) put seven temperatures spanning 11โ25 ยฐC into a
0.2-wide band, pinned a 2,840 W power sensor flat at 1.0, and turned an
electricity price of 0.43 into 0.007. Two of those channels were constants and
the third had no resolution left.
Each channel now learns its own range and reports where the current value sits inside it โ receptor neurons adapt their gain to the stimulus range they actually receive, which is why you can see indoors and outdoors. It costs two floats per entity and they persist across restarts.
abs(value) meant โ15 ยฐC and +15 ยฐC were identical readings.
On a Swedish install, that sign is the signal.sun.sun reports above_horizon, which didn't parse
as a number โ so the sun was a constant.unknown and nothing said so. A sensor stuck
on unknown is not a smell of nothing; it is no smell at all.LPLC2 โ the population that drives escape โ fires at an object expanding in the visual field. For a target of size L at range r closing at speed v, the angular size is ฮธ โ L/r, so the expansion rate is ฮธฬ = Lยทv/rยฒ. That rยฒ is the whole character of the response: the same footsteps count for far more at one metre than at five, which is why a real fly leaves it so late and then goes all at once.
A ranging sensor โ mmWave radar, ultrasonic, BLE distance โ gives r directly and v by differencing, so it delivers exactly the quantity the circuit is built for, by radar instead of by photons. Someone walking in at 1 m/s, sampled at a range of distances:
Note the shape. Nothing at all until about 2.4 m, then the same full burst at every distance inside it. That is not a rounding artefact, it is the pathway: the giant-fibre escape is a trigger, so the interesting quantity is where the threshold falls, and the rยฒ is what puts it there. Halve the walking speed and the fly lets you get to 1.7 m instead; the response itself does not get any smaller.
The first version of these numbers was wrong in a way that flattered it. The model adds a small resting drive to every neuron, standing in for the parts of the brain that are not modelled โ and that drive and the firing threshold happened to be the same number, so the escape pathway sat exactly at its own threshold. Measured from rest, a looming input of 1e-5 produced escape 0.286 and an input of 1.25 produced 0.284. The same burst. ฮธฬ = v/rยฒ was being computed faithfully and then thrown away, and every claim on this page about the rยฒ was decorative.
The check that was supposed to catch this held the stimulus on for twelve steps, where a weak one simply adapts away โ but a real approach is a transient, and that is the case that mattered. DNp09 and its LPLC2 inputs are silent at rest in the animal, which is what makes them a trigger rather than a readout, so the resting drive is now withheld from precisely the circuits already marked phasic. No new constant, no new list. The validation suite now measures the onset response across four decades.
Optic flow needs frames close enough together to correspond. The first house this ran on had
exactly one camera โ a motorway traffic camera whose own photo_time showed it updating
every five minutes. At that spacing there is no correspondence between frames at
all: flow would be noise, and a looming detector fed from it would fire constantly and mean
nothing. The radar in the hallway was the better eye. The same mistake was made again in the
simulated radar on the demo box, at five seconds instead of five minutes, and it had the same
character: looming is a derivative, so the sample interval decides the shortest approach that can
be measured at all.
The ranging-sensor path is honest, but it is not how the animal does it. A fly computes expansion from photons, in the optic lobe, before anything central hears about it โ so now it does that too, in the browser, because that is where the frames are.
It is the published circuit rather than a motion detector: photoreceptors on a 32ร24 grid (a Drosophila eye has about 750 ommatidia at roughly 5ยฐ, so that is the number and not an approximation), a lamina high-pass, HassensteinโReichardt correlators giving the four direction-selective layers of the lobula plate, and then 63 LPLC2 cells tiling the field.
The LPLC2 stage is the whole thing. Klapoetke and colleagues showed in 2017 that each cell has four dendritic branches, one in each layer, each offset so it reads motion pointing away from that cell's receptive-field centre โ and that the cell needs all four driven at once. That coincidence requirement is the entire selectivity. Getting it wrong is instructive: summing the four branches instead scored a drifting grating at nine times an actual approach, and a disc crossing the view at thirty times, because one large branch carried the sum on its own. A geometric mean cannot be carried by one term. With it, both score exactly zero.
tools/test_vision.mjs, all against the same approach. The
escape threshold is about 0.1 and is enforced by the network, not by the eye.Three other things had to be measured rather than assumed. Normalising a local response by a field-wide contrast let a small object on a plain wall score a hundred times an approach, so contrast is now measured inside each cell's own receptive field. Taking the peak over 63 cells and 60 frames was measuring the maximum of noise, so each cell integrates over 120 ms first. And a camera panning across a textured room still beat a real approach fourfold โ no read-out fixed that, and five were tried, because it is not a read-out problem. It is a self-motion problem, and the fly's own answer works: flow coherence is 0.00 for an approach and 0.76 to 0.84 for a pan, which leaves a gap nobody had to tune a threshold into.
Clutter costs it about 25ร. Real LPLC2 is also less sensitive in a textured scene, but in practice this works far better against a plain wall than against a bookshelf. It still clears the escape threshold in clutter, at 0.124 against about 0.1, which is not much margin.
An approach during a camera pan is missed entirely, because the self-motion gate cannot tell the difference. A fly has exactly the same blind spot during a saccade.
And a directed escape is not possible with this data pack. LPLC2 is retinotopic, so in the animal the population says where the threat is and the fly turns away from it. Ours cannot: the hemibrain is a hemibrain, and the looming pathway in the pack is 0 left, 130 right, 147 unlabelled, with 1 left and 12 right descending neurons. There is no left population to compare against. The central complex is balanced 23/23, 21/21 and 12/12, so the compass and steering are unaffected โ but the escape burst stays undirected, which is at least true to DNp09. The validation suite checks those counts, so the day someone builds a pack from a whole-brain reconstruction, the check fails and this paragraph becomes wrong on purpose.
Nothing leaves the page. Home Assistant receives an expansion rate and an angle about ten times a second, which is roughly what a real LPLC2 population sends down its axons: a magnitude and a retinotopic address, not a picture.
The fair question about all of this is "why". A fly that walks across a dashboard is a pretty picture, and bolting a utility function onto it โ save power, learn my habits โ would be a rules engine in a fly costume, which is the other failure mode and the worse one.
There is a real answer in between, and it is not a graft. The mushroom body โ the largest thing modelled here โ exists to tell familiar from unfamiliar, and that happens to be the thing rules engines are worst at.
Dasgupta, Stevens & Navlakha (2017, Science) showed the Kenyon cell layer is a locality-sensitive hash: a sparse random projection whose codes stay close for similar inputs and far apart for different ones, which is an efficient novelty detector and was published as one. Hattori et al. (2017, Cell) found the circuit that reads it out โ repeated exposure depresses KCโMBON synapses in the ฮฑโฒ3 compartment whether or not anything good or bad happened, so those cells fire hard for something new and barely at all for something met many times.
Both are in the pack. ฮฑโฒ3 is MBON16, MBON17 and MBON28, and none of them is one of the approach/avoid cells โ which is the whole reason for using that compartment rather than depressing the valence MBONs and calling the result novelty.
binary_sensor.housefly_unusual is that, made slow enough to be worth saying. It
holds its tongue entirely until it has learned something, then wants two minutes of sustained
strangeness before it speaks. No training set, no labels, nothing sent anywhere โ
it learns what your house is like by living in it. And it will never tell you what changed,
only that something has, which is precisely the thing a rule cannot do, because you would have had
to write the rule first.
A sparse hash ticking twice a second on a CPU costs nothing. Asking a language model to go and look at a whole house costs real money. So let the fly decide when it is worth asking.
HouseFly fires a fly_house_unusual event on the rising edge, carrying the novelty
figure, how long it has held, and a shortlist of suspects โ which configured inputs
the surprise is arriving through, obtained by reading the measured PNโKC wiring backwards from the
cells that are active and have not habituated.
Not "what is wrong": the Kenyon code is a hash and a hash does not invert, and channels collide because a random projection with more entities than glomeruli must. Measured on a synthetic input, three of the top four channels were among the eight genuinely driven and one was not. It narrows the field, which is the right job for something whose next step is to hand the question to a system that can actually go and look.
A fly has no concept of your electricity bill, and the honest thing is to say so rather than build something that pretends otherwise. The punishment pathway is real โ PPL1 dopaminergic neurons depress KCโMBON, which is genuine operant conditioning โ so feeding consumption in as aversive would work mechanically. It would not save you anything: the actuation budget is sixty calls an hour with deadbands and quiet hours, and the motor output is not a controller. You would get a fly that avoids the expensive rooms. A demonstrable behaviour, not an energy saving.
The version that does work is the one above. Power is just another input channel, so adding a consumption sensor to the inputs means "unusual" covers it for free โ it will tell you when your electricity stops looking like your electricity, which is the failure mode that actually costs money and exactly the one you cannot write a rule for in advance.
The clock used to peak at a fixed 06:00 and 18:43. That is nobody's daylight, and at this
latitude it is not close for most of the year. It now watches whatever light the house reports โ an
illuminance sensor if there is one, sun.sun's elevation otherwise โ learns where dawn
and dusk actually fall, and moves the morning and evening oscillators there.
This is photoperiod tracking and not entrainment, and the difference is worth being exact about. Entrainment is a free-running oscillator being pulled into phase by a zeitgeber, and there is no free-running oscillator here โ the clock in this model is a function of local time. So moving where the peaks sit is the honest version of the claim, and estimating dawn from the light happens in the coordinator and is labelled a stand-in rather than dressed up as neural.
Separately and acutely, light drives l-LNv, the arousal-promoting clock cells, which are genuinely light-responsive (Shang et al. 2008). Arousal measured 0.17 in the dark at 00:30 and 0.51 with the light on. The photoperiod shifts when it is active; this makes it active now.
Averaging times of day is where this goes wrong, twice. The mean of 23:50 and 00:10 is not midday, so the learned phases are dragged round the circle; and a dusk learned at 23:30 against a time of 00:15 is forty-five minutes apart, not three quarters of a day, so the Gaussian needed the same treatment. Before that fix it reported no evening at all โ a bug that only shows up in winter, or in a house that gets up late.
This is a real model of real circuits, and it is still a model.
It is not conscious, it is not an agent, it does not understand your house, and it is not a scientific instrument. It is a small animal's wiring diagram with your sensors plugged into it.
python3 tools/validate.py runs the shipped brain and checks every number on this
page, printing them all โ including the marginal ones and the two that are documented failures. CI
runs it on every push, alongside HACS's own validation action and Home Assistant's hassfest, and it
installs Node because the eye's suite runs where the frames do.
a bump forms peak 0.231, trough 0.000 PASS
it holds still with no self-motion 0 deg of drift PASS
turning rotates the bump, right direction correlation +0.98 PASS
a constant turn does NOT keep rotating it an offset, not a velocity PASS
the looming pathway has no left hemisphere 0 left, 130 right PASS
punishment depresses KCโMBON synapses 20,391 plastic synapses PASS
something new registers as novel first sight 0.989 PASS
...and stops being novel with exposure after 400 ticks 0.025 PASS
...with no teaching signal at all valence memory untouched PASS
a trivial expansion triggers nothing 1e-5..1e-2 gave 0.0000 PASS
a camera panning produces nothing textured pan 0.0000 PASS
dawn at 04:00 gives a fly that wakes at 4 peak moved 06:00 โ 04:00 PASS
an empty quiet-hours window means never not, as it did, always PASS
50/50 checks passed
There is a hosted demo: a real Home Assistant, on a real Vome instance, with a small fake house in it. The link signs you in as a non-admin guest, so you can prod everything without needing an account.
If you find it asleep, that is not a fault โ it is crepuscular, and the middle of the afternoon is its siesta. It will still bolt at you.
HouseFly is a HACS custom repository. In Home Assistant:
https://github.com/Vortitron/HouseFly, type IntegrationBoth Lovelace cards register themselves โ no resource setup needed:
# Lets the fly out onto this dashboard. Draws nothing itself: it reports
# where your cards are and paints the fly over the top of them.
- type: custom:housefly-overlay
show_debug: true
# The connectome, with live activity in it.
- type: custom:housefly-brain-card
The repository ships a testbed/ โ a throwaway Home Assistant with a fake ten-room house:
24 lights, 23 switches, 30 drifting sensors, 15 motion detectors, all template helpers with nothing
behind them, plus six entities that exist purely so you can watch the safety layer refuse them.
git clone https://github.com/Vortitron/HouseFly
cd HouseFly/testbed && ./up.sh # http://localhost:8124
Or skip the setup entirely: Vome hosts Home Assistant instances you can create in a few seconds, which is where the live demo of this runs.
A fly with write access to your house is a bad idea unless the boundaries are real ones.
lock, alarm_control_panel,
cover, climate, water_heater, humidifier, valve,
vacuum, lawn_mower, script, automation, scene.boiler,
freezer, pump, oven, server, alarm,
garage, charger, and about twenty more.Start it on a spare lamp.
Both datasets are CC-BY 4.0, and both are fetched at build time rather than vendored.
The flies did the hard part.