Architecture
This page explains what the Pivot integration does inside Home Assistant, what it creates, reads and writes, and the boundaries of its access.
The full source is available at alistairmerritt/pivot-integration.
Firmware: Pivot also uses custom firmware for the Home Assistant Voice Preview Edition. The firmware is a separate part of the project and can be reviewed independently on the Firmware page.
What the integration is
Pivot is a Home Assistant custom integration written in Python.
Its job is fairly simple:
-
Provision entities
It creates and manages the Home Assistant entities that Pivot firmware relies on, including bank values, active bank, bank assignments, colour settings and control switches. -
Respond to changes
It listens for knob turns, bank changes, button presses and relevant external entity updates, then calls the appropriate Home Assistant services in response.
The integration runs inside Home Assistant. It does not run its own web server, connect to a separate backend or cloud service, or access anything outside your Home Assistant instance.
Trust and security model
Pivot is an independent project and is not affiliated with Nabu Casa or the ESPHome team. It is built and maintained by u/Pivotonian, a long-time Home Assistant community contributor.
The integration is designed to remain local and deliberately narrow in scope. It uses standard Home Assistant entity platforms, service calls, event listeners and device APIs. What it does not do lists the specific boundaries.
Both the integration and firmware are open source and can be inspected before installation.
For the integration, the modules below show how the code is divided and what each part is responsible for. The firmware can be reviewed separately at alistairmerritt/pivot-firmware, including the complete home-assistant-voice.yaml.
The Firmware page also explains what leaves the device, how microphone audio is handled, and how to compare Pivot’s configuration with the official Voice PE firmware.
Module overview
The integration is split into a small set of modules with defined roles.
| File | Responsibility |
|---|---|
__init__.py |
Setup, teardown and wiring listeners together |
bank_control.py |
Knob changes, bank switching, gauge sync and external state sync |
button.py |
Button press handling and pivot_button_press event firing |
entity_mappings.py |
Maps a 0–100 Pivot value to the correct Home Assistant service call for each supported domain |
announcements.py |
Formats and triggers spoken announcements |
mirror.py |
Watches assigned lights and mirrors their colour into the bank colour entity |
device_sync.py |
Pushes settings to the device after Home Assistant starts so they remain correct after a restart |
blueprints.py |
Sends a one-time notification on first setup with links to import the optional timer blueprints from GitHub |
config_flow.py |
Setup flow and options flow |
const.py |
Entity definitions, constants and shared configuration |
| platform files | Entity platform implementations such as number, text, switch, binary_sensor, light and select |
The separation is mainly there to keep the integration easier to inspect and reason about. It is still one system, but each module has a narrower role.
What it creates
On setup, the integration registers a device and creates a set of standard Home Assistant entities.
These are normal Home Assistant entities using standard platforms.
Per device, per bank
For each of the four banks, Pivot creates:
number– bank value (0–100)text– assigned entity IDtext– live bank LED colourtext– configured bank colourbinary_sensor– passive flagswitch– mirror light enabledswitch– announce value enabledlight– virtual bank colour picker
Per device, shared
Pivot also creates shared device-level entities:
number– active bankswitch– control modeswitch– show control valueswitch– dim when idleswitch– system announcementsswitch– mute announcementstext– TTS entitytext– media player entity
Timer entities
Timer entities are also created by the integration and are disabled by default:
number– timer durationselect– timer statetext– timer end timetext– restore value for Show Control Value (diagnostic)
All entity IDs follow a stable pattern:
{platform}.{device_suffix}_{key}
They are pinned explicitly so they remain stable across Home Assistant restarts and device renames.
Why so many entities?
Around forty entities per device, plus four timer entities that are disabled by default, is a fair thing to question.
The reason is that these entities form the interface between the integration and the firmware.
ESPHome has no private side channel into Home Assistant. If the firmware needs to know which bank is active, what value it holds, what colour it should use or whether a setting is enabled, that information needs to exist somewhere the device can subscribe to. Pivot uses normal Home Assistant entities for that purpose.
In practice, the entities act as shared values between the two halves of Pivot.
This is deliberate. Pivot does not maintain a separate private state store for the values shared with the firmware. Bank values, assignments, colours and settings remain visible in Home Assistant, where they can be inspected in Developer Tools, used in automations or displayed on dashboards.
What it reads
Pivot reads the state it needs in order to operate and keep the firmware in sync.
Assigned entity states
When a bank is active, Pivot may read the current state of the assigned entity so the gauge and firmware state can stay in sync.
Examples include:
- brightness from a light
- volume from a media player
- percentage from a fan
- target temperature from a climate entity
- position from a cover, or whether it is open or closed if it cannot report a position
- whether a cover accepts a position, based on its supported features, which determines whether its bank is passive
- on/off state from a switch or
input_boolean, shown on the ring - value from a
numberorinput_number
Its own entities
Pivot also reads its own entities to determine things such as:
- which bank is active
- which entity each bank is assigned to
- whether control mode is enabled
- whether announcements are enabled
- whether light mirroring is enabled
Device registry lookup
During setup, Pivot performs a read-only lookup in Home Assistant’s registry so it can locate the relevant event entity for the ESPHome device.
When pushing settings, it also reads the device’s current name from its ESPHome configuration entry so the sync continues to work if the device is renamed.
Only the device name is read for this purpose. Pivot does not read the stored ESPHome password or encryption key, and it does not modify device registry data.
What it writes
Actions against assigned entities are made through normal Home Assistant service calls. Pivot also updates its own entities as part of keeping the integration and firmware in sync.
It does not bypass Home Assistant to control assigned devices directly.
Writes to assigned entities
Depending on the bank assignment, Pivot may call services such as:
light.turn_onwithbrightness_pctmedia_player.volume_setfan.set_percentageclimate.set_temperaturecover.set_cover_positionnumber.set_valueinput_number.set_valuehomeassistant.togglescene.turn_onscript.turn_onmedia_player.media_play_pausecover.toggle
These calls are made in response to Pivot input or explicit sync behaviour.
Writes to Pivot’s own entities
Pivot also updates its own entities when needed, for example:
- updating bank values so the gauge reflects an external change
- storing TTS and media player selections from the integration settings
- updating colour values used by the firmware
Repairs notices
If an integration entry is linked to a device whose button events Pivot cannot hear, Pivot raises a notice in Home Assistant’s Repairs section (Settings → Repairs).
The notice is removed again once button events can be detected or the integration entry is deleted.
Settings push to the device
Once Home Assistant has fully started, Pivot pushes its current settings to the ESPHome device using the pivot_sync_settings_v2 action exposed by the firmware.
Home Assistant exposes this as:
esphome.{device_name}_pivot_sync_settings_v2
The current ESPHome device name is looked up for each attempt, so the sync continues to work if the device is renamed.
The sync sends:
- Control Mode
- Show Control Value
- Dim LEDs When Idle
- the per-bank Mirror Light settings
- the per-bank passive flags
- the active bank
- each bank’s current value
- both sets of bank colours
This is a normal Home Assistant service call. It does not bypass Home Assistant.
The extra sync is needed because the ESPHome integration normally forwards state changes to subscribed devices. During a Home Assistant restart, the Voice PE can reconnect before Pivot’s entities have finished restoring. If an entity is simply restored to its previous value, there may be no new state change for ESPHome to forward.
The settings push makes sure the firmware receives the current values regardless of startup order.
It also runs when the device connects later and retries with backoff until a successful attempt is confirmed. If the sync cannot complete, Pivot logs a warning.
Blueprint notification
On first setup, Pivot sends a one-time Home Assistant notification with links to import the optional timer blueprints from GitHub.
The blueprints are not installed or copied automatically. Importing them is optional and user-initiated.
What it does not do
Pivot does not:
- make external HTTP requests
- call external APIs
- send telemetry
- access Home Assistant credentials or tokens
- write to
configuration.yaml - write to
automations.yaml - write to
scripts.yaml - maintain its own database
- continuously poll Home Assistant in the background
The integration is event-driven and reacts when relevant state changes occur.
Scope of control
Pivot does not scan your Home Assistant instance and start controlling entities on its own.
Its control is limited to two places:
- the entity explicitly assigned to a bank
- Pivot’s own entities and settings
That scope is intentional. Pivot only controls the Home Assistant entities you assign to it.
Why this needs to be a custom integration
A fair question is: why not just do this with blueprints?
- Entity provisioning – blueprints cannot create entities, and Pivot needs real entities with stable IDs and device registration for bank values, assignments, colours, switches and timer state.
- State restoration – Pivot uses Home Assistant’s restore-capable entity classes so values survive a restart in the normal way.
- Loop prevention – handling two-way state synchronisation without treating an external update as a new knob turn is more reliable to manage in Python than through a chain of automations.
- Responsiveness – knob changes arrive quickly, and native callbacks let Pivot handle them directly without relying on multiple automations and templates.
- Device model – Pivot behaves like a normal Home Assistant device, with grouped entities and a consistent interface between the integration and firmware.
Limitations and design boundaries
Pivot is intentionally narrow in scope.
A few things to keep in mind:
- supported behaviour depends on the assigned entity domain
- some domains use simple on/off or trigger-style interactions rather than continuous control
- some behaviour is entity-dependent because different Home Assistant integrations expose different attributes and capabilities
- the timer blueprints are optional and imported separately — they are not required for Pivot to work
- firmware and integration versions should be kept in sync — see the compatibility table on the Changelog page
The goal is not to abstract every possible Home Assistant entity perfectly. It is to provide a stable, predictable control layer for the supported use cases.
Source
The integration is open source and Apache 2.0 licensed at alistairmerritt/pivot-integration.