Implement add-output-sensors: 36/41 tasks complete
Three sensor entities per AL profile expose the curve's current outputs
plus actual sun elevation as graphable numerics with
SensorStateClass.MEASUREMENT, so HA's recorder + apexcharts-card chart
them natively:
- sensor.<profile>_output_brightness (% — from self._settings)
- sensor.<profile>_output_color_temp (K — from self._settings)
- sensor.<profile>_sun_elevation (° — from sun.sun.attributes.elevation)
Architecture: master switch publishes computed outputs to
hass.data[DOMAIN][entry_id]["outputs"] after each curve tick, then fires
a per-entry dispatcher signal. Sensors subscribe and read from the cache.
Single computation path, push-based updates, no polling.
The existing master switch attributes (brightness_pct, color_temp_kelvin,
synthetic sun_position in [-1,+1]) are unchanged — sensors are purely
additive. Manifest bumped to 2.2.0-cdit.1 (minor; no breaking changes).
Artifact realignment along the way: Q2 answer assumed sun_position was
Sun2 elevation degrees, but reading color_and_brightness.py revealed
it's a synthetic float in [-1,+1] derived from the brightness curve.
Switched to pulling actual elevation from sun.sun (Decision 8 added),
renamed sensor from sun_position to sun_elevation. Friendly names use
asymmetric "Output" prefix to dodge collision with the adapt-brightness
switch and Min/Max color-temp numbers.
Remaining: group 6 (manual live-HA verification on
homeassistant.onca-blenny.ts.net) — requires HACS deploy.
15 new sensor tests pass; full suite green (123/123). openspec
validate --strict green. ruff clean on new files.
2026-05-21 10:11:52 +02:00
|
|
|
"""Sensor platform for the Adaptive Lighting integration (CDiT fork).
|
|
|
|
|
|
|
|
|
|
Each config entry exposes three read-only output sensors that publish the
|
|
|
|
|
curve's current target values + the actual sun elevation:
|
|
|
|
|
|
|
|
|
|
- ``sensor.<profile>_output_brightness`` (% — from self._settings["brightness_pct"])
|
|
|
|
|
- ``sensor.<profile>_output_color_temp`` (K — from self._settings["color_temp_kelvin"])
|
|
|
|
|
- ``sensor.<profile>_sun_elevation`` (° — from sun.sun.attributes["elevation"])
|
|
|
|
|
|
|
|
|
|
The sensors update via a per-entry dispatcher signal fired by the master
|
|
|
|
|
switch after every curve evaluation. They are pure readers — no curve math
|
|
|
|
|
runs in the sensor class. See ``add-output-sensors/design.md`` decisions
|
|
|
|
|
2, 3, and 8.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
|
|
from homeassistant.components.sensor import SensorEntity, SensorStateClass
|
|
|
|
|
from homeassistant.core import callback
|
|
|
|
|
from homeassistant.helpers.device_registry import DeviceEntryType
|
|
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
|
|
|
from homeassistant.helpers.entity import DeviceInfo
|
|
|
|
|
|
2026-05-25 16:11:52 +02:00
|
|
|
from .const import CONF_LUX_SENSOR, DOMAIN, OUTPUT_SENSORS, SIGNAL_OUTPUTS_UPDATED
|
Implement add-output-sensors: 36/41 tasks complete
Three sensor entities per AL profile expose the curve's current outputs
plus actual sun elevation as graphable numerics with
SensorStateClass.MEASUREMENT, so HA's recorder + apexcharts-card chart
them natively:
- sensor.<profile>_output_brightness (% — from self._settings)
- sensor.<profile>_output_color_temp (K — from self._settings)
- sensor.<profile>_sun_elevation (° — from sun.sun.attributes.elevation)
Architecture: master switch publishes computed outputs to
hass.data[DOMAIN][entry_id]["outputs"] after each curve tick, then fires
a per-entry dispatcher signal. Sensors subscribe and read from the cache.
Single computation path, push-based updates, no polling.
The existing master switch attributes (brightness_pct, color_temp_kelvin,
synthetic sun_position in [-1,+1]) are unchanged — sensors are purely
additive. Manifest bumped to 2.2.0-cdit.1 (minor; no breaking changes).
Artifact realignment along the way: Q2 answer assumed sun_position was
Sun2 elevation degrees, but reading color_and_brightness.py revealed
it's a synthetic float in [-1,+1] derived from the brightness curve.
Switched to pulling actual elevation from sun.sun (Decision 8 added),
renamed sensor from sun_position to sun_elevation. Friendly names use
asymmetric "Output" prefix to dodge collision with the adapt-brightness
switch and Min/Max color-temp numbers.
Remaining: group 6 (manual live-HA verification on
homeassistant.onca-blenny.ts.net) — requires HACS deploy.
15 new sensor tests pass; full suite green (123/123). openspec
validate --strict green. ruff clean on new files.
2026-05-21 10:11:52 +02:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
config_entry: ConfigEntry,
|
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
|
) -> None:
|
2026-05-25 16:11:52 +02:00
|
|
|
"""Create the output sensors for this config entry."""
|
|
|
|
|
has_lux = bool(
|
|
|
|
|
config_entry.options.get(CONF_LUX_SENSOR)
|
2026-06-05 10:49:30 +02:00
|
|
|
or config_entry.data.get(CONF_LUX_SENSOR),
|
2026-05-25 16:11:52 +02:00
|
|
|
)
|
Implement add-output-sensors: 36/41 tasks complete
Three sensor entities per AL profile expose the curve's current outputs
plus actual sun elevation as graphable numerics with
SensorStateClass.MEASUREMENT, so HA's recorder + apexcharts-card chart
them natively:
- sensor.<profile>_output_brightness (% — from self._settings)
- sensor.<profile>_output_color_temp (K — from self._settings)
- sensor.<profile>_sun_elevation (° — from sun.sun.attributes.elevation)
Architecture: master switch publishes computed outputs to
hass.data[DOMAIN][entry_id]["outputs"] after each curve tick, then fires
a per-entry dispatcher signal. Sensors subscribe and read from the cache.
Single computation path, push-based updates, no polling.
The existing master switch attributes (brightness_pct, color_temp_kelvin,
synthetic sun_position in [-1,+1]) are unchanged — sensors are purely
additive. Manifest bumped to 2.2.0-cdit.1 (minor; no breaking changes).
Artifact realignment along the way: Q2 answer assumed sun_position was
Sun2 elevation degrees, but reading color_and_brightness.py revealed
it's a synthetic float in [-1,+1] derived from the brightness curve.
Switched to pulling actual elevation from sun.sun (Decision 8 added),
renamed sensor from sun_position to sun_elevation. Friendly names use
asymmetric "Output" prefix to dodge collision with the adapt-brightness
switch and Min/Max color-temp numbers.
Remaining: group 6 (manual live-HA verification on
homeassistant.onca-blenny.ts.net) — requires HACS deploy.
15 new sensor tests pass; full suite green (123/123). openspec
validate --strict green. ruff clean on new files.
2026-05-21 10:11:52 +02:00
|
|
|
entities = [
|
|
|
|
|
AdaptiveOutputSensor(
|
|
|
|
|
hass=hass,
|
|
|
|
|
entry=config_entry,
|
|
|
|
|
output_key=row["key"],
|
|
|
|
|
display_name=row["name"],
|
|
|
|
|
unit=row["unit"],
|
|
|
|
|
icon=row["icon"],
|
|
|
|
|
)
|
|
|
|
|
for row in OUTPUT_SENSORS
|
2026-05-25 16:11:52 +02:00
|
|
|
if not row.get("conditional") or has_lux
|
Implement add-output-sensors: 36/41 tasks complete
Three sensor entities per AL profile expose the curve's current outputs
plus actual sun elevation as graphable numerics with
SensorStateClass.MEASUREMENT, so HA's recorder + apexcharts-card chart
them natively:
- sensor.<profile>_output_brightness (% — from self._settings)
- sensor.<profile>_output_color_temp (K — from self._settings)
- sensor.<profile>_sun_elevation (° — from sun.sun.attributes.elevation)
Architecture: master switch publishes computed outputs to
hass.data[DOMAIN][entry_id]["outputs"] after each curve tick, then fires
a per-entry dispatcher signal. Sensors subscribe and read from the cache.
Single computation path, push-based updates, no polling.
The existing master switch attributes (brightness_pct, color_temp_kelvin,
synthetic sun_position in [-1,+1]) are unchanged — sensors are purely
additive. Manifest bumped to 2.2.0-cdit.1 (minor; no breaking changes).
Artifact realignment along the way: Q2 answer assumed sun_position was
Sun2 elevation degrees, but reading color_and_brightness.py revealed
it's a synthetic float in [-1,+1] derived from the brightness curve.
Switched to pulling actual elevation from sun.sun (Decision 8 added),
renamed sensor from sun_position to sun_elevation. Friendly names use
asymmetric "Output" prefix to dodge collision with the adapt-brightness
switch and Min/Max color-temp numbers.
Remaining: group 6 (manual live-HA verification on
homeassistant.onca-blenny.ts.net) — requires HACS deploy.
15 new sensor tests pass; full suite green (123/123). openspec
validate --strict green. ruff clean on new files.
2026-05-21 10:11:52 +02:00
|
|
|
]
|
|
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdaptiveOutputSensor(SensorEntity):
|
|
|
|
|
"""One read-only output value from an AL profile's curve tick."""
|
|
|
|
|
|
|
|
|
|
_attr_has_entity_name = True
|
|
|
|
|
_attr_should_poll = False
|
|
|
|
|
_attr_state_class = SensorStateClass.MEASUREMENT
|
|
|
|
|
_attr_device_class = None # explicit — no fitting HA device class
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
hass: HomeAssistant,
|
|
|
|
|
entry: ConfigEntry,
|
|
|
|
|
output_key: str,
|
|
|
|
|
display_name: str,
|
|
|
|
|
unit: str,
|
|
|
|
|
icon: str,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Initialise a single output sensor entity."""
|
|
|
|
|
self._hass = hass
|
|
|
|
|
self._entry = entry
|
|
|
|
|
self._output_key = output_key
|
|
|
|
|
self._attr_name = display_name
|
|
|
|
|
self._attr_translation_key = output_key
|
|
|
|
|
self._attr_unique_id = f"{entry.entry_id}_{output_key}"
|
|
|
|
|
self._attr_native_unit_of_measurement = unit
|
|
|
|
|
self._attr_icon = icon
|
|
|
|
|
# Renders as `unknown` until the first dispatcher signal arrives.
|
|
|
|
|
# Do NOT use RestoreEntity — restored values would be stale (the
|
|
|
|
|
# sun has moved); `unknown` for one tick is honest.
|
|
|
|
|
self._attr_native_value = None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def device_info(self) -> DeviceInfo:
|
|
|
|
|
"""Group with the profile's switches and number entities."""
|
|
|
|
|
profile_name = self._entry.data.get("name") or self._entry.title
|
|
|
|
|
return DeviceInfo(
|
|
|
|
|
identifiers={(DOMAIN, profile_name)},
|
|
|
|
|
name=profile_name,
|
|
|
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
|
|
|
"""Subscribe to the per-entry outputs-updated dispatcher signal."""
|
|
|
|
|
await super().async_added_to_hass()
|
|
|
|
|
signal = SIGNAL_OUTPUTS_UPDATED.format(entry_id=self._entry.entry_id)
|
|
|
|
|
self.async_on_remove(
|
|
|
|
|
async_dispatcher_connect(
|
|
|
|
|
self._hass,
|
|
|
|
|
signal,
|
|
|
|
|
self._handle_outputs_updated,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
|
def _handle_outputs_updated(self) -> None:
|
|
|
|
|
"""Read this sensor's value from the cache and write state."""
|
|
|
|
|
outputs = (
|
Delete upstream docs apparatus; rewrite README for the fork
The CDiT fork is single-household and was carrying an entire upstream
docs/ mkdocs site documenting features the fork has removed (sleep mode,
take-over-control, brightness_mode selector, YAML config). The README
mostly auto-generated from those pages via markdown-code-runner.
What's gone:
- docs/ — all 11 pages + assets + run_markdown_code_runner.py
- .github/workflows/docs.yml — zensical+shinylive GitHub Pages build
- .github/workflows/markdown-code-runner.yml — auto-edit of README from docs/
- .github/update-services.py / update-strings.py — would clobber the
hand-edited services.yaml / strings.json
- scripts/update-generated-content — orchestrator for the above
- custom_components/adaptive_lighting/_docs_helpers.py + docs_gen.py —
only used by the deleted markdown-code-runner
- zensical.toml — docs-site config
- "docs" dependency group in pyproject.toml (markdown-code-runner,
shinylive, zensical, etc.) + lock regenerated
What's new:
- README.md rewritten to describe the fork's actual surface — 3 switches,
4 number entities, 3 sensor entities per profile, 2 services (apply +
change_switch_settings), entry-only setup, UI tuning, sun source.
Preserves the existing "What's new in 2.1" / "2.2" block quotes.
- webapp/README.md annotated to flag the Shiny simulator models the
upstream curve (with brightness_mode, sleep mode, etc.) and does not
reflect the fork.
- CLAUDE.md command table no longer references update-generated-content.
Sensor.py + test_sensor_platform.py: incidental black reformat from the
lint pass (multi-arg function calls split per line).
Tests still pass (123/123). Lint clean. Sensors implementation
unchanged.
2026-05-21 10:23:04 +02:00
|
|
|
self._hass.data.get(DOMAIN, {}).get(self._entry.entry_id, {}).get("outputs")
|
Implement add-output-sensors: 36/41 tasks complete
Three sensor entities per AL profile expose the curve's current outputs
plus actual sun elevation as graphable numerics with
SensorStateClass.MEASUREMENT, so HA's recorder + apexcharts-card chart
them natively:
- sensor.<profile>_output_brightness (% — from self._settings)
- sensor.<profile>_output_color_temp (K — from self._settings)
- sensor.<profile>_sun_elevation (° — from sun.sun.attributes.elevation)
Architecture: master switch publishes computed outputs to
hass.data[DOMAIN][entry_id]["outputs"] after each curve tick, then fires
a per-entry dispatcher signal. Sensors subscribe and read from the cache.
Single computation path, push-based updates, no polling.
The existing master switch attributes (brightness_pct, color_temp_kelvin,
synthetic sun_position in [-1,+1]) are unchanged — sensors are purely
additive. Manifest bumped to 2.2.0-cdit.1 (minor; no breaking changes).
Artifact realignment along the way: Q2 answer assumed sun_position was
Sun2 elevation degrees, but reading color_and_brightness.py revealed
it's a synthetic float in [-1,+1] derived from the brightness curve.
Switched to pulling actual elevation from sun.sun (Decision 8 added),
renamed sensor from sun_position to sun_elevation. Friendly names use
asymmetric "Output" prefix to dodge collision with the adapt-brightness
switch and Min/Max color-temp numbers.
Remaining: group 6 (manual live-HA verification on
homeassistant.onca-blenny.ts.net) — requires HACS deploy.
15 new sensor tests pass; full suite green (123/123). openspec
validate --strict green. ruff clean on new files.
2026-05-21 10:11:52 +02:00
|
|
|
)
|
|
|
|
|
if not outputs:
|
|
|
|
|
# Early signal (e.g., setup race) — leave state as unknown.
|
|
|
|
|
return
|
|
|
|
|
self._attr_native_value = outputs.get(self._output_key)
|
|
|
|
|
self.async_write_ha_state()
|