A Home Assistant custom integration – know exactly what triggered your smart devices.
Whodidit creates a diagnostic sensor for any entity you choose to monitor. Every time that entity's state changes — or a meaningful attribute changes (brightness, color, volume...) — the sensor updates to record what caused it (automation, script, scene, dashboard/UI, service account, or the device itself), who did it, when, and how confident Whodidit is in that answer.
Attribution / inspiration. Whodidit is an independent, from-scratch reimplementation inspired by the (no longer available) sfox38/whodunnit project (MIT licensed). No original source code was copied — this integration was built purely from the publicly documented behaviour, README and release notes of that project, combined with Home Assistant's own official APIs. Full credit to the original author for the concept.
- What it does
- How it works
- Installation
- Setup
- Supported domains
- Sensor states
- Sensor attributes
- Confidence levels
whodidit_trigger_detectedevent- Automation examples
- Caveats and limitations
- Original specification
- Version history
Whodidit creates a diagnostic sensor for any supported entity. Each state or relevant attribute change updates the sensor with:
- What caused the change (automation, script, scene, dashboard, physical press, service account, or the device itself)
- Who did it (person name, when triggered via the UI)
- Which specific automation/script/scene was responsible
- When it happened (ISO 8601 timestamp)
- How confident Whodidit is (
high/medium/low) - A rolling history of the last 25 trigger events
- A cache debug attribute explaining how the classification was reached
All of this persists across Home Assistant restarts (RestoreEntity).
Home Assistant attaches a Context (documented, stable public API: id, parent_id, user_id) to every state change. Whodidit listens — once, globally, for all tracked entities — to automation_triggered, script_started and to scene.turn_on service calls (scenes do not fire a dedicated activation event in HA core, so the service-call context is used instead), caches these contexts, and matches them against the context of each subsequent state change.
Detection cascade:
- Direct cache hit on the context ID → the change was caused by a cached automation/script/scene run. Confidence: High.
- No cache hit, but
user_idis set → a human (or a service account, if theuser_idhas no linkedpersonentity and/or is HAsystem_generated) acted via UI/app. Confidence: High. - No cache hit, no user, but
parent_idexists → HA was involved upstream; Whodidit tries to resolve the parent context too. Resolved → High confidence with the specific source named. Unresolved (deep chains, third-party integrations) → classified aswhodidit.indirect/ Automation (Indirect), Confidence: Medium. - Nothing matches → the change came straight from the device (physical button, hardware timer, firmware event). Confidence: High.
Attribute-only changes (e.g. dimming a light without toggling it) follow the same cascade and are debounced to one update per 2 seconds per entity.
- Open HACS → three-dot menu → Custom repositories.
- Add
https://github.com/dvbit/whodidit, category Integration. - Install Whodidit, then restart Home Assistant.
- Copy
custom_components/whodiditinto yourconfig/custom_components/directory. - Restart Home Assistant.
Settings → Devices & Services → + Add Integration → Whodidit → pick the entity to monitor. Repeat for each entity you want tracked (already-tracked entities are hidden from the picker). Each tracked entity gets its own sensor and its own config entry.
switch, light, fan, media_player, cover, lock, vacuum, siren, humidifier, climate, remote, water_heater, valve, number, select, button, input_boolean, input_button, input_number, input_select, input_text, alarm_control_panel, timer.
Helper entities without a physical device get a virtual device created automatically to host the sensor; it is removed automatically when the Whodidit entry is deleted.
| State | Meaning |
|---|---|
monitoring |
Active, no change recorded yet |
automation |
An automation triggered the change |
script |
A script triggered the change |
scene |
A scene activation triggered the change |
ui |
A human user acted via dashboard/app |
service |
A service account (Node-RED, AppDaemon...) triggered it |
device |
A physical/device-internal event triggered it |
source_type, source_id, source_name, context_id, user_id, event_time, confidence, history_log (last 25 entries), cache_debug.
| Level | Meaning |
|---|---|
| High | Direct context match, or certainty of no HA involvement |
| Medium | HA involvement confirmed but specific source unresolved |
| Low | Possible ESPHome context-reuse misclassification |
Fired on every classification, unlike a plain state trigger which would not fire for repeated identical sources. Use this for automations:
automation:
- alias: "Notify of unexpected garage light change"
trigger:
- platform: event
event_type: whodidit_trigger_detected
event_data:
entity_id: light.garage_light
action:
- service: notify.mobile_app
data:
message: >
The garage light was changed by
{{ trigger.event.data.source_name }}
via {{ trigger.event.data.state }}.For every tracked entity you can enable a companion binary sensor that turns ON at the first physical click (a classification with source_type = device) and turns OFF when the click-detection window closes. The model is inspired by dvbit/switch_interaction.
Enabled during the config flow (step 2) or later via Configure on the entry. Fields:
- Enable physical-interaction binary sensor — master switch.
- Click window (seconds) — default 3. The detection window; each click within it extends the window (sliding).
- The binary sensor goes ON at the first physical click and OFF when the window closes. It is ON for the duration of a "click train" and OFF between trains.
- The
click_countattribute holds the number of physical clicks in the train. It persists after the sensor goes OFF (keeps showing the last train size) and resets to 0 only at the first click of the next train. Example: a single click shows 1; after the window, a double-click shows 2 (not 3).
whodidit.reset_physical_interaction — forces OFF and clears click_count. entity_id accepts either the tracked entity or the binary sensor itself.
service: whodidit.reset_physical_interaction
data:
entity_id: light.kitchenautomation:
- alias: "Triple physical click -> movie scene"
trigger:
- platform: state
entity_id: binary_sensor.kitchen_light_physical_interaction
to: "off"
condition:
- condition: template
value_template: "{{ trigger.from_state.attributes.click_count == 3 }}"
action:
- service: scene.turn_on
target:
entity_id: scene.movieDon't let a motion sensor turn off a light that was manually turned on:
automation:
- alias: "Smart motion off - respect manual control"
trigger:
- platform: state
entity_id: binary_sensor.office_motion
to: "off"
condition:
- condition: not
conditions:
- condition: state
entity_id: sensor.office_light_trigger_source
state: "device"
action:
- service: light.turn_off
target:
entity_id: light.office_lightAlert on low-confidence classification:
automation:
- alias: "Warn on low confidence Whodidit reading"
trigger:
- platform: event
event_type: whodidit_trigger_detected
event_data:
entity_id: light.garage_light
confidence: low
action:
- service: notify.mobile_app
data:
message: "Whodidit is uncertain about the garage light trigger source."The integration ships a custom Lovelace card that is registered automatically — no separate HACS "Frontend" install needed. After updating, hard-refresh the browser (Ctrl/Cmd+Shift+R) or clear the companion-app cache once so the new resource loads.
Add it to a dashboard:
type: custom:whodidit-card
entity: sensor.kitchen_light_trigger_sourceThe card shows:
- Last interaction — state icon, localized state and a small colour-coded confidence dot (green = high, amber = medium, red = low). Click the row to open a history popup with the last 25 entries.
- Physical interaction — a discreet Active/Idle indicator plus
click_countand last-click time (only when the binary sensor is enabled). - Bottom-right controls — a reset button (when the binary sensor exists) and a settings cog (⚙️) opening a dialog to change, on the fly: enable/disable the physical-interaction sensor and the click window. Saving calls
whodidit.update_options, which reloads the entry so changes take effect immediately.
Note on distribution. HACS does not surface cards that live inside an Integration repository in its "Frontend" tab. That is expected: Whodidit serves the card as a static asset and registers it as a Lovelace resource itself, so it works without any manual resource entry (in Lovelace storage mode). In YAML mode add the resource manually:
url: /whodidit/whodidit-card.js,type: module.
- System restarts: state changes that occur while HA is offline are not captured.
- ESPHome context bleed: ESPHome devices may reuse the previous HA context for ~5s after a command; a physical press in that window can be misclassified as UI with
confidence: low. - Indirect automations: deeply nested chains or third-party integrations that create their own context chains resolve to
Automation (Indirect)at Medium confidence. - Overloaded networks: the context cache has a 2-minute TTL; on severely congested systems events may arrive out of order.
- Physical vs internal events: HA does not distinguish a genuine physical press from a device-internal firmware event at the context level, so neither can Whodidit.
Consolidated requirement used to build this integration
Integrazione custom HA (Python, config_flow), feature-parity completa con
whodunnit v1.3.0, rebrand come "whodidit":
Core detection: sensore diagnostic per entità monitorata, stato =
monitoring/automation/script/scene/ui/service/device. Cascata a 4 livelli:
cache context_id -> user_id (persona/service account) -> parent_id
(risoluzione ricorsiva) -> device. Confidence high/medium/low. Listener
condivisi singoli su eventi automation/script/scene, cache context TTL 2
min con cleanup periodico. Cache identità utente TTL 5 min.
Attributi sensore: source_type, source_id, source_name, context_id,
user_id, event_time, confidence, history_log (ultimi 25, persistente),
cache_debug (matched_entry, age, total_cache_entries).
Evento whodidit_trigger_detected sul bus, payload completo, fired ad ogni
classificazione.
Attribute-only changes: monitoraggio per dominio (light, climate,
media_player, fan, cover, water_heater, humidifier, vacuum), debounce 2s.
ESPHome bleed detection: finestra 5s, confidence low se rilevato riuso
context.
Persistenza & lifecycle: RestoreEntity, virtual device per helper senza
device fisico, availability tracking, diagnostics download,
entity_category diagnostic, SensorDeviceClass.ENUM.
Config flow: picker entità (esclude già tracciate), 1 config entry = 1
sensore = 1 device page.
Domini supportati (21): switch, light, fan, media_player, cover, lock,
vacuum, siren, humidifier, climate, remote, water_heater, valve, number,
select, button, input_boolean, input_button, input_number, input_select,
input_text, alarm_control_panel, timer.
No dashboard card - solo integrazione.
Localizzazione: EN/IT/FR/ES/DE. Output HACS-ready, README EN+IT.
- Simplified the physical-interaction model. The binary sensor is now simply ON during a click train and OFF when the detection window closes. Removed the motion/occupancy reference sensors and the separate reset lapse entirely — the only physical-interaction options are the enable toggle and the click window.
click_countstill persists after OFF and resets at the next train. The manualwhodidit.reset_physical_interactionservice is retained. Legacy options from older versions (reference sensors, reset lapse) are ignored/removed automatically.
- Card: the header now shows the monitored entity name. The trigger-source sensor exposes two new attributes for this,
tracked_entityandtracked_entity_name. - Card: the history popup now shows the user for UI actions (
by <name>) and no longer repeats a redundant "Device" label for device/monitoring entries, matching the main row.
- Fix: in some cases the whodidit entities were created without a device (or spawned an orphan device). The integration previously tried to merge its entities into the tracked entity's physical device by copying its identifiers and connections — a pattern that is deprecated and could silently fork a duplicate/empty device (HA dev blog 2026-07-21). Whodidit now always creates its own named service device and nests it under the physical device via
via_device, so the entities are reliably grouped on a named device page and shown as a child of the real device when there is one.
- The integration now ships its own brand icon in a
brand/folder (brand/icon.png,brand/logo.png+ @2x). Since Home Assistant 2026.3 custom integrations can provide local brand images, served through HA's/api/brands/integration/whodidit/…proxy and taking priority over the CDN — nohome-assistant/brandsPR needed. The icon shows on the Devices & Services page, device pages and throughout the HA frontend. - Note: on HA versions older than 2026.3 this folder is simply ignored (no breakage). A known HACS bug (hacs/integration#5171) means the icon may still appear blank in the HACS store list until HACS adds a fallback to the local brands API; everywhere else in HA it works.
- Added the project icon to the top of the README (visible on GitHub and in the HACS Info tab).
- Entities now carry dynamic mdi icons in the Home Assistant UI: the trigger-source sensor changes icon per state (radar/robot/script/palette/tap/cog/double-tap) and the physical-interaction binary sensor shows an active/idle hand.
- Fix (card):
deviceandmonitoringno longer show a duplicated "Device" label — the redundant source name is suppressed, leaving just the time. - Fix (card + backend): UI actions now show the user who performed them (
by <name>). A UI service-call context cached with an empty name is now resolved to the actual person/service account on a direct cache hit, sosource_nameis populated instead of blank.
- Fix: corrected
click_countbehaviour. When the detection window closes the value now persists (it keeps showing the last completed train, e.g. 2); it resets to 0 only at the first click of the next train. So a single click shows 1; after the window a double-click shows 2 (not 3), and the previous value stays visible in between. The value is restored across restarts.
- Fix: UI/dashboard actions were sometimes misclassified as
device. Home Assistant frequently emits the resultingstate_changedevent withuser_id = None, keeping onlyparent_idpointing back to the originating service call (core behaviour, see core issue #90669). Whodidit now caches every user-initiated service-call context and resolves it viaparent_id, so dashboard taps are correctly reported asui(orservicefor service accounts). Note: a few integrations emit a brand-new context with neitheruser_idnorparent_idpreserved; those changes remain indistinguishable from a physicaldeviceevent at the context level.
- Redesign: the Whodidit Card is now minimalist and closer to native Lovelace styling. Confidence is shown as a small coloured dot (green/amber/red) instead of a text badge; clicking the state row opens a history popup; the reset and settings-cog controls sit at the bottom-right of the card.
- Fix:
click_countnow counts clicks within the detection window only (a "click train"). When the window closes the counter resets to 0, so the next physical click starts fresh at 1 — a single click shows 1, then a following double-click shows 2 (not 3). The counter is independent from the binary sensor's reset lapse and is no longer restored across restarts.
- New: bundled Whodidit Card (
custom:whodidit-card) auto-registered by the integration — last interaction, physical-interaction block, 25-entry history timeline, and a settings cog to edit options live. - New:
whodidit.update_optionsservice backing the card's settings dialog. - Manifest now declares
frontend+httpdependencies for static-asset serving.
- New: integration icon (magnifying glass over a
?— the classic "who did it?" motif) added asicon.png/icon@2x.pngin the component folder. Home Assistant shows it in the Integrations page automatically; on GitHub / HACS the same asset appears in the repo card.
- New: optional Physical Interaction binary sensor per tracked entity with
click_countattribute (model inspired bydvbit/switch_interaction). - New: three-mode auto-reset (occupancy > motion > time-only) plus manual reset via the new
whodidit.reset_physical_interactionservice. - New: two-step config flow (entity picker + physical-interaction options) and full Options Flow to edit settings later.
- Fix:
HTTP 400when opening the config flow — the entity selector was passingexclude_entities=None, which fails voluptuous schema validation on the frontend.exclude_entitiesis now omitted when no entities are already tracked. - Fix:
manifest.jsonversionfield aligned to fullMAJOR.MINOR.PATCHform for stricter HA loaders.
- Full feature-parity reimplementation as specified above.