mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-16 08:44:03 +02:00
Adds four live-tunable `number` entities per AL profile (min/max brightness, min/max color temp) that own the runtime curve values. Slider changes take effect on the next curve tick — no integration reload. State persists across HA restart via `RestoreNumber`. Curve math now reads `min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp` from the four runtime entities via the entity registry, falling back to `entry.options` when an entity is unavailable. The options flow seeds its four range fields from the current entity state so the dialog matches reality. Also fixes entity friendly names via HA's `has_entity_name` composition: profile "Dining MVP" now reads as "Dining MVP", "Dining MVP Brightness", "Dining MVP Color" — short enough for HA's tightest cards. `unique_id`s are unchanged so existing entity_ids stay stable. Manifest bumped to 2.1.0-cdit.1 (minor, no breaking changes). 14 new tests in `tests/test_number_platform.py`; 108 passing overall. OpenSpec change archived once 9.x live-HA verification completes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
10 KiB
1. Platform foundation — number.py skeleton and const.py constants
- 1.1 Add
Platform.NUMBERto thePLATFORMSlist in__init__.pyso HA forwardsasync_setup_entryto the new platform. [R1] - 1.2 In
const.py, add a mappingRANGE_ENTITIES(or four explicit constants) covering the four entities:unique_idsuffix, friendly-name slug,native_min,native_max,step,unit,icon. One source for both platform setup and tests. [R1, D5, D6] - 1.3 Create
custom_components/adaptive_lighting/number.pywith anasync_setup_entry(hass, config_entry, async_add_entities)that instantiates four entities (one per row inRANGE_ENTITIES) and callsasync_add_entities(entities). [R1] - 1.4 Update the existing
device_infoblock (or shared helper) so the new entities attach to the same(DOMAIN, entry.entry_id)device as the three switches. [R1]
2. Entity class — RestoreNumber subclass with seed logic
- 2.1 Define
AdaptiveRangeNumber(RestoreNumber)innumber.pywith_attr_has_entity_name = True,_attr_mode = NumberMode.SLIDER, and_attr_should_poll = False. Constructor takes(entry, field_key, native_min, native_max, step, unit, icon). [R1, R7, D6, D11] - 2.2 Implement
unique_idproperty asf"{entry.entry_id}_{field_key}". [R1, D5] - 2.3 Set
_attr_nameon each instance to the role label per the D11 table: "Min brightness", "Max brightness", "Min color temp", "Max color temp". The device's name carries the profile context — HA composes the full friendly name automatically. [R7, D11] - 2.4 Implement
async_added_to_hasswith the three-tier seed precedence: (a) preferentry.options[CONF_*]if its value is newer than the restored state (compare viaRestoreNumber.async_get_last_number_data().native_valueagainst options), (b) restored state, (c)entry.options[CONF_*]as the first-creation fallback. [R2, D2, D3] - 2.5 Implement
async_set_native_value(value)to set_attr_native_value, callasync_write_ha_state(), and return. The method SHALL NOT callhass.config_entries.async_update_entry(no write-through to options). [R4, D1, D2]
3. Curve math — read bounds from entities, fallback to options
- 3.1 Add a helper
_get_runtime_range(hass, entry, field_key)(inswitch.pyor a shared helper module) that doesstate = hass.states.get(f"number.adaptive_lighting_{slugify(entry.title)}_{field_key}")→ returnsint(state.state)if state is set and not unavailable, elseint(entry.options[CONF_*]), logging the fallback at DEBUG. [R3, D8, D9] - 3.2 In
AdaptiveSwitch._get_settings()(or whereverSunLightSettingsis constructed), replace the fourentry.options[CONF_*]reads formin_brightness,max_brightness,min_color_temp,max_color_tempwith calls to_get_runtime_range(...). [R3, MR4, D1, D8] - 3.3 Verify no other reads of these four CONF keys remain in the curve evaluation path (
brightness_pct,color_temp_kelvin,brightness_and_color,sun_position). Other reads (e.g., the options-flow schema seeding) are intentionally untouched here. [R3, MR4]
4. Options flow — seed the 4 range fields from entity state
- 4.1 Modify
_build_options_schema(current, ...)inconfig_flow.pyto accept the four range entity states (or read them inline viahass.states.get(...)). When an entity state is available, use it as the field default; otherwise fall back to the matchingentry.options[CONF_*]value. [R6, D4] - 4.2 In
async_step_init, compute the fourcurrent_*values once (using the helper from 4.1), then pass them into the schema builder. [R6, D4] - 4.3 Confirm the other ~14 fields still seed from
entry.optionsunchanged. [R6]
5. Wire-up — async_setup_entry sequencing
- 5.1 Confirm the order
async_setup_entrycallsasync_forward_entry_setups(entry, PLATFORMS)is unchanged — bothswitchandnumberplatforms set up in parallel. [R1] - 5.2 Verify the entity-unavailable fallback (Decision 9) actually fires during the brief race window where the switch starts evaluating before the number platform has registered all four entities. This is the natural state during a fresh
async_setup_entry; the curve math should not crash. [R3, D9]
6. Entity-naming hygiene — retrofit the three existing switches
- 6.1 In
switch.py, set_attr_has_entity_name = TrueonAdaptiveSwitch,AdaptColorSwitch, andAdaptBrightnessSwitch. [R7, D11] - 6.2 Set
_attr_nameper the D11 table:AdaptiveSwitch._attr_name = None(master takes device name),AdaptBrightnessSwitch._attr_name = "Brightness",AdaptColorSwitch._attr_name = "Color". Delete any code that hand-composes "Adaptive Lighting …" into the friendly name. [R7, D11] - 6.3 Confirm the shared
device_infoblock setsname = entry.title(orentry.data[CONF_NAME], whichever is the user-facing string). This is the anchor for the composed friendly names. [R7, D11] - 6.4 Verify
unique_ids are NOT changed by this group — only_attr_nameand_attr_has_entity_name. The entity registry must keep existing entity_ids stable. [R7, D11] - 6.5 Manual check on live HA after deploy: pre-existing entity_ids unchanged (no duplicates, no broken automations), friendly names now read as "Dining MVP Brightness" / "Dining MVP Color" instead of "Adaptive Lighting Adapt Brightness dining_mvp_lights". [R7, D11]
7. Tests — tests/test_number_platform.py
- 7.1 New test file
tests/test_number_platform.pywith autouse PHACC fixture fromconftest.py. [R1] - 7.2 Add test: creating a new config entry registers exactly four
numberentities owned by the entry. Assert the suffixes are_min_brightness,_max_brightness,_min_color_temp,_max_color_temp. [R1] - 7.3 Add test: each of the four entities is attached to the same device as the profile's switches. [R1]
- 7.4 Add test: brightness entities expose
native_min_value=1,native_max_value=100,native_step=1,native_unit_of_measurement="%",mode=NumberMode.SLIDER. Color-temp entities expose1000/10000/100/"K"/SLIDER. [R1, D6] - 7.5 Add test:
async_set_native_valueupdatesstatebut does not callhass.config_entries.async_update_entry(use a mock spy). [R4, D2] - 7.6 Add test: slider change does not invoke
async_unload_entry/async_setup_entry. [R4] - 7.7 Add test: simulating an HA restart — pre-seed
RestoreNumberstate to 30 formin_brightness, set up the entry, assert entity state is 30 (not the default 5 fromentry.options). [R2] - 7.8 Add test: options-flow save with new range values triggers a reload, and after the reload the entity state reflects the just-saved values (not the previously-restored values). [R5, D3]
- 7.9 Add test: opening the options flow seeds the four range fields from
hass.states.get(<entity_id>).state, not fromentry.options. Set the entity to 80, leave options at 100, assert the flow's schema default is 80. [R6, D4] - 7.10 Add test: opening the options flow when an entity is unavailable falls back to
entry.options[CONF_*]. [R6, D9] - 7.11 Add test: curve math reads runtime values — set
number.adaptive_lighting_<name>_max_brightnessto 70, setentry.options[CONF_MAX_BRIGHTNESS]to 100, run a curve evaluation at "peak day," assert the returned brightness is 70. [R3, MR4] - 7.12 Add test: curve math fallback — make the entity
unavailable, setentry.options[CONF_MAX_BRIGHTNESS]to 90, evaluate at peak day, assert brightness is 90 and a DEBUG log line names the missing entity. [R3, D9]
8. Translations and docs
- 8.1 Add
entity.number.min_brightness.name,_max_brightness,_min_color_temp,_max_color_tempkeys tostrings.jsonwith plain-language labels ("Min brightness," "Max brightness," etc.). [R1, polish] - 8.2 Mirror the additions in
translations/en.json. Other locales out of scope (covered bycomplete-i18n-translationsfollow-up). [R1, polish] - 8.3 Add an
entity.number.<key>.unit_of_measurementmapping if HA's frontend requires it for slider display (verify against current HA — likely auto-derived fromnative_unit_of_measurement). [R1] - 8.4 Add a short section to
README.mdunder "What's new in 2.1" naming the four entities, explaining the slider-vs-options-flow split ("sliders tune live; options-flow sets defaults; saving the dialog resets the sliders to the saved values"), and showing a one-line Lovelace YAML snippet (type: entitieswith the four range entities). [R5, R6, D3, polish] - 8.5 Append a
2.1.0-cdit.1entry toCHANGELOG.mdlisting: 4 new entities per profile, RestoreNumber persistence, curve math now reads from entities, options-flow open seeds from entities, no reload on slider change. [polish]
9. Manual verification on live HA
- 9.1 Deploy to
homeassistant.onca-blenny.ts.netvia HACS. Verify the fournumber.adaptive_lighting_*entities appear under each of the 6 profiles' devices. [R1] - 9.2 Move a slider on one profile via the dashboard. Verify (a) no integration reload occurs (check Integration page → no "reloading" banner; entity IDs unchanged), (b) the next curve tick uses the new value (watch the master switch's
brightness_pctattribute over ~90 s). [R3, R4] - 9.3 Open the options flow on the same profile. Verify the four range fields show the just-moved slider values, not the original setup defaults. [R6]
- 9.4 Save the options flow with different values. Verify the sliders snap to the new values after reload. [R5, D3]
- 9.5 Restart HA. Verify the slider values persist (RestoreNumber works). [R2]
10. Validation gate
- 10.1
openspec validate add-runtime-range-controls --strictreturns green. [polish] - 10.2
uv run pytest tests/test_number_platform.pypasses. Existing tests stay green (uv run pytest). [polish] - 10.3
./scripts/lintclean. [polish]