Rename range sliders to lower/upper + friendlier new-profile defaults

Device page sorts entities alphabetically by name, so 'Min/Max …' always
listed max above min ('a' < 'i'). Rename the four range numbers to
'Brightness lower/upper' and 'Color temp lower/upper' so each quantity's
lower bound sorts first. Pin suggested_object_id to the field key so new
profiles keep slugging number.<profile>_min_brightness etc., identical to
profiles created before the rename; existing entity_ids are registry-
preserved via unchanged unique_ids.

New-profile defaults: brightness 5-100% -> 10-90% (5% proved too dim on
gloomy days; 90% leaves manual-boost headroom), min color temp 2200K ->
2000K (deeper warm tone won in practice). Existing profiles unaffected —
saved options and restored slider values take precedence.

Specs synced: runtime-range-controls naming table + scenarios,
output-sensors collision references. Two new tests cover the slug pin
and the lower/upper friendly-name composition.
This commit is contained in:
Casey 2026-06-05 10:49:40 +02:00
commit ada3538449
8 changed files with 73 additions and 27 deletions

View file

@ -51,20 +51,22 @@ DOCS[CONF_INITIAL_TRANSITION] = "Fade time when a light first turns on, in secon
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
DOCS[CONF_INTERVAL] = "How often to recompute and re-apply the curve, in seconds."
CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100
# CDiT default: 90 instead of upstream's 100. Leaves headroom so a manual
# "brighter please" bump above the curve is always possible.
CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 90
DOCS[CONF_MAX_BRIGHTNESS] = "Brightness at the peak of the day, in percent."
CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500
DOCS[CONF_MAX_COLOR_TEMP] = "Color temperature at the peak of the day, in Kelvin."
# CDiT default: 5 instead of upstream's 1. 1% reads as off on most bulbs;
# 5% is the dim-but-visible floor.
CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 5
# CDiT default: 10 instead of upstream's 1. 1% reads as off on most bulbs
# (and 5% proved too dim on gloomy days); 10% is the usable floor.
CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 10
DOCS[CONF_MIN_BRIGHTNESS] = "Brightness during the night, in percent."
# CDiT default: 2200 K instead of upstream's 2000 K. 2000 K reads as
# sodium-vapor orange; 2200 K is a softer warm-lamp tone.
CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2200
# CDiT default: 2000 K (matches upstream). Earlier the fork used 2200 K
# ("sodium-vapor orange" worry), but in practice the deeper warm tone won.
CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000
DOCS[CONF_MIN_COLOR_TEMP] = "Color temperature during the night, in Kelvin."
CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False
@ -165,12 +167,14 @@ UNDO_UPDATE_LISTENER = "undo_update_listener"
# `field_key` becomes the entity's unique-id suffix; the `conf_key` ties
# the entity back to its initial-seed value in `entry.options`.
# Design decisions 5, 6, 11 — see add-runtime-range-controls/design.md.
# Naming: "lower/upper" (not "min/max") so the device page — which sorts
# entities alphabetically by name — lists min before max for each quantity.
RANGE_ENTITIES: list[dict[str, Any]] = [
{
"field_key": "min_brightness",
"conf_key": CONF_MIN_BRIGHTNESS,
"default": DEFAULT_MIN_BRIGHTNESS,
"name": "Min brightness",
"name": "Brightness lower",
"native_min": 1,
"native_max": 100,
"step": 1,
@ -181,7 +185,7 @@ RANGE_ENTITIES: list[dict[str, Any]] = [
"field_key": "max_brightness",
"conf_key": CONF_MAX_BRIGHTNESS,
"default": DEFAULT_MAX_BRIGHTNESS,
"name": "Max brightness",
"name": "Brightness upper",
"native_min": 1,
"native_max": 100,
"step": 1,
@ -192,7 +196,7 @@ RANGE_ENTITIES: list[dict[str, Any]] = [
"field_key": "min_color_temp",
"conf_key": CONF_MIN_COLOR_TEMP,
"default": DEFAULT_MIN_COLOR_TEMP,
"name": "Min color temp",
"name": "Color temp lower",
"native_min": 1000,
"native_max": 10000,
"step": 100,
@ -203,7 +207,7 @@ RANGE_ENTITIES: list[dict[str, Any]] = [
"field_key": "max_color_temp",
"conf_key": CONF_MAX_COLOR_TEMP,
"default": DEFAULT_MAX_COLOR_TEMP,
"name": "Max color temp",
"name": "Color temp upper",
"native_min": 1000,
"native_max": 10000,
"step": 100,

View file

@ -100,6 +100,16 @@ class AdaptiveRangeNumber(RestoreNumber):
# async_added_to_hass overrides with a restored or just-saved value.
self._attr_native_value = self._options_value()
@property
def suggested_object_id(self) -> str | None:
"""Pin entity_id slugs to the field key (e.g. ``_min_brightness``).
The display names use "lower/upper" wording (device-page sort order),
but entity_ids stay on the min/max field keys so new profiles slug
identically to ones created before the rename.
"""
return self._field_key
@property
def device_info(self) -> DeviceInfo:
"""Group with the profile's switches under one device."""

View file

@ -259,16 +259,16 @@
"entity": {
"number": {
"min_brightness": {
"name": "Min brightness"
"name": "Brightness lower"
},
"max_brightness": {
"name": "Max brightness"
"name": "Brightness upper"
},
"min_color_temp": {
"name": "Min color temp"
"name": "Color temp lower"
},
"max_color_temp": {
"name": "Max color temp"
"name": "Color temp upper"
}
},
"sensor": {

View file

@ -247,16 +247,16 @@
"entity": {
"number": {
"min_brightness": {
"name": "Min brightness"
"name": "Brightness lower"
},
"max_brightness": {
"name": "Max brightness"
"name": "Brightness upper"
},
"min_color_temp": {
"name": "Min color temp"
"name": "Color temp lower"
},
"max_color_temp": {
"name": "Max color temp"
"name": "Color temp upper"
}
},
"sensor": {

View file

@ -139,7 +139,7 @@ The resulting friendly names SHALL follow this table for a profile named `Dining
| Output-color-temp sensor | `"Output color temp"` | `Dining MVP Output color temp` |
| Sun-elevation sensor | `"Sun elevation"` | `Dining MVP Sun elevation` |
The chosen role labels SHALL NOT collide with any existing entity's `_attr_name` on the same device (specifically: not `"Brightness"`, which is the adapt-brightness switch's role per `add-runtime-range-controls`, and not `"Min color temp"` / `"Max color temp"`, which are the range-number roles).
The chosen role labels SHALL NOT collide with any existing entity's `_attr_name` on the same device (specifically: not `"Brightness"`, which is the adapt-brightness switch's role per `add-runtime-range-controls`, and not `"Color temp lower"` / `"Color temp upper"`, which are the range-number roles).
#### Scenario: Friendly names compose from device name + sensor role
@ -155,7 +155,7 @@ The chosen role labels SHALL NOT collide with any existing entity's `_attr_name`
- **WHEN** all ten entities' friendly names are inspected
- **THEN** no two entities SHALL share the same friendly name
- **AND** specifically the adapt-brightness switch ("Dining MVP Brightness") and the output-brightness sensor ("Dining MVP Output brightness") SHALL be distinguishable strings
- **AND** the output-color-temp sensor ("Dining MVP Output color temp") SHALL be distinguishable from the "Min color temp" and "Max color temp" number entities
- **AND** the output-color-temp sensor ("Dining MVP Output color temp") SHALL be distinguishable from the "Color temp lower" and "Color temp upper" number entities
### Requirement: Ambient lux sensor mirrors the configured lux sensor's reading

View file

@ -116,10 +116,12 @@ The resulting friendly names SHALL follow this table for a profile named `Dining
| Master switch | `None` | `Dining MVP` |
| Adapt-brightness switch | `"Brightness"` | `Dining MVP Brightness` |
| Adapt-color switch | `"Color"` | `Dining MVP Color` |
| Min brightness number | `"Min brightness"` | `Dining MVP Min brightness` |
| Max brightness number | `"Max brightness"` | `Dining MVP Max brightness` |
| Min color temp number | `"Min color temp"` | `Dining MVP Min color temp` |
| Max color temp number | `"Max color temp"` | `Dining MVP Max color temp` |
| Min brightness number | `"Brightness lower"` | `Dining MVP Brightness lower` |
| Max brightness number | `"Brightness upper"` | `Dining MVP Brightness upper` |
| Min color temp number | `"Color temp lower"` | `Dining MVP Color temp lower` |
| Max color temp number | `"Color temp upper"` | `Dining MVP Color temp upper` |
The range numbers use "lower/upper" wording (not "Min/Max") so the HA device page — which sorts entities alphabetically by friendly name — lists each quantity's lower bound before its upper bound. The number entities SHALL additionally pin `suggested_object_id` to their field key (`min_brightness`, `max_brightness`, `min_color_temp`, `max_color_temp`) so newly created profiles slug the same entity_ids as profiles created before the rename.
Existing `unique_id`s SHALL remain unchanged; the entity registry SHALL preserve existing `entity_id`s for any deployed install.
@ -130,7 +132,8 @@ Existing `unique_id`s SHALL remain unchanged; the entity registry SHALL preserve
- **THEN** the master switch's friendly name SHALL be exactly "Dining MVP"
- **AND** the adapt-brightness switch's friendly name SHALL be exactly "Dining MVP Brightness"
- **AND** the adapt-color switch's friendly name SHALL be exactly "Dining MVP Color"
- **AND** the four range number entities' friendly names SHALL be "Dining MVP Min brightness", "Dining MVP Max brightness", "Dining MVP Min color temp", "Dining MVP Max color temp"
- **AND** the four range number entities' friendly names SHALL be "Dining MVP Brightness lower", "Dining MVP Brightness upper", "Dining MVP Color temp lower", "Dining MVP Color temp upper"
- **AND** the four range number entities' entity_ids SHALL slug from the field keys (e.g. `number.dining_mvp_min_brightness`), not from the display names
#### Scenario: Existing entity_ids survive the rename

View file

@ -27,7 +27,7 @@ T_SUNSET = dt.datetime(2026, 3, 21, 18, 0, 0, tzinfo=UTC)
@pytest.fixture
def settings() -> SunLightSettings:
"""Standard CDiT defaults — 5100 % brightness, 22005500 K."""
"""Fixed test range — 5-100 % brightness, 2200-5500 K (explicit, not DEFAULT_*)."""
return SunLightSettings(
name="test",
min_brightness=5,

View file

@ -79,6 +79,35 @@ async def test_four_range_entities_registered(hass) -> None:
assert eid.startswith("number.")
# ---------------------------------------------------------------------------
# Naming — "lower/upper" display names, but entity_ids slug from field keys
# (suggested_object_id pin keeps new profiles consistent with pre-rename ones)
# ---------------------------------------------------------------------------
async def test_entity_ids_slug_from_field_keys(hass) -> None:
"""Renamed display names must not leak into entity_id slugs."""
entry = await _setup_entry(hass)
for field_key in FIELD_KEYS:
eid = _resolve_entity_id(hass, entry, field_key)
assert eid == f"number.{PROFILE_NAME}_{field_key}"
async def test_friendly_names_use_lower_upper_wording(hass) -> None:
"""Device page sorts alphabetically; lower/upper puts min above max."""
expected_roles = {
"min_brightness": "Brightness lower",
"max_brightness": "Brightness upper",
"min_color_temp": "Color temp lower",
"max_color_temp": "Color temp upper",
}
entry = await _setup_entry(hass)
for field_key, role in expected_roles.items():
eid = _resolve_entity_id(hass, entry, field_key)
state = hass.states.get(eid)
assert state.attributes["friendly_name"] == f"{PROFILE_NAME} {role}"
# ---------------------------------------------------------------------------
# 7.3 — Same device as the profile's switches
# ---------------------------------------------------------------------------