Spoolman Entity Integration#
Each Spoolman spool is exposed in Home Assistant as a device that owns a collection of sensor and select entities. Two platform files drive this: sensor.py creates read-only measurement and metadata sensors, and select.py creates a writable location select. All entity state comes from a shared SpoolManCoordinator that polls the Spoolman API for spools, filaments, extra fields, and locations each refresh cycle.
The dynamic listener pattern means new spools appear automatically in Home Assistant without a reload β as does any new location added in Spoolman. See Dynamic Entity Registration below for details.
Sensor Entities Per Spool#
The _build_entities_for_spool() helper assembles the full sensor stack for a single spool. All sensor classes live in the sensors/ directory and are exported via sensors/__init__.py.
Always created (regardless of data):
| Entity class | What it measures |
|---|---|
Spool | Primary device entity (color thumbnail) |
SpoolFlowRate | Flow rate |
SpoolEstimatedRunOut | Estimated run-out date |
SpoolUsedWeight | Used weight (g) |
SpoolRemainingLength | Remaining length (mm) |
SpoolUsedLength | Used length (mm) |
SpoolLocation | Location (read-only text) |
SpoolUsedPercentage | Percentage used |
SpoolId | Spoolman spool ID |
Conditionally created (only when the corresponding field is present on the spool or filament object): SpoolRegistered, SpoolFirstUsed, SpoolLastUsed, SpoolPrice, SpoolWeight, SpoolLotNumber, SpoolComment, FilamentDensity, FilamentDiameter, FilamentExtruderTemp, FilamentBedTemp, FilamentArticleNumber, FilamentName, FilamentMaterial, FilamentColorHex, VendorName, FilamentWeight .
Extra fields: One SpoolExtraField sensor is created per key in spool["extra"], tracked by (spool_id, field_key) tuple to avoid duplicates .
Entity pictures (100Γ100 px PNG) are generated for Spool and FilamentColorHex entities via _generate_entity_picture(), which supports single-color, coaxial, and longitudinal multi-color filaments.
SpoolLocation: Sensor vs. Select#
Location is represented by two distinct entities on the same spool device:
-
SpoolLocation(sensor) β A read-onlySensorEntitydefined insensors/spool_location.py. Itsstateproperty returnsspool.get("location", "Unknown"). This is useful for automations that react to where a spool is stored. -
SpoolLocationSelect(select) β A writableSelectEntitydefined inselect.py. Selecting a new option callsapi.patch_spool(spool_id, {"location": option})and immediately triggers a coordinator refresh. The icon for both ismdi:map-marker.
Both share the same unique_id pattern β spoolman_{entry_id}_spool_{id}_location β using sensor. and select. prefixes respectively .
Location Options: Canonical List vs. Derived Fallback#
The dropdown options for SpoolLocationSelect are resolved by _resolve_locations(coordinator_data, spools):
- Primary: uses
coordinator.data["locations"], populated each poll byGET /api/v1/location. This includes locations that are configured in Spoolman but have no spool assigned (e.g., empty AMS trays) β fixing the issue tracked in PR #839 . - Fallback: if the
/locationendpoint is absent (older Spoolman servers), derives the list from thelocationfield of all known spools .
SpoolLocationSelect._handle_coordinator_update() re-invokes _resolve_locations() on every coordinator update, so newly added locations appear in the dropdown without requiring a reload .
Dynamic Entity Registration#
Before PR #839, entities were only created for spools present at async_setup_entry time β new spools required a full integration reload. Both platforms now register a coordinator listener to detect and register entities on the fly.
In sensor.py :
- The
add_dynamic_entities()callback fires on each coordinator update. - It computes the diff between
coordinator.data["spools"]and theexisting_spool_idsset. - New spools are passed to
_async_add_new_spools(), launched viahass.async_create_task()so the synchronous callback can trigger the async image-generation path. - New extra-field sensors on already-known spools are also detected and registered in the same callback.
In select.py :
add_dynamic_selects()mirrors the same pattern, creating aSpoolLocationSelectfor each new spool ID.
Both listeners are registered once via coordinator.async_add_listener(...) at the end of async_setup_entry .
Key Files#
| File | Role |
|---|---|
custom_components/spoolman/sensor.py | async_setup_entry, _build_entities_for_spool, dynamic listener |
custom_components/spoolman/select.py | SpoolLocationSelect, _resolve_locations, dynamic listener |
custom_components/spoolman/sensors/spool_location.py | SpoolLocation read-only sensor |
custom_components/spoolman/sensors/__init__.py | Exports all 24+ sensor classes |
custom_components/spoolman/coordinator.py | SpoolManCoordinator._async_update_data β fetches spools, filaments, extra fields, locations |