A MajorDom integration — bridges ESPHome devices into the MajorDom language.
Built for the MajorDom Hub, but it doesn't need it: this is a standalone, standardized
library for ESPHome that you can use on its own (see Run it standalone below). Built on the
MajorDom Integration SDK. The entry point
is ESPHomeController (majordom_esphome/controller.py), which the Hub — or the SDK's dev
runner — instantiates and drives through its lifecycle: discovery → pairing → commands →
teardown.
- Other protocols: browse the MajorDom integrations.
- Create your own: start from the integration template.
Full integration-author docs — the controller lifecycle, data models, storing data, discovery, and a worked example — live at docs.majordom.io.
poetry install && poetry run poe install| Task | Description |
|---|---|
poe check |
Full quality pipeline (ruff, ty, pytest, poetry build/check) |
poe check --ci |
Same, plus git diff --exit-code |
Work lands on develop; master is protected and released via Actions → Release. Tests drive
the controller with the SDK's test doubles against a simulated ESPHome device — no physical
hardware required (see tests/).
majordom-esphome is a standalone library — import it into your own app, or run just this
integration interactively (discover, pair, control, and inspect devices from a prompt) with no
Hub. It needs ESPHome devices reachable on the local network (Wi-Fi or Ethernet).
See Standalone mode for the interactive CLI, watch mode, and the programmatic API.
The integration is transport-agnostic: it speaks to ESPHome devices over any supported transport, so swapping from Wi-Fi to Ethernet needs no source change.
- Native API (default): encrypted TCP connection to the ESPHome native API port (
6053). - REST / HTTP: fallback polling via HTTP when the native API is disabled on the device.
- Selecting the transport: by default the integration attempts native API first, then
falls back to HTTP. Pin one explicitly with the
MAJORDOM_ESPHOME_TRANSPORTenv var (native/http) or theESPHomeController.transportattribute. Seemajordom_esphome/transport.py.
- Protocol / platform: ESPHome via native API and HTTP REST.
- Transport(s): TCP/IP (Wi-Fi, Ethernet).
- Supported devices: ESPHome-flashed devices — lights, switches, sensors, climate, covers, fans, number, select, text, button, lock, valve, etc.
- Credentials needed to pair: API encryption key (if enabled on the device); otherwise none.
- Hardware adapters: none — ESPHome devices are self-contained and connect over the network.
- Third-party software services: none — the integration speaks directly to ESPHome devices.
- OS / permissions: network access to the ESPHome devices (same LAN or routable subnet); mDNS/SSDP may be required for discovery depending on configuration.
Every integration is two things stacked: the MajorDom integration layer — mapping the protocol to MajorDom's domain model — sitting on top of the protocol stack it bridges. The top layer is always this repo.
| Layer | Protocol | Implemented by |
|---|---|---|
| MajorDom integration | maps ESPHome ↔ MajorDom domain model | this repo, always |
| Application (7) | ESPHome Native API / HTTP REST | this integration |
| Transport (4) | TCP | OS / device firmware |
| Network (3) | IP (IPv4 / IPv6) | OS / device firmware |
| Data link / Physical (1–2) | Wi-Fi (802.11) or Ethernet | device hardware |
Two checklists — this README is where you track them (tick items as you implement them and the
matching test in tests/ goes green).
Implementation — makes the integration functional:
- Discovery services registered via
self.dependencies.zeroconf_discovery_service; cancel closures saved and called instop - Discovery service listeners fire when devices are found, and the controller calls
self.dependencies.output.controller_did_receive_discovery - Discovery of devices already paired to the Hub on reconnect, e.g. after a reboot
(
self.dependencies.output.controller_did_connect_deviceis called) -
start_pairing_windowimplemented (mDNS discovery window for new devices) - Device pairing
- Device schema is properly mapped: device info, parameter list, and each parameter's metadata are translated to MajorDom's domain model
- Hub → Device control (
send_commandis implemented) - Device → Hub event subscription (
self.dependencies.output.controller_did_receive_eventsis called on incoming state changes) -
identifyis implemented (device-side LED / buzzer trigger via ESPHome service call) -
unpairis implemented -
fetchis implemented - Paired devices going offline/coming back online while the Hub is running — set
device.availableaccordingly (reportcontroller_did_lose_device), and clear/setlast_errorto match - Graceful shutdown in
stop, cancelling any running tasks, discovery stopped, all connections closed - Tests pass against a virtual/simulated ESPHome device (
tests/test_controller.py) - README fully filled in
Quality — makes it reliable and maintainable (the bar for release):
- Recovers automatically from connection loss / offline device / restarted backend — retried with backoff, no manual restart
- No exception escapes the controller — every background task, subscription loop, and
callback catches its own errors; nothing raised into
self.dependencies.output.* - Failures are surfaced, not raised — logged once (no spam) and reflected on the device
(
available/last_error), cleared on recovery - Re-authenticates automatically when the API key is rejected (if the device requires one)
- Fully asynchronous — no blocking I/O on the event loop; heavy/blocking work runs off-loop
- Stable identity — device and parameter UUIDs derived through the SDK helpers from the device's MAC address, identical across restarts/re-pairs
- End-to-end tests drive pair → command → fetch → events →
unpairagainst a virtual device (majordom_integration_sdk.testing) - Failure paths tested — offline device, transport error, rejected credentials degrade gracefully (no raise)
- Broad device coverage — lights, switches, sensors, climate, covers, fans, number, select, text, button, lock, valve (virtual-device catalogue in CI)
- Fully typed (
ty, no package-wide ignores) and clean (poe check) with no warnings - Readable & structured — conversion logic in a mapper, models separated, comments where intent isn't obvious
- Efficient — subscriptions over polling; batch reads; no redundant work
- Diagnosable — logging at the right levels to debug a device problem from logs alone
- Rich parameter metadata — correct
visibilityper parameter and a sensiblemain_parameter, so the app presents a clean control-center action and a tidy parameter list (Parameter UX) - Owned — a listed maintainer who keeps it working as the ESPHome/library evolve
Every parameter's UX metadata is resolved from several sources. Two independent axes, each with its own priority ladder (first match wins). See also the parameter-ux recipe.
Visibility / role / unit — resolved by classify_entity() in esphome_spec.py:
| # | Source | What it is |
|---|---|---|
| 1 | OUR_ENTITY_UX (VISIBILITY_OVERRIDES, USER_READINGS, EVERYDAY_CONTROL_ENTITIES) |
our hand curation — a human's call wins over everything |
| — | metadata / internal diagnostic entity | forced system (safety; debug counters & internal flags stay hidden) |
| 2 | v2 quirk entity metadata | per-device judgment from a loaded esphomequirks QuirkBuilder (quirk_ux_map()), runtime |
| 3 | ESPHOME_ENTITY_UX |
standard-entity judgment harvested from esphome (scripts/harvest_esphome.py, vendored — esphome is not a runtime dep) |
| 4 | fallback policy | heuristic (sensor → user, switch/number → setting); logs a warning so uncurated entities surface. Flip _FALLBACK_HIDE_UNCURATED to hide-by-default once coverage is validated on real devices. |
Bounds (min/max/step) — a separate ladder (resolve_metadata_bounds()):
- the device's own limit attributes' runtime values (
METADATA_SOURCES) — ground truth for this device; - ESPHome component schema tables (
ENTITY_MIN_STEP, platform range); - platform default. A missing expected limit is logged (quirk detection).
Quirks. esphomequirks.setup() runs once at controller startup so discovered devices are
presented in quirked form (manufacturer entities decoded into named/typed attributes; v2 entity
metadata attached). This requires the ESPHome 2024.x+ stack.
Drift. scripts/check_esphome_drift.py re-harvests esphome and diffs against the vendored
artifact via the SDK's diff_specs, tiering changes ADD / REMOVE / RECLASSIFY (high-risk —
changes what current users already see). CI opens a Dependabot-style refresh PR on drift.
The device/parameter ids are derived from the device's MAC address via the SDK's UUID helpers, so they're stable across restarts and namespaced per integration.
ESPHome devices must have the api: component enabled in their YAML for the native API transport
to work. If the device only exposes a web server, the integration falls back to HTTP REST polling.
See LICENSE. For commercial licensing or partnership inquiries regarding MajorDom, contact us via parker-industries.org/partnership.