·VA

Case study

NILM disaggregation · Linkya

ML · Time-series · Signal processing

GitHub

Home Assistant tracks everything — except the water heater, hardwired to the breaker panel with no smart plug possible. Linkya feeds the Linky signal into a Seq2Point NILM engine: draw the signatures, train, the heater emerges and publishes to HA. Blind spot closed.

Role
R&D & dev
Stack
Python · TensorFlow · Keras · Seq2Point
Status
In production
The problem: an appliance out of reach

Home Assistant measures everything with a smart plug: TV, laptop, fridge, oven, router. Consumption is covered, appliance by appliance.

Except the water heater. Hardwired directly to the breaker panel, dedicated circuit — no way to slip a smart plug in. And yet it runs several times a day, a few kilowatt-hours each cycle. In the HA consumption donut, it shows as an 'untracked' slice.

The Linkya approach

NILMNon-Intrusive Load Monitoring: infer each appliance's usage from the meter's aggregate signal alone.

Linkya implements a Seq2Point model on an LSTM/GRU architecture with an attention mechanism. In the interface, we explore the global Linky signal, spot the water heater's characteristic transitions, draw the signatures, then train.

The model learns to recognize these fingerprints in the aggregate signal's noise. Once trained, it detects continuously and publishes the heater's state directly to Home Assistant via MQTT.

Let's be honest: I wasn't an ML specialist before this project. I cleared the path paired with AI, without dropping the discipline that makes the result trustworthy. Working with AI →

Linkya in action
Linkya interface: appliance list, annotated Linky power curve with water heater signatures, detection list.
Left: the configured appliance with 54 signatures and 72 detections. Centre: the Linky signal with detection markers. Right: detected cycles with timestamp and duration.
How we train

In the UI, we visualise the global Linky signal over the last N days, spot the characteristic transitions — a regular power spike, a consistent start time (off-peak hours for the water heater). We highlight signatures on the curve and assign them to the appliance.

Linkya interface: selecting a time range on the power curve.
Highlight directly on the curve the characteristic zones. A few clicks create a signature.
Linkya interface: new signature created.
Once drawn, the signature is assigned to the appliance and ready for training.
Refinement and negative detections

After a few signatures are recorded, we train the model to recognise these fingerprints in the aggregate signal's noise. We can then test detection on the rest of the curve and refine signatures as needed. A bad detection can become a 'negative' signature — the model learns what not to confuse.

Linkya interface: an erroneous detection annotated as a negative signature.
A bad detection becomes a counter-example: the model learns what it must not confuse.
Publishing to Home Assistant

Once the model is satisfactory and the option enabled, Linkya detects continuously and publishes the appliance state directly to Home Assistant via MQTT.

Linkya interface: list of new detections published to Home Assistant.
Detections appear in real time. The water heater entity is now visible in HA just like any smart plug.
What works
The water heater comes out

The main blind spot is closed: activations, estimated usage, time patterns. The entity appears in HA just like any smart plug.

Complete donut

The 'untracked' slice disappears. Consumption is now attributed, including the appliance that couldn't be wired.

Confidence measured

Every detection is scored against the reference signatures. The 30-day mean confidence is visible live.

Zero extra hardware

Everything runs on the meter signal already captured. No new hardware, no wiring.

Live
NILM · Linkya model trained Last water heater detection on
The hard limit

Identical appliances. Same heater model in the bedroom, the guest room and the office: identical electrical signature.

The model clearly sees that a heater is running, but not which one. Three interchangeable loads, one fingerprint. That's NILM's real wall in general — not a Linkya bug, a physical constraint of the aggregate signal.

Where it runs

Linkya isn't a standalone project: it's one of the apps deployed on my self-hosted infra.

Same Raspberry Pi, same Docker Compose base as the rest. One more app behind the single front door.

See the infra →

References