mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-08 02:17:22 +02:00
Implement add-runtime-range-controls
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>
This commit is contained in:
parent
c1a98a5c91
commit
d5d3ecc9d4
21 changed files with 1280 additions and 65 deletions
|
|
@ -21,7 +21,7 @@ from .const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["switch"]
|
||||
PLATFORMS = ["switch", "number"]
|
||||
|
||||
# unique_id suffix(es) that this fork no longer creates. Any entity in the
|
||||
# registry whose unique_id ends with one of these strings AND that is owned
|
||||
|
|
@ -84,8 +84,7 @@ def _remove_orphan_sleep_entities(
|
|||
for entry in registry.entities.values()
|
||||
if entry.config_entry_id == config_entry.entry_id
|
||||
and any(
|
||||
entry.unique_id.endswith(suffix)
|
||||
for suffix in _REMOVED_UNIQUE_ID_SUFFIXES
|
||||
entry.unique_id.endswith(suffix) for suffix in _REMOVED_UNIQUE_ID_SUFFIXES
|
||||
)
|
||||
]
|
||||
for entity_id in entries_to_remove:
|
||||
|
|
@ -137,9 +136,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_forward_entry_unload(
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry,
|
||||
"switch",
|
||||
PLATFORMS,
|
||||
)
|
||||
data = hass.data[DOMAIN]
|
||||
if unload_ok:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ except ImportError: # pragma: no cover — HA < 2025.1 fallback for dev env
|
|||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import section
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
EntitySelector,
|
||||
|
|
@ -68,6 +69,7 @@ from .const import (
|
|||
DEFAULT_SUNSET_ENTITY,
|
||||
DEFAULT_TRANSITION,
|
||||
DOMAIN,
|
||||
RANGE_ENTITIES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
@ -152,7 +154,6 @@ def _build_options_schema(
|
|||
show_send_split_delay: bool,
|
||||
) -> vol.Schema:
|
||||
"""Build the sectioned options schema from the entry's current values."""
|
||||
|
||||
targets_section = section(
|
||||
vol.Schema(
|
||||
{
|
||||
|
|
@ -186,7 +187,10 @@ def _build_options_schema(
|
|||
): _color_temp_selector(),
|
||||
vol.Required(
|
||||
CONF_PREFER_RGB_COLOR,
|
||||
default=current.get(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR),
|
||||
default=current.get(
|
||||
CONF_PREFER_RGB_COLOR,
|
||||
DEFAULT_PREFER_RGB_COLOR,
|
||||
),
|
||||
): BooleanSelector(),
|
||||
},
|
||||
),
|
||||
|
|
@ -383,6 +387,24 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
|||
current = dict(conf.data)
|
||||
current.update(conf.options)
|
||||
|
||||
# Overlay live values from the four runtime range number entities
|
||||
# so the dialog matches what the user's lights are actually running
|
||||
# (spec R6, design D4). Other ~14 fields keep their `entry.options`
|
||||
# values from above.
|
||||
registry = er.async_get(self.hass)
|
||||
for row in RANGE_ENTITIES:
|
||||
unique_id = f"{conf.entry_id}_{row['field_key']}"
|
||||
entity_id = registry.async_get_entity_id("number", DOMAIN, unique_id)
|
||||
if entity_id is None:
|
||||
continue
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state is None or state.state in (None, "unavailable", "unknown"):
|
||||
continue
|
||||
try:
|
||||
current[row["conf_key"]] = int(float(state.state))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
flat = _flatten_sections(user_input)
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = (
|
|||
)
|
||||
|
||||
CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1
|
||||
DOCS[CONF_INITIAL_TRANSITION] = (
|
||||
"Fade time when a light first turns on, in seconds."
|
||||
)
|
||||
DOCS[CONF_INITIAL_TRANSITION] = "Fade time when a light first turns on, in seconds."
|
||||
|
||||
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
|
||||
DOCS[CONF_INTERVAL] = "How often to recompute and re-apply the curve, in seconds."
|
||||
|
|
@ -150,6 +148,54 @@ ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
|
|||
ATTR_ADAPTIVE_LIGHTING_MANAGER = "manager"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
# Runtime range number-entity declarations. Each tuple drives both the
|
||||
# number platform's entity creation and the curve-math read path. The
|
||||
# `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.
|
||||
RANGE_ENTITIES: list[dict[str, Any]] = [
|
||||
{
|
||||
"field_key": "min_brightness",
|
||||
"conf_key": CONF_MIN_BRIGHTNESS,
|
||||
"name": "Min brightness",
|
||||
"native_min": 1,
|
||||
"native_max": 100,
|
||||
"step": 1,
|
||||
"unit": "%",
|
||||
"icon": "mdi:brightness-3",
|
||||
},
|
||||
{
|
||||
"field_key": "max_brightness",
|
||||
"conf_key": CONF_MAX_BRIGHTNESS,
|
||||
"name": "Max brightness",
|
||||
"native_min": 1,
|
||||
"native_max": 100,
|
||||
"step": 1,
|
||||
"unit": "%",
|
||||
"icon": "mdi:brightness-7",
|
||||
},
|
||||
{
|
||||
"field_key": "min_color_temp",
|
||||
"conf_key": CONF_MIN_COLOR_TEMP,
|
||||
"name": "Min color temp",
|
||||
"native_min": 1000,
|
||||
"native_max": 10000,
|
||||
"step": 100,
|
||||
"unit": "K",
|
||||
"icon": "mdi:thermometer-low",
|
||||
},
|
||||
{
|
||||
"field_key": "max_color_temp",
|
||||
"conf_key": CONF_MAX_COLOR_TEMP,
|
||||
"name": "Max color temp",
|
||||
"native_min": 1000,
|
||||
"native_max": 10000,
|
||||
"step": 100,
|
||||
"unit": "K",
|
||||
"icon": "mdi:thermometer-high",
|
||||
},
|
||||
]
|
||||
|
||||
ATTR_ADAPT_COLOR = "adapt_color"
|
||||
DOCS[ATTR_ADAPT_COLOR] = "Adjust the color of supporting lights over the day."
|
||||
ATTR_ADAPT_BRIGHTNESS = "adapt_brightness"
|
||||
|
|
@ -161,7 +207,7 @@ DOCS[CONF_TURN_ON_LIGHTS] = "Also turn on any targeted lights that are currently
|
|||
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
|
||||
CONF_USE_DEFAULTS = "use_defaults"
|
||||
DOCS[CONF_USE_DEFAULTS] = (
|
||||
'Source for any field not supplied in this call: '
|
||||
"Source for any field not supplied in this call: "
|
||||
'"current" (keep existing values), "factory" (documented defaults), '
|
||||
'or "configuration" (the profile\'s own configured defaults).'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@
|
|||
"iot_class": "calculated",
|
||||
"issue_tracker": "https://github.com/CaseyRo/adaptive-lighting/issues",
|
||||
"requirements": ["ulid-transform"],
|
||||
"version": "2.0.0-cdit.1"
|
||||
"version": "2.1.0-cdit.1"
|
||||
}
|
||||
|
|
|
|||
155
custom_components/adaptive_lighting/number.py
Normal file
155
custom_components/adaptive_lighting/number.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"""Number platform for the Adaptive Lighting integration (CDiT fork).
|
||||
|
||||
Each config entry exposes four live-tunable sliders that own the runtime
|
||||
values the curve math reads on every tick:
|
||||
|
||||
- ``number.<profile>_min_brightness``
|
||||
- ``number.<profile>_max_brightness``
|
||||
- ``number.<profile>_min_color_temp``
|
||||
- ``number.<profile>_max_color_temp``
|
||||
|
||||
The entities extend ``RestoreNumber`` so values survive HA restarts without
|
||||
a separate ``Store`` helper. Slider changes do NOT write back to
|
||||
``entry.options`` (no integration reload). Options-flow saves reload the
|
||||
integration, and the resulting fresh entities prefer the just-saved
|
||||
``entry.options`` value over the restored state. See design.md decisions
|
||||
1-3 of the ``add-runtime-range-controls`` change.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberMode,
|
||||
RestoreNumber,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
from .const import DOMAIN, RANGE_ENTITIES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, # noqa: ARG001
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create the four range entities for this config entry."""
|
||||
entities = [
|
||||
AdaptiveRangeNumber(
|
||||
entry=config_entry,
|
||||
field_key=row["field_key"],
|
||||
conf_key=row["conf_key"],
|
||||
display_name=row["name"],
|
||||
native_min=row["native_min"],
|
||||
native_max=row["native_max"],
|
||||
step=row["step"],
|
||||
unit=row["unit"],
|
||||
icon=row["icon"],
|
||||
)
|
||||
for row in RANGE_ENTITIES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AdaptiveRangeNumber(RestoreNumber):
|
||||
"""Live-tunable slider for one of the four curve range values."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_mode = NumberMode.SLIDER
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
entry: ConfigEntry,
|
||||
field_key: str,
|
||||
conf_key: str,
|
||||
display_name: str,
|
||||
native_min: float,
|
||||
native_max: float,
|
||||
step: float,
|
||||
unit: str,
|
||||
icon: str,
|
||||
) -> None:
|
||||
"""Initialise a single range number entity."""
|
||||
self._entry = entry
|
||||
self._field_key = field_key
|
||||
self._conf_key = conf_key
|
||||
self._attr_name = display_name
|
||||
self._attr_translation_key = field_key
|
||||
self._attr_unique_id = f"{entry.entry_id}_{field_key}"
|
||||
self._attr_native_min_value = native_min
|
||||
self._attr_native_max_value = native_max
|
||||
self._attr_native_step = step
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._attr_icon = icon
|
||||
# Initial value falls back to the options snapshot until
|
||||
# async_added_to_hass overrides with a restored or just-saved value.
|
||||
self._attr_native_value = self._options_value()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Group with the profile's switches under one device."""
|
||||
profile_name = self._entry.data.get("name") or self._entry.title
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, profile_name)},
|
||||
name=profile_name,
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
def _options_value(self) -> float:
|
||||
"""Read the seed value from entry.options (typed cast)."""
|
||||
raw = self._entry.options.get(self._conf_key)
|
||||
if raw is None:
|
||||
raw = self._entry.data.get(self._conf_key, self._attr_native_min_value)
|
||||
return float(raw)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Seed the entity value with three-tier precedence.
|
||||
|
||||
(a) First-creation: no restored state → use ``entry.options[conf_key]``.
|
||||
(b) Restored state exists AND was persisted AFTER the entry was last
|
||||
modified → the user moved the slider since the last options-flow
|
||||
save; use the restored value (slider survives restart).
|
||||
(c) Restored state exists BUT the entry was modified AFTER the
|
||||
restored state was persisted → an options-flow save changed the
|
||||
value; use ``entry.options[conf_key]`` (just-saved wins).
|
||||
"""
|
||||
await super().async_added_to_hass()
|
||||
options_value = self._options_value()
|
||||
last_state = await self.async_get_last_state()
|
||||
if last_state is None or last_state.state in (None, "unknown", "unavailable"):
|
||||
self._attr_native_value = options_value
|
||||
return
|
||||
try:
|
||||
restored_value = float(last_state.state)
|
||||
except (TypeError, ValueError):
|
||||
self._attr_native_value = options_value
|
||||
return
|
||||
entry_modified = getattr(self._entry, "modified_at", None)
|
||||
if entry_modified is not None and entry_modified > last_state.last_updated:
|
||||
# Entry was edited (via options-flow save) after the entity's
|
||||
# last persist → the just-saved options value supersedes.
|
||||
self._attr_native_value = options_value
|
||||
else:
|
||||
self._attr_native_value = restored_value
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Persist the new slider value to entity state only.
|
||||
|
||||
Does NOT write to ``entry.options`` — that would trigger an
|
||||
``OptionsFlowWithReload`` reload on every slider tick. The
|
||||
RestoreNumber base class persists the value across restarts.
|
||||
"""
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
|
|
@ -243,5 +243,21 @@
|
|||
"incompatible_version": {
|
||||
"message": "Adaptive Lighting v{current_version} (CDiT fork) is incompatible with the existing config entry (version {entry_version}). Delete the entry from Settings → Devices & Services and create a new one."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"min_brightness": {
|
||||
"name": "Min brightness"
|
||||
},
|
||||
"max_brightness": {
|
||||
"name": "Max brightness"
|
||||
},
|
||||
"min_color_temp": {
|
||||
"name": "Min color temp"
|
||||
},
|
||||
"max_color_temp": {
|
||||
"name": "Max color temp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ async def handle_change_switch_settings(
|
|||
)
|
||||
|
||||
|
||||
async def async_setup_entry( # noqa: PLR0915
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
|
|
@ -356,6 +356,7 @@ async def async_setup_entry( # noqa: PLR0915
|
|||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
icon=ICON_COLOR_TEMP,
|
||||
display_name="Color",
|
||||
)
|
||||
adapt_brightness_switch = SimpleSwitch(
|
||||
which="Adapt Brightness",
|
||||
|
|
@ -363,6 +364,7 @@ async def async_setup_entry( # noqa: PLR0915
|
|||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
icon=ICON_BRIGHTNESS,
|
||||
display_name="Brightness",
|
||||
)
|
||||
switch = AdaptiveSwitch(
|
||||
hass,
|
||||
|
|
@ -733,6 +735,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
_only_once: bool = False
|
||||
_auto_reset_manual_control_time: float = 0
|
||||
|
||||
# Entity-name composition: device name carries the profile name; the
|
||||
# master switch's friendly name is just the device name. See design
|
||||
# decision D11 of add-runtime-range-controls.
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
_attr_icon = "mdi:weather-sunny-alert"
|
||||
|
||||
def __init__(
|
||||
|
|
@ -750,6 +758,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self.manager = manager
|
||||
self.adapt_color_switch = adapt_color_switch
|
||||
self.adapt_brightness_switch = adapt_brightness_switch
|
||||
self._config_entry = config_entry
|
||||
|
||||
data = validate(config_entry)
|
||||
|
||||
|
|
@ -832,14 +841,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._sunset_entity = data[CONF_SUNSET_ENTITY]
|
||||
self._expand_light_groups()
|
||||
|
||||
self._sun_light_settings = SunLightSettings(
|
||||
name=self._name,
|
||||
max_brightness=data[CONF_MAX_BRIGHTNESS],
|
||||
max_color_temp=data[CONF_MAX_COLOR_TEMP],
|
||||
min_brightness=data[CONF_MIN_BRIGHTNESS],
|
||||
min_color_temp=data[CONF_MIN_COLOR_TEMP],
|
||||
ramp_half_width_seconds=RAMP_HALF_WIDTH_SECONDS,
|
||||
)
|
||||
# Snapshot the four range values for the fallback path; the
|
||||
# `sun_light_settings` property rebuilds the dataclass on each tick
|
||||
# using live values from the number entities, falling back to these
|
||||
# snapshots when an entity is unavailable.
|
||||
self._fallback_min_brightness = data[CONF_MIN_BRIGHTNESS]
|
||||
self._fallback_max_brightness = data[CONF_MAX_BRIGHTNESS]
|
||||
self._fallback_min_color_temp = data[CONF_MIN_COLOR_TEMP]
|
||||
self._fallback_max_color_temp = data[CONF_MAX_COLOR_TEMP]
|
||||
_LOGGER.debug(
|
||||
"%s: Set switch settings for lights '%s'. now using data: '%s'",
|
||||
self._name,
|
||||
|
|
@ -894,10 +903,59 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
t_sunset = t_sunset + timedelta(days=1)
|
||||
return t_sunrise, t_sunset
|
||||
|
||||
def _get_runtime_range(self, field_key: str) -> int:
|
||||
"""Read the live curve bound from its number entity.
|
||||
|
||||
Looks up the entity by stable unique_id via the entity registry,
|
||||
reads the state-machine value, and falls back to the snapshot
|
||||
captured at setup when the entity is unavailable (spec R3, D8, D9).
|
||||
"""
|
||||
registry = entity_registry.async_get(self.hass)
|
||||
unique_id = f"{self._config_entry.entry_id}_{field_key}"
|
||||
entity_id = registry.async_get_entity_id("number", DOMAIN, unique_id)
|
||||
if entity_id is not None:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state is not None and state.state not in (
|
||||
None,
|
||||
"unavailable",
|
||||
"unknown",
|
||||
):
|
||||
try:
|
||||
return int(float(state.state))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
fallback = {
|
||||
"min_brightness": self._fallback_min_brightness,
|
||||
"max_brightness": self._fallback_max_brightness,
|
||||
"min_color_temp": self._fallback_min_color_temp,
|
||||
"max_color_temp": self._fallback_max_color_temp,
|
||||
}[field_key]
|
||||
_LOGGER.debug(
|
||||
"%s: runtime range entity unavailable for '%s' "
|
||||
"(unique_id=%s) — falling back to entry.options value %s",
|
||||
self._name,
|
||||
field_key,
|
||||
unique_id,
|
||||
fallback,
|
||||
)
|
||||
return int(fallback)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device if any."""
|
||||
return f"Adaptive Lighting: {self._name}"
|
||||
def sun_light_settings(self) -> SunLightSettings:
|
||||
"""Return a fresh `SunLightSettings` built from current entity states.
|
||||
|
||||
Reads the four runtime range values on every property access so curve
|
||||
evaluations always see the latest slider position. The dataclass init
|
||||
cost is microseconds — cheap enough to do every tick (D8).
|
||||
"""
|
||||
return SunLightSettings(
|
||||
name=self._name,
|
||||
max_brightness=self._get_runtime_range("max_brightness"),
|
||||
max_color_temp=self._get_runtime_range("max_color_temp"),
|
||||
min_brightness=self._get_runtime_range("min_brightness"),
|
||||
min_color_temp=self._get_runtime_range("min_color_temp"),
|
||||
ramp_half_width_seconds=RAMP_HALF_WIDTH_SECONDS,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
|
|
@ -1120,7 +1178,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if events is None:
|
||||
return None
|
||||
t_sunrise, t_sunset = events
|
||||
self._settings = self._sun_light_settings.get_settings(
|
||||
self._settings = self.sun_light_settings.get_settings(
|
||||
transition,
|
||||
t_sunrise,
|
||||
t_sunset,
|
||||
|
|
@ -1316,7 +1374,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if events is not None:
|
||||
t_sunrise, t_sunset = events
|
||||
self._settings.update(
|
||||
self._sun_light_settings.get_settings(
|
||||
self.sun_light_settings.get_settings(
|
||||
transition,
|
||||
t_sunrise,
|
||||
t_sunset,
|
||||
|
|
@ -1417,7 +1475,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
force=True,
|
||||
)
|
||||
|
||||
def fire_manual_control_event( # noqa: ARG002
|
||||
def fire_manual_control_event(
|
||||
self,
|
||||
light: str,
|
||||
context: Context,
|
||||
|
|
@ -1439,6 +1497,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
class SimpleSwitch(SwitchEntity, RestoreEntity):
|
||||
"""Representation of a Adaptive Lighting switch."""
|
||||
|
||||
# Entity-name composition: device name carries the profile name, this
|
||||
# switch's `_attr_name` carries only its role ("Brightness" / "Color").
|
||||
# HA composes "<device> <role>" automatically. D11.
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
which: str,
|
||||
|
|
@ -1446,8 +1509,14 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
icon: str,
|
||||
display_name: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Adaptive Lighting switch."""
|
||||
"""Initialize the Adaptive Lighting switch.
|
||||
|
||||
`which` is the legacy slug token used for the entity's unique_id
|
||||
(kept stable to preserve existing entity_ids). `display_name`
|
||||
overrides the user-facing role label; falls back to `which`.
|
||||
"""
|
||||
self.hass = hass
|
||||
data = validate(config_entry)
|
||||
self._icon = icon
|
||||
|
|
@ -1455,14 +1524,11 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
self._which = which
|
||||
self._config_name = data[CONF_NAME]
|
||||
self._unique_id = f"{self._config_name}_{slugify(self._which)}"
|
||||
# Internal label for log messages (keep includes profile + role).
|
||||
self._name = f"Adaptive Lighting {which}: {self._config_name}"
|
||||
self._attr_name = display_name or which
|
||||
self._initial_state = initial_state
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of entity."""
|
||||
|
|
@ -1485,7 +1551,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
identifiers={
|
||||
(DOMAIN, self._config_name),
|
||||
},
|
||||
name=f"Adaptive Lighting: {self._config_name}",
|
||||
name=self._config_name,
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -243,5 +243,21 @@
|
|||
"incompatible_version": {
|
||||
"message": "Adaptive Lighting v{current_version} (CDiT fork) is incompatible with the existing config entry (version {entry_version}). Delete the entry from Settings → Devices & Services and create a new one."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"min_brightness": {
|
||||
"name": "Min brightness"
|
||||
},
|
||||
"max_brightness": {
|
||||
"name": "Max brightness"
|
||||
},
|
||||
"min_color_temp": {
|
||||
"name": "Min color temp"
|
||||
},
|
||||
"max_color_temp": {
|
||||
"name": "Max color temp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue