fix: entity translations were dead, and both validators disabled themselves

Entity._name_internal returns _attr_name before it ever consults
translation_key (homeassistant/helpers/entity.py:689). sensor.py and number.py
set both, so every entity.sensor.* and entity.number.* string added to
strings.json has been unreachable — the five sliders and five sensors rendered
English regardless of the user's language, bolted onto an integration that is
otherwise translated into 36. Dropping the three _attr_name assignments makes
the keys live; display_name was then a dead constructor parameter and goes too.

Two keys existed in strings.json but not translations/en.json (ambient_lux,
lux_reduction) and ramp_half_width was in neither, so a bare deletion would
have left three entities unnamed. Added them to both files.

ambient_lux carries unit lx and is textbook SensorDeviceClass.ILLUMINANCE; it
was inheriting the class-level _attr_device_class = None along with the four
curve outputs that genuinely have no equivalent. Without it the sensor loses
standard icon, unit handling and long-term-statistics grouping. Now a per-key
lookup, so the outputs keep their honest None.

The five number entities are behaviour knobs, not state — EntityCategory.CONFIG
moves them off the primary device page.

Both validate.yml and hassfest.yaml carried `schedule: cron "0 0 * * *"`, and
GitHub auto-disables any workflow with a schedule after 60 days of repository
inactivity. Both were taken out on 2026-09-08, 60 days after the last push. A
disabled workflow also stops answering push and pull_request, so this repo now
has no HACS or hassfest validation at all. Removing the cron is the durable
fix; re-enabling in the UI just restarts the same clock.

README claimed the tanh ramp has "a fixed 30-minute half-width" 60 lines above
documenting the live 5-120 min slider that replaced it.

164 tests pass, ruff clean.
This commit is contained in:
Casey 2026-09-08 13:34:27 +02:00
commit 6725f164aa
7 changed files with 58 additions and 31 deletions

View file

@ -4,9 +4,11 @@ on:
push:
branches: [main, master]
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
# No schedule: GitHub auto-disables any workflow containing one after 60
# days of repository inactivity, and a disabled workflow stops answering
# push and pull_request too. Both of this repo's validators were taken
# out that way on 2026-09-08.
jobs:
validate_hassfest:

View file

@ -4,9 +4,11 @@ on:
push:
branches: [main, master]
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
# No schedule: GitHub auto-disables any workflow containing one after 60
# days of repository inactivity, and a disabled workflow stops answering
# push and pull_request too. Both of this repo's validators were taken
# out that way on 2026-09-08.
jobs:
validate_hacs:

View file

@ -67,8 +67,9 @@
> (`sensor.sun2_dawn`, `sensor.sun2_astro_dawn`, etc.) work without code
> changes — see "Recommended companions" below.
> 3. **Synthetic tanh curve** — brightness and color temperature follow a
> smooth tanh ramp anchored at the two sun events with a fixed 30-minute
> half-width. No `brightness_mode` selector to fiddle with.
> smooth tanh ramp anchored at the two sun events, with a half-width you can
> tune live (5-120 min, default 30). No `brightness_mode` selector to fiddle
> with.
> 4. **Fewer features, on purpose** — sleep mode, take-over-control, manual
> sun-time overrides, `only_once`, and `adapt_only_on_bare_turn_on` are
> **removed**. Manual overrides are expected to live at the

View file

@ -31,6 +31,7 @@ from homeassistant.components.number import (
NumberMode,
RestoreNumber,
)
from homeassistant.const import EntityCategory
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
@ -56,7 +57,6 @@ async def async_setup_entry(
field_key=row["field_key"],
conf_key=row["conf_key"],
default=row["default"],
display_name=row["name"],
native_min=row["native_min"],
native_max=row["native_max"],
step=row["step"],
@ -75,6 +75,8 @@ class AdaptiveRangeNumber(RestoreNumber):
_attr_has_entity_name = True
_attr_mode = NumberMode.SLIDER
_attr_should_poll = False
# Behaviour knobs, not state — keep them off the primary device page.
_attr_entity_category = EntityCategory.CONFIG
def __init__(
self,
@ -83,7 +85,6 @@ class AdaptiveRangeNumber(RestoreNumber):
field_key: str,
conf_key: str,
default: float,
display_name: str,
native_min: float,
native_max: float,
step: float,
@ -95,7 +96,7 @@ class AdaptiveRangeNumber(RestoreNumber):
self._field_key = field_key
self._conf_key = conf_key
self._default = default
self._attr_name = display_name
# No _attr_name — see the note in sensor.py.
self._attr_translation_key = field_key
self._attr_unique_id = f"{entry.entry_id}_{field_key}"
self._attr_native_min_value = native_min
@ -197,13 +198,14 @@ class AdaptiveRampWidthNumber(RestoreNumber):
_attr_has_entity_name = True
_attr_mode = NumberMode.SLIDER
_attr_should_poll = False
# Behaviour knobs, not state — keep them off the primary device page.
_attr_entity_category = EntityCategory.CONFIG
def __init__(self, *, entry: ConfigEntry) -> None:
"""Initialise the ramp half-width entity from its const declaration."""
self._entry = entry
self._field_key: str = RAMP_WIDTH_ENTITY["field_key"]
self._default: float = float(RAMP_WIDTH_ENTITY["default"])
self._attr_name = RAMP_WIDTH_ENTITY["name"]
self._attr_translation_key = self._field_key
self._attr_unique_id = f"{entry.entry_id}_{self._field_key}"
self._attr_native_min_value = RAMP_WIDTH_ENTITY["native_min"]

View file

@ -18,7 +18,11 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.components.sensor import SensorEntity, SensorStateClass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -49,7 +53,6 @@ async def async_setup_entry(
hass=hass,
entry=config_entry,
output_key=row["key"],
display_name=row["name"],
unit=row["unit"],
icon=row["icon"],
)
@ -59,13 +62,17 @@ async def async_setup_entry(
async_add_entities(entities)
# Only ambient_lux maps onto a standard HA device class. The rest are AL curve
# outputs with no equivalent, and claiming one would break their unit handling.
_SENSOR_DEVICE_CLASSES = {"ambient_lux": SensorDeviceClass.ILLUMINANCE}
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,
@ -73,7 +80,6 @@ class AdaptiveOutputSensor(SensorEntity):
hass: HomeAssistant,
entry: ConfigEntry,
output_key: str,
display_name: str,
unit: str,
icon: str,
) -> None:
@ -81,8 +87,10 @@ class AdaptiveOutputSensor(SensorEntity):
self._hass = hass
self._entry = entry
self._output_key = output_key
self._attr_name = display_name
# No _attr_name: Entity._name_internal returns it before it ever
# consults translation_key, which would leave every translation dead.
self._attr_translation_key = output_key
self._attr_device_class = _SENSOR_DEVICE_CLASSES.get(output_key)
self._attr_unique_id = f"{entry.entry_id}_{output_key}"
self._attr_native_unit_of_measurement = unit
self._attr_icon = icon

View file

@ -258,20 +258,29 @@
},
"entity": {
"number": {
"min_brightness": {
"name": "Brightness lower"
},
"max_brightness": {
"name": "Brightness upper"
},
"max_color_temp": {
"name": "Color temp upper"
},
"min_brightness": {
"name": "Brightness lower"
},
"min_color_temp": {
"name": "Color temp lower"
},
"max_color_temp": {
"name": "Color temp upper"
"ramp_half_width": {
"name": "Ramp half-width"
}
},
"sensor": {
"ambient_lux": {
"name": "Ambient lux"
},
"lux_reduction": {
"name": "Lux reduction"
},
"output_brightness": {
"name": "Output brightness"
},
@ -280,12 +289,6 @@
},
"sun_elevation": {
"name": "Sun elevation"
},
"ambient_lux": {
"name": "Ambient lux"
},
"lux_reduction": {
"name": "Lux reduction"
}
}
}

View file

@ -246,20 +246,29 @@
},
"entity": {
"number": {
"min_brightness": {
"name": "Brightness lower"
},
"max_brightness": {
"name": "Brightness upper"
},
"max_color_temp": {
"name": "Color temp upper"
},
"min_brightness": {
"name": "Brightness lower"
},
"min_color_temp": {
"name": "Color temp lower"
},
"max_color_temp": {
"name": "Color temp upper"
"ramp_half_width": {
"name": "Ramp half-width"
}
},
"sensor": {
"ambient_lux": {
"name": "Ambient lux"
},
"lux_reduction": {
"name": "Lux reduction"
},
"output_brightness": {
"name": "Output brightness"
},