21 β Adapter System¶
Scope: Complete implementation reference for the adapter subsystem:
adapters/registry.py(AdapterCoordinator + shim functions),adapters/config_schema.py(ADAPTER_CONFIG_SCHEMA),adapters/config_loader.py(stored adapter loading), andadapters/eufy/adapter.py(Eufy reference implementation). Every registry pattern, schema block, and config-loading path is derived directly from the source.
1. Overview¶
The adapter system is the brand abstraction layer. Every piece of brand-specific knowledge β entity naming, vocabulary, completion signals, dispatch templates, discovery config, maintenance component definitions β lives in an adapter config dict registered with the adapter registry. The framework never hard-codes brand names or entity ID patterns.
Module roles:
| Module | Role |
|---|---|
adapters/registry.py |
Stores registered configs; provides both class-based (new) and shim (legacy) access surfaces |
adapters/config_schema.py |
ADAPTER_CONFIG_SCHEMA β the authoritative definition of every valid adapter field |
adapters/config_loader.py |
Loads stored adapter configs from integration storage at startup |
adapters/eufy/adapter.py |
Assembles and registers the Eufy X10 Pro Omni adapter β the reference implementation |
adapters/roborock/adapter.py |
Assembles and registers the Roborock adapter (S6 first profile) β the second shipping brand |
Two brands ship today. Eufy is the full-feature reference; Roborock is the
second concrete adapter, auto-detected per vacuum in __init__.py by
manufacturer/model (manufacturer Roborock / model prefix roborock.) so a
mixed household wires each correctly with no user input. Roborock fills the same
schema out completely differently (native get_maps discovery, renumbering
segment ids, path-optimized order, a live map image, a native current-room
signal) β it's the Eufy worked example's foil, documented
in 29-roborock-adapter.
2. Registry (registry.py)¶
2.1 AdapterCoordinator¶
AdapterCoordinator is instantiated once per config entry in __init__.py::async_setup_entry. It sets itself as the module-level _active_coordinator pointer on creation, enabling the legacy shim functions to route through it.
coordinator = AdapterCoordinator(hass, entry) # sets _active_coordinator
coordinator.register_adapter_config(vacuum_entity_id, config)
coordinator.get_adapter_config(vacuum_entity_id) -> dict | None
coordinator.get_all_adapter_configs() -> dict[str, dict]
coordinator.unregister_adapter_config(vacuum_entity_id) -> None
coordinator.clear_registry() # empties this coordinator's _registry
coordinator.get_adapter_value(vacuum_entity_id, *path, fallback=None) -> Any
coordinator.shutdown() # clears _active_coordinator
The coordinator owns its own _registry: dict[str, dict] dict, separate from the module-level _REGISTRY fallback.
2.2 Validation¶
register_adapter_config() calls _validate_adapter() before storing:
_validate_adapter() returns a list of human-readable issue strings (empty = valid); it does not raise. register_adapter_config() logs every returned issue as a warning and then stores the config anyway β validation issues are advisory, not blocking. The one hard failure is a structurally unusable config: if config is not a dict, register_adapter_config() raises TypeError (and _validate_adapter() returns the single issue "adapter config must be a dict").
Current checks:
mappingblock (when present): must be a dict;mapping.segmenter_engineis required and must resolve to a known engine (known_engine_names()inmapping/segmenter_engines.py);mapping.segmenter_tuningmust pass the resolved engine's ownvalidate_tuning().job_segmenterblock (when present): must be a dict;job_segmenter.engineis required and must resolve to a known job/run segmenter engine (known_job_engine_names()inlearning/job_segmenter_engines.py);job_segmenter.tuningmust pass the resolved engine's ownvalidate_tuning(). This mirrors themappingcheck (deferred import). Note this is the counter/run segmenter seam, distinct from the map segmentermappingblock above (see Β§2.4).room_attributionblock (when present): must be a dict;room_attribution.engineis required and must resolve to a known room-attribution engine (known_room_attribution_names()inlearning/room_attribution_engines.py);room_attribution.tuningmust pass the resolved engine's ownvalidate_tuning(). This mirrors thejob_segmentercheck (deferred import). An absent block falls back to the Eufy engine (eufy_anchor_winding_v1); declarenoop_room_attributionto disable external-run auto-attribution. This is the external-run room-attribution seam (see Β§2.4).room_profilesblock (when present): must be a dict;room_profiles.default_profile(when set) must be a string, and each ofbuiltins,custom_template,legacy_aliases,floor_type_water_defaults,floor_type_fan_defaults,normalize_defaults(when set) must be a dict. The framework merges this block over the in-code defaults per key (resolve_profile_catalog()), so a partial block is fine β this rule only catches a malformed declaration.dispatch.template(when present): must resolve to a registered dispatch engine (known_dispatch_templates()inqueue/dispatch_engines.py). A schema-valid template with no registered engine yet is flagged rather than silently falling back to the Eufy shape.
Additional validation is expected to be added as the schema stabilizes.
2.3 Legacy shim functions (module level)¶
For backward compatibility, module-level functions route to _active_coordinator if set, otherwise fall back to the module-level _REGISTRY dict (used in tests and pre-coordinator paths):
register_adapter_config(vacuum_entity_id, config)
get_adapter_config(vacuum_entity_id) -> dict | None
get_all_adapter_configs() -> dict[str, dict]
unregister_adapter_config(vacuum_entity_id) -> None
clear_registry() -> None
get_adapter_value(vacuum_entity_id, *path, fallback=None) -> Any
get_adapter_value(*path) does nested dict traversal: each path segment indexes one level deeper. Returns fallback on any KeyError or TypeError.
2.4 Pluggable engine seams¶
Four brand-specific subsystems are pluggable behind the same seam shape: a Protocol, a module-level registry dict, a get_*() resolver with a fallback, a known_*() enumerator for the validator, an adapter config block that names the engine, and a _validate_adapter rule. The adapter declares which engine; the framework owns resolution and the cross-engine contract. This is how a second brand swaps brand-specific behavior without touching the framework call sites.
| Seam | Module | Protocol | Registry / resolver | Adapter block | Fallback | select-style framework function |
|---|---|---|---|---|---|---|
| Map segmenter | mapping/segmenter_engines.py |
MapSegmenter |
_SEGMENTER_ENGINES / get_segmenter_engine() |
mapping (segmenter_engine + segmenter_tuning) |
noop_fallback (empty result) |
β |
| Dispatch engine | queue/dispatch_engines.py |
DispatchEngine |
_DISPATCH_ENGINES / get_dispatch_engine() |
dispatch (template + field map; see "Dispatch-engine specifics" below) |
eufy_room_clean |
β |
| Job/run segmenter | learning/job_segmenter_engines.py |
JobSegmenter |
_JOB_SEGMENTER_ENGINES / get_job_segmenter_engine() |
job_segmenter (engine + tuning) |
eufy_counter_v1 |
counter_segmentation.select_active |
| Room attribution | learning/room_attribution_engines.py |
RoomAttributionEngine |
_ROOM_ATTRIBUTION_ENGINES / get_room_attribution_engine() |
room_attribution (engine + tuning) |
eufy_anchor_winding_v1 |
β |
Two segmenters, different jobs β do not conflate them. The map segmenter (eufy_cv_v1, the Eufy CV pipeline in adapters/eufy/segmentor.py) turns a map image into polygonal room overlays. The job/run segmenter (eufy_counter_v1) turns a counter-sample stream (cleaning_time / cleaning_area) into ordered per-room boundaries within a single run β no geometry. They are independent seams with independent registries.
Job-segmenter specifics (learning/job_segmenter_engines.py):
- Eufy fallback, not noop. Unlike the map seam (whose
get_segmenter_engine()falls back tonoop_fallback),get_job_segmenter_engine()falls back to the Eufy engine (_FALLBACK_JOB_ENGINE = "eufy_counter_v1") for an absent/unknown name β same policy as the dispatch seam. The framework's historical no-adapter default is Eufy counter segmentation, and live rollover + learned history must keep working byte-for-byte; a noop fallback would silently stop live rollover.NoopJobSegmenter("noop_job_fallback") stays registered for a future brand with no segmentable signal, but it is not the fallback. select_activestays a framework function, not on the engine. The job pipeline is three stages βfind_candidates β select_active β build_segments. The engine owns the two brand-specific stages (find_candidates,build_segments) plus the legacy one-shot compositionsegment_legacy.select_activeis pure ranking/filtering over the candidate shape (kind/confident/strength/id), so it is brand-agnostic and stays a direct framework import (counter_segmentation.select_active) β the external-review wizard's count/toggle re-selection logic is then uniform across brands.- Cross-engine contract. The
JobBoundaryCandidateandJobSegmentTypedDicts are the canonical shape every engine emits (the exact field union the counter primitives already produce).EufyCounterSegmenterdelegates verbatim to thecounter_segmentationprimitives, and itsDEFAULT_TUNING(gap_delayed_s35,gap_transit_s60,gap_plateau_s90,area_jump_m22.0,cadence_s30) is defined by reference to that module's constants, so the Eufy path can't drift. - The Eufy
kindvocabulary ("wash_plateau"/"transit"/"area_jump"/"weak") is produced byfind_candidatesand referenced at the Eufy-specific call sites (liverollover_kinds, the legacy{"wash_plateau","area_jump"}filter). A future brand supplies its own engine and its own kind literals at those sites β the documented extension point; no kind indirection is built into the seam.
Dispatch-engine specifics (queue/dispatch_engines.py):
- Beyond
template+ field map, a dispatch engine also declares its job model.DispatchEngine.job_modelis"atomic_batch"(one dispatch of a fixed room set β the default, mixed in by_SinglePhaseMixin) or"sequenced"(a logical job is an ordered list of phases, each its own dispatch that finalizes like a one-room atomic sub-job).build_phases()returns the ordered per-phase payload envelopes; the default is a single phase ==build_payload()output, so an atomic engine is exactly a one-phase sequenced engine and the framework treats both uniformly. build_phases(strict_order=True)turns a flat-id engine into a per-room sequenced job. A flat-id batch shape (generic_room_ids/ itsroborock_segment_cleannaming subclass) path-optimizes and ignores the dispatched order for a multi-room batch. Withstrict_orderset,GenericRoomIdsEngine.build_phases()instead emits one single-segment phase per resolved room in queue order β the sequenced job model then cleans them strictly in order, and each phase carries its own room's passes (the batch path otherwise collapses passes to one max-wins value). This is the shipping Roborock opt-in (dispatch_engines.py:79-98, 219-281, 284).- Per-phase watchdog timing is adapter-declarable. The framework re-dispatches each strict-order phase from a background task (the device just docked after the previous room and ignores a clean sent at that instant) and verifies it actually started, retrying if not β the retry loop doubling as the per-phase watchdog. The timing knobs live under
dispatch.phase_timing:settle_seconds/dock_settle_seconds/verify_seconds/confirm_seconds/poll_seconds+max_attempts.core/manager.py::_phase_timingmerges the adapter's declared overrides over the in-core_PHASE_*defaults per key, so a brand whose post-dock transient differs declares only what it needs and anything omitted stays byte-identical to the defaults. The whole mode is gated oncapabilities.honors_clean_orderbeingFalse(a path-optimizing brand); an order-honoring brand like Eufy never enters it. - The strict-order phase logic lives in
jobs/phase_runner.py::PhaseRunner(bundled subsystem), not the manager. The phase advance/finalize decision at the completion hook isPhaseRunner.maybe_advance_phase;EufyVacuumManager.maybe_advance_phaseis a one-line delegator that calls it. The watchdog itself β the background re-dispatch + settle/verify/retry loop and per-phase timing capture (_run_advanced_phase,_await_phase_started,_dispatch_active_phase,_vacuum_started_cleaning) β is also onPhaseRunner(constructed asself.phase_runner). What stays oncore/manager.pyis only the timing config: the_PHASE_*module constants and the_phase_timingresolverPhaseRunnerreads.
For the per-field documentation of dispatch (including phase_timing and the strict-order keys) see Adapter config reference Β§13.
3. Config Schema (config_schema.py)¶
ADAPTER_CONFIG_SCHEMA is a single dict defining all valid top-level blocks. The 21 top-level keys:
| Block | Description |
|---|---|
adapter_id |
str β unique brand identifier |
source |
"code" or "config" |
display_name |
Human-readable adapter name |
brand |
Short brand/app name the card uses in copy (generic phrasing when absent) |
entities |
Entity ID map (18 entity keys) |
vocabulary |
State string sets and card dropdown options |
completion |
Completion signal configuration |
charging |
Charging detection configuration |
error_tracking |
Error channel configuration |
dock_events |
Dock event recording configuration |
post_job_wash_amendment |
Post-job mop-wash amendment configuration |
discovery |
Room discovery source and cadence |
setup |
Adapter-specific setup step list |
dispatch |
Room-clean command template and field mapping |
capabilities |
Detected capability flags |
live_transition |
Live room-rollover orchestration (enabled / rollover_kinds / native_transition_source) |
external_mid_run_statuses |
task_status strings = robot docked mid-run and will resume (holds the external run open instead of closing at the dock) |
settings_selects |
Global select entities for recovering per-room settings on external (app-started) runs β canonical key β {entity_id, value_map} |
maintenance_components |
Consumable component definitions |
upkeep_catalog |
Per-model upkeep guide library |
water_model_configs |
Tank capacity and water usage constants |
Note: the 21 keys above are the complete set in
ADAPTER_CONFIG_SCHEMA. Several blocks the Eufy adapter actually declares are not in the schema dict βmapping,job_segmenter,room_profiles,map_state_source,map_render,room_attribution,anomaly, andwash_frequency_bounds. The schema walker iterates the schema's keys, so extra blocks are simply ignored by the schema (declaring them there is a deferred follow-up)._validate_adapter()nonetheless validatesmapping,job_segmenter,room_attribution, androom_profilesopportunistically when present (see Β§2.2 and Β§2.4); the remaining schema-absent blocks carry orchestration knobs with no validation rule yet. The Eufy CV map segmenter lives inadapters/eufy/segmentor.py; the Eufy counter/run segmenter engine iseufy_counter_v1inlearning/job_segmenter_engines.py.
3.1 entities block (18 keys)¶
| Key | Domain | Description |
|---|---|---|
task_status |
sensor | Vacuum task/operation state |
dock_status |
sensor | Dock station state |
active_map |
sensor | Currently active map ID |
active_cleaning_target |
sensor | Room(s) currently being cleaned |
cleaning_time |
sensor | Total cleaning duration in seconds |
cleaning_area |
sensor | Total area cleaned in mΒ² |
battery |
sensor | Battery percentage |
error_message |
sensor | Current error message |
charging |
binary_sensor | Is the vacuum currently charging? |
wash_frequency_mode |
select | Mop wash frequency mode |
wash_frequency_value_time |
number | Mop wash frequency value (minutes) |
dry_duration |
select | Mop dry duration setting |
water_level |
sensor or select | Station water level |
robot_position_x |
sensor | X coordinate (vacuum space) |
robot_position_y |
sensor | Y coordinate (vacuum space) |
work_mode |
sensor | Current work/drive mode |
cleaning_intensity |
select | Suction/cleaning intensity |
scene_select |
select | Vendor-app scenes select (eufy-clean select.<object_id>_scene); options are saved app scenes and selecting one runs it immediately; surfaced on the dashboard snapshot for the card's App-scenes run-launcher; absent (Roborock) hides the group. |
3.2 vocabulary block¶
State string sets (all normalized to lowercase before matching unless noted):
| Key | Type | Description |
|---|---|---|
hard_service_states |
list[str] | Dock states that block manual actions |
drying_states |
list[str] | Dock states that indicate active drying |
active_run_task_states |
list[str] | Task states that count as "active run" |
not_error_sentinels |
list[str] | error_message values that mean "no error" |
blocked_work_mode_states |
list[str] | Work modes that block queue-engine jobs |
blocked_task_status_states |
list[str] | Task status values that block queue-engine jobs |
blocked_dock_status_states |
list[str] | Dock status values that block queue-engine jobs |
cancel_service_exclusion_states |
list[str] | Task status values that explain early return as service (not cancel) |
cancel_detection_states |
dict[str, Any] | Normalized task_status transition strings the cancel detector matches: active (cleaning state β string or list), returning (return-to-dock state), paused; a cancel is activeβreturning or pausedβreturning |
water_level_aliases |
dict[str, str] | Brand display strings β canonical water level keys |
wash_frequency_mode_aliases |
dict[str, str] | Brand display strings β canonical frequency keys |
clean_mode_aliases |
dict[str, str] | Brand clean-mode display strings β canonical codes (e.g. "vacuum and mop" β vacuum_mop) the card vocab is keyed on |
clean_intensity_aliases |
dict[str, str] | Brand clean-intensity display strings β canonical codes (may be empty when values already slug to the code) |
fan_speed_aliases |
dict[str, str] | Brand suction/fan-speed display strings β canonical codes (e.g. "boostiq" β boost) |
clean_mode_options |
list[{value, label}] | Card dropdown options for clean mode |
fan_speed_options |
list[{value, label}] | Card dropdown options for fan speed |
water_level_options |
list[{value, label}] | Card dropdown options for water level |
clean_intensity_options |
list[{value, label}] | Card dropdown options for clean intensity |
3.3 dispatch block¶
Controls how room-clean commands are assembled:
| Field | Eufy value | Description |
|---|---|---|
template |
"eufy_room_clean" |
Which payload template to use |
service_domain |
"vacuum" |
HA service domain |
service_name |
"send_command" |
HA service name |
command |
"room_clean" |
Command string within the service call |
map_id_field |
"map_id" |
Top-level payload field for map ID |
map_id_type |
"int" |
Cast map_id to this type before sending |
room_id_field |
"id" |
Per-room field for room ID |
clean_passes_field |
"clean_times" |
Per-room field for clean passes |
rooms_field |
"rooms" |
Top-level payload field for rooms array |
room_fields |
dict | Per-room field renames and value_map transforms |
room_fields entry:
{
"fan_speed": {
"field_name": "fan_speed", # target field name in the API payload
"value_map": None, # None = pass-through; dict = rename values
}
}
Built-in dispatch templates:
| Template | Brand | Payload shape |
|---|---|---|
eufy_room_clean |
Eufy | {map_id, rooms: [{id, clean_times, fan_speed, ...}]} |
roborock_segment_clean |
Roborock | Segment-ID based |
dreame_room_clean |
Dreame | Dreame-specific |
generic_room_ids |
Any | Room ID list only |
3.4 maintenance_components block¶
Dict keyed by component_id. Each entry:
| Field | Type | Description |
|---|---|---|
sensor_suffix |
str | None | Suffix used to build the upstream sensor entity ID |
proxy_for |
str | None | If set, this component aliases another component's sensor |
default_interval_hours |
float | Factory replacement interval |
max_interval_hours |
float | Maximum allowed user-override interval |
label |
str | Display name |
icon |
str | MDI icon |
reset_button |
dict | None | Upstream replacement-counter reset button resolution (entity_suffixes + token_sets); absent = no reset button |
3.5 capabilities block¶
Boolean flags set by detect_capabilities() at adapter registration time:
| Flag | Description |
|---|---|
supports_mop_features |
Vacuum has mop hardware |
supports_water_control |
Water level can be programmatically set |
supports_path_control |
Cleaning path type can be set |
supports_edge_mopping |
Edge mopping setting is available |
supports_mop_wash |
Dock can auto-wash the mop |
supports_mop_dry |
Dock can auto-dry the mop |
supports_empty_dust |
Dock can auto-empty the dustbin |
supports_robot_position |
Position X/Y sensors are present |
supports_station_water |
Station water level sensor is present |
Note β entity-detected flags are not the whole block. The nine flags above are what
detect_capabilities()can probe from the HA entity registry and state machine. The samecapabilitiesblock also carries adapter-literal behavioral flags that no entity probe can see β they describe firmware behavior and are set directly in the adapter config (e.g. the Roborock S6 adapter atadapters/roborock/adapter.py):honors_clean_order,supports_room_profiles,position_lock_reliable, androoms_unique_per_job(plus the Roborock zone capssupports_zone_cleanandzone_maxatadapters/roborock/adapter.py). See Adapter config reference Β§14 for the full set and each flag's default and effect.Do not treat
supports_base_stationandsupports_map_boundsas adapter-literal capability flags β no adapter config sets them. They are computed at snapshot time incore/manager.py::get_dashboard_snapshot:supports_base_station(line 3648) fromdock_events.enabledOR the mop-wash / mop-dry / empty-dust / station-water caps, andsupports_map_bounds(line 3659) frommapping.segmenter_engine != "noop_fallback".See also: 22-adapter-config-reference for the complete field-by-field documentation of every block (
entities,vocabulary,dispatch,maintenance_components,capabilities, and all sub-schemas).
4. Config Loader (config_loader.py)¶
4.1 Startup loading¶
Called from async_setup_entry before code adapter registration. Reads data["adapters"] β a dict of {vacuum_entity_id: config_dict} stored by the UI wizard β and calls register_adapter_config() for each. Returns the count of successfully registered configs.
Code adapters registered afterward overwrite stored configs for the same vacuum entity ID. This means code adapters always take precedence.
4.2 Save / delete / read¶
Writes todata["adapters"][vacuum_entity_id]. Caller must call manager.async_save() and register_adapter_config() separately.
Removes from data["adapters"]. Returns True if removed, False if not present. Caller handles save.
Read-only. Returns stored config or None.
5. Eufy Adapter (adapters/eufy/adapter.py)¶
The Eufy adapter is the reference implementation of ADAPTER_CONFIG_SCHEMA. Every field maps to a measured or observed value.
5.1 Entry point¶
Called once per managed vacuum at startup from async_setup_entry. Idempotent β re-calling overwrites the previous registration.
5.2 Assembly steps¶
- Read
vacuum.attributes.detected_modelto determinemodel_familyvia_detect_model_family(). - Build
entity_candidatesdict (two naming-variant candidates per entity where robovac_mqtt uses different suffixes between versions). - Build
capability_hintsdict β model-based boolean hints fordetect_capabilities(). - Call
detect_capabilities(hass, *, vacuum_entity_id, detected_model, entity_candidates, model_family, capability_hints, maintenance_components)(all args afterhassare keyword-only) β probes the HA entity registry and state machine; returns capability flags and resolved entity IDs. - Build the full
configdict from all sub-modules:entities.py,buttons.py,vocabulary.py,maintenance_components.py,upkeep_catalog.py,upkeep_guides.py,water_config.py,constants.py. - Strip
Nonevalues from the entities dict (absent entities degrade gracefully per the schema). - Call
register_adapter_config(vacuum_entity_id, config).
5.3 Eufy-specific sub-modules¶
| Module | Exported symbols |
|---|---|
adapters/eufy/const.py |
ADAPTER_ID, STORAGE_KEY |
adapters/eufy/constants.py |
POST_JOB_AMENDMENT_MIN_WASH_INTERVAL_SECONDS, POST_JOB_AMENDMENT_TIMEOUT_SECONDS |
adapters/eufy/entities.py |
build_entity_id(), all SUFFIX_* and DOMAIN_* constants |
adapters/eufy/vocabulary.py |
HARD_SERVICE_STATES, DRYING_STATES, ACTIVE_RUN_TASK_STATES, HA_ACTIVE_VACUUM_STATES, DOCK_EVENT_TRIGGERS, WATER_LEVEL_ALIASES, WASH_FREQUENCY_MODE_ALIASES, CLEAN_MODE_ALIASES, CLEAN_INTENSITY_ALIASES, FAN_SPEED_ALIASES, NOT_ERROR_SENTINELS, CANCEL_SERVICE_EXCLUSION_STATES |
adapters/eufy/maintenance_components.py |
MAINTENANCE_COMPONENTS |
adapters/eufy/model_catalog.py |
detect_model_family() |
adapters/eufy/upkeep_catalog.py |
UPKEEP_GUIDE_FAMILY_NAMES, UPKEEP_MODEL_GUIDE_FAMILIES, UPKEEP_MODEL_NAMES |
adapters/eufy/upkeep_guides.py |
UPKEEP_GUIDE_LIBRARY |
adapters/eufy/water_config.py |
WATER_MODEL_CONFIGS |
adapters/eufy/buttons.py |
DOCK_ACTION_CANDIDATES, DOCK_ACTION_TOKENS, RESET_CANDIDATES, RESET_TOKENS β dock-action and maintenance-reset button entity-resolution candidates / token-sets |
adapters/eufy/discovery.py |
get_active_map_id(), discover_rooms_for_vacuum() β Eufy room-discovery helpers |
adapters/eufy/lifecycle.py |
_get_lifecycle_watch_entities(), _completed_finalize_signals(), _active_cleaning_target_cleared() β translate Eufy entity naming + state vocabulary into the framework lifecycle listener's signals |
adapters/eufy/segmentor.py |
detect_room_segments() β Eufy CV map-segmentation pipeline (the brand's map segmenter, eufy_cv_v1; distinct from the counter/run segmenter eufy_counter_v1 in learning/job_segmenter_engines.py β see Β§2.4) |
5.4 Entity ID construction¶
build_entity_id(vacuum_entity_id, suffix, domain="sensor") derives an entity ID using the object_id_suffix strategy:
object_id = vacuum_entity_id.split(".", 1)[1] # e.g. "alfred"
entity_id = f"{domain}.{object_id}_{suffix}" # e.g. "sensor.alfred_task_status"
6. Startup Registration Order¶
The two-phase registration order at async_setup_entry time:
1. load_stored_adapter_configs(hass, data)
β registers any UI-wizard-built configs first
2. register_eufy_adapter_for_vacuum(hass, vacuum_entity_id) [for each managed vacuum]
β overwrites stored configs; code adapters always win
This order means: if a user built a custom adapter config via the UI wizard and then the Eufy code adapter is also registered, the code adapter takes precedence. The stored config is not deleted β it persists for reference and is used if the code adapter is removed.
7. Porting to a New Brand¶
To add a new brand adapter:
- Create
adapters/{brand}/adapter.pywith aregister_{brand}_adapter_for_vacuum(hass, vacuum_entity_id)function. - Build the config dict using
ADAPTER_CONFIG_SCHEMAas the reference. Every framework-read field must be present; card-only fields (vocabulary.clean_mode_options, etc.) are optional. - Set
dispatch.templateto one of the four built-in templates, or add a new template to the dispatch engine. - Pick the two segmenter engines (see Β§2.4). Declare
mapping.segmenter_engine(ornoop_fallbackif the brand yields no map image) andjob_segmenter.engine(ornoop_job_fallbackif the brand emits no per-room run signal). For Eufy these areeufy_cv_v1andeufy_counter_v1; a brand whose boundary detection differs registers its own engine in the relevant registry and names it here. Optionally declareroom_profilesto override the framework's default profile vocabulary (an absent block uses the in-code defaults verbatim). - Register via
register_adapter_config(vacuum_entity_id, config)at startup. - The adapter's
setup.stepsdeclaration controls which setup-wizard screens the user sees (seesetup/drift.py).
See the porting guide for the complete porting walkthrough.