mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-16 08:44:03 +02:00
Clean up all remaining ruff errors; lint is now fully green
Config: - .ruff.toml target-version py310 -> py312, matching pyproject's requires-python (>=3.12) and the 3.12 'type' aliases switch.py already ships. Unblocks two false invalid-syntax errors. - Ignore D102 in tests (test-class methods), consistent with the existing D100/D103 test ignores. Code (no behavior changes): - COM812 trailing commas + UP017 dt.UTC via ruff --fix, black re-wrap - RUF002: unicode minus/en-dash -> ASCII hyphen in docstrings - SIM105: contextlib.suppress for the lux-reading parse in config_flow - TRY300: move return out of try in _read_lux_sensor - PLC0415: hoist inline asyncio/logging imports to module top - E741: rename ambiguous 'l' comprehension variable to light_id - PT006/PT018: parametrize tuple + split compound assertion - D102: docstring for OptionsFlow.async_step_init - ARG001/ARG002: noqa with justification on HA-required signatures - PLR0912/PLR0915: noqa on prepare_adaptation_data and _update_attrs_and_maybe_adapt_lights (upstream-inherited complexity; splitting would hurt readability more than it helps) ./scripts/lint passes clean; 148 tests pass.
This commit is contained in:
parent
ada3538449
commit
1c56674379
11 changed files with 68 additions and 37 deletions
|
|
@ -1,6 +1,8 @@
|
|||
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
|
||||
|
||||
target-version = "py310"
|
||||
# Matches pyproject.toml's requires-python (>=3.12); switch.py already uses
|
||||
# 3.12-only `type` alias statements.
|
||||
target-version = "py312"
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
|
||||
|
|
@ -27,6 +29,7 @@ ignore = [
|
|||
"tests/*.py" = [
|
||||
"ARG001", # Unused function argument: `call`
|
||||
"D100", # Missing docstring in public module
|
||||
"D102", # Missing docstring in public method (test-class methods)
|
||||
"D103", # Missing docstring in public function
|
||||
"D205", # 1 blank line required between summary line and description
|
||||
"D400", # First line should end with a period
|
||||
|
|
|
|||
|
|
@ -123,7 +123,10 @@ def _remove_orphan_lux_sensors(
|
|||
registry.async_remove(entry.entity_id)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, # noqa: ARG001 — signature required by HA
|
||||
config_entry: ConfigEntry,
|
||||
) -> bool:
|
||||
"""Reject older config-entry versions with a friendly recreate message.
|
||||
|
||||
Spec R8 + design D4: this fork deliberately does not migrate upstream
|
||||
|
|
|
|||
|
|
@ -145,12 +145,12 @@ def _tanh_day_curve(
|
|||
"""Piecewise tanh ramp from value_min to value_max around sunrise/sunset.
|
||||
|
||||
Schema (specs/options-flow/spec.md R4):
|
||||
- t ≤ t_sunrise − half_width: value_min
|
||||
- t_sunrise − half_width < t < t_sunrise + half_width:
|
||||
- t ≤ t_sunrise - half_width: value_min
|
||||
- t_sunrise - half_width < t < t_sunrise + half_width:
|
||||
tanh ramp min → max
|
||||
- t_sunrise + half_width ≤ t ≤ t_sunset − half_width:
|
||||
- t_sunrise + half_width ≤ t ≤ t_sunset - half_width:
|
||||
value_max
|
||||
- t_sunset − half_width < t < t_sunset + half_width:
|
||||
- t_sunset - half_width < t < t_sunset + half_width:
|
||||
tanh ramp max → min
|
||||
- t ≥ t_sunset + half_width: value_min
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Diagnostics) using HA's `section()` helper and native selectors throughout.
|
|||
See design.md decisions 1, 5, 6, 9, 14 and specs/options-flow/spec.md R1-R7.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -464,6 +465,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
|||
continue
|
||||
|
||||
async def async_step_init(self, user_input: dict[str, Any] | None = None):
|
||||
"""Render the single-page options form and handle its submission."""
|
||||
conf = self.config_entry
|
||||
if conf.source == SOURCE_IMPORT:
|
||||
return self.async_abort(reason="yaml_managed")
|
||||
|
|
@ -508,10 +510,8 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
|||
if lux_sensor_id:
|
||||
lux_state = self.hass.states.get(lux_sensor_id)
|
||||
if lux_state and lux_state.state not in ("unavailable", "unknown"):
|
||||
try:
|
||||
with contextlib.suppress(TypeError, ValueError):
|
||||
lux_reading = f"{float(lux_state.state):.0f} lx"
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Utility functions for HA core."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
|
|
@ -81,8 +82,6 @@ def setup_service_call_interceptor(
|
|||
call.data,
|
||||
)
|
||||
# Call original service handler with processed data
|
||||
import asyncio
|
||||
|
||||
target = existing_service.job.target
|
||||
if asyncio.iscoroutinefunction(target):
|
||||
await target(call)
|
||||
|
|
|
|||
|
|
@ -1112,9 +1112,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
return None
|
||||
try:
|
||||
value = float(state.state)
|
||||
return value if value > 0 else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return value if value > 0 else None
|
||||
|
||||
def _call_on_remove_callbacks(self) -> None:
|
||||
"""Call callbacks registered by async_on_remove."""
|
||||
|
|
@ -1211,7 +1211,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
force=False,
|
||||
)
|
||||
|
||||
async def prepare_adaptation_data(
|
||||
# Complexity inherited from upstream's per-light adaptation pipeline;
|
||||
# splitting it would obscure the linear lux-gate -> service-data flow.
|
||||
async def prepare_adaptation_data( # noqa: PLR0912, PLR0915
|
||||
self,
|
||||
light: str,
|
||||
transition: int | None = None,
|
||||
|
|
@ -1502,7 +1504,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
SIGNAL_OUTPUTS_UPDATED.format(entry_id=self._config_entry.entry_id),
|
||||
)
|
||||
|
||||
async def _update_attrs_and_maybe_adapt_lights(
|
||||
async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912
|
||||
self,
|
||||
*,
|
||||
context: Context,
|
||||
|
|
@ -1542,7 +1544,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if self._lux_turned_off and self._target_lux > 0:
|
||||
current_lux = self._read_lux_sensor()
|
||||
if current_lux is not None and current_lux <= self._target_lux:
|
||||
recovering = [l for l in self._lux_turned_off if l in lights]
|
||||
recovering = [
|
||||
light_id for light_id in self._lux_turned_off if light_id in lights
|
||||
]
|
||||
self._lux_turned_off -= set(recovering)
|
||||
for light_id in recovering:
|
||||
_LOGGER.debug(
|
||||
|
|
@ -1653,7 +1657,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
def fire_manual_control_event(
|
||||
self,
|
||||
light: str,
|
||||
context: Context,
|
||||
context: Context, # noqa: ARG002 — signature kept for manager compatibility
|
||||
) -> None:
|
||||
"""Deprecated no-op kept for compatibility with the manager bus listener.
|
||||
|
||||
|
|
|
|||
|
|
@ -42,12 +42,12 @@ class TestBrightnessCurve:
|
|||
"""Spec R4: piecewise tanh ramp around the two sun events."""
|
||||
|
||||
def test_min_brightness_more_than_half_width_before_sunrise(self, settings):
|
||||
"""T = sunrise − 2h → still deep night, brightness at minimum."""
|
||||
"""T = sunrise - 2h → still deep night, brightness at minimum."""
|
||||
t = T_SUNRISE - timedelta(hours=2)
|
||||
assert settings.brightness_pct(t, T_SUNRISE, T_SUNSET) == 5
|
||||
|
||||
def test_min_brightness_exactly_at_clamp_boundary(self, settings):
|
||||
"""T = sunrise − half_width → exactly at the boundary, still min."""
|
||||
"""T = sunrise - half_width → exactly at the boundary, still min."""
|
||||
t = T_SUNRISE - timedelta(seconds=HALF_WIDTH)
|
||||
assert settings.brightness_pct(t, T_SUNRISE, T_SUNSET) == 5
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ def _section_inner_keys(schema_section) -> set[str]:
|
|||
def test_options_schema_has_all_seven_sections_in_order() -> None:
|
||||
"""R1: the options form returns the seven named sections in order."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
keys = [
|
||||
k.schema if hasattr(k, "schema") else k
|
||||
|
|
@ -115,7 +117,9 @@ def test_each_section_contains_only_its_specified_fields() -> None:
|
|||
the layout table.
|
||||
"""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=True, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=True,
|
||||
show_target_lux=False,
|
||||
)
|
||||
for marker in schema.schema: # type: ignore[attr-defined]
|
||||
section_id = marker.schema if hasattr(marker, "schema") else marker
|
||||
|
|
@ -137,7 +141,9 @@ def test_each_section_contains_only_its_specified_fields() -> None:
|
|||
def test_send_split_delay_hidden_when_driver_false() -> None:
|
||||
"""R2: send_split_delay is absent when separate_turn_on_commands=False."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
advanced_marker = next(
|
||||
m
|
||||
|
|
@ -151,7 +157,9 @@ def test_send_split_delay_hidden_when_driver_false() -> None:
|
|||
def test_send_split_delay_visible_when_driver_true() -> None:
|
||||
"""R2: send_split_delay appears when separate_turn_on_commands=True."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=True, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=True,
|
||||
show_target_lux=False,
|
||||
)
|
||||
advanced_marker = next(
|
||||
m
|
||||
|
|
@ -170,7 +178,9 @@ def test_send_split_delay_visible_when_driver_true() -> None:
|
|||
def test_default_sunrise_and_sunset_entities() -> None:
|
||||
"""R3: default entities point at the built-in sun.sun sensors."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
sun_marker = next(
|
||||
m
|
||||
|
|
@ -200,7 +210,9 @@ def test_sun_entity_selectors_are_strict_timestamp_sensors() -> None:
|
|||
device_class=timestamp.
|
||||
"""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
sun_marker = next(
|
||||
m
|
||||
|
|
@ -229,9 +241,11 @@ def test_sun_entity_selectors_are_strict_timestamp_sensors() -> None:
|
|||
|
||||
|
||||
def test_brightness_uses_slider_number_selector() -> None:
|
||||
"""R5: brightness fields are NumberSelectors with slider mode, 1–100 %, step 1."""
|
||||
"""R5: brightness fields are NumberSelectors with slider mode, 1-100 %, step 1."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
daytime_marker = next(
|
||||
m
|
||||
|
|
@ -252,9 +266,11 @@ def test_brightness_uses_slider_number_selector() -> None:
|
|||
|
||||
|
||||
def test_color_temp_uses_box_number_selector() -> None:
|
||||
"""R5: color-temp fields are NumberSelectors, 1000–10000 K, step 100."""
|
||||
"""R5: color-temp fields are NumberSelectors, 1000-10000 K, step 100."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
daytime_marker = next(
|
||||
m
|
||||
|
|
@ -276,7 +292,9 @@ def test_color_temp_uses_box_number_selector() -> None:
|
|||
def test_booleans_use_boolean_selector() -> None:
|
||||
"""R5: every boolean field renders as a BooleanSelector."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=True, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=True,
|
||||
show_target_lux=False,
|
||||
)
|
||||
boolean_fields = {
|
||||
CONF_PREFER_RGB_COLOR,
|
||||
|
|
@ -368,7 +386,9 @@ async def test_options_flow_renders_sectioned_schema(hass) -> None:
|
|||
def test_target_lux_hidden_when_no_sensor() -> None:
|
||||
"""target_lux should not appear when lux_sensor is empty."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
lux_marker = next(
|
||||
m
|
||||
|
|
@ -400,7 +420,9 @@ def test_target_lux_visible_when_sensor_set() -> None:
|
|||
def test_lux_sensor_selector_filters_to_illuminance() -> None:
|
||||
"""lux_sensor entity selector should filter to device_class=illuminance."""
|
||||
schema = _build_options_schema(
|
||||
{}, show_send_split_delay=False, show_target_lux=False
|
||||
{},
|
||||
show_send_split_delay=False,
|
||||
show_target_lux=False,
|
||||
)
|
||||
lux_marker = next(
|
||||
m
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ async def test_stale_version_raises_config_entry_error(hass, caplog) -> None:
|
|||
# The full captured log (which includes the exception traceback) should
|
||||
# carry our friendly "delete and recreate" message so the user sees it.
|
||||
text = caplog.text.lower()
|
||||
assert "delete" in text and "recreate" in text
|
||||
assert "delete" in text
|
||||
assert "recreate" in text
|
||||
|
||||
|
||||
async def test_unload_entry(hass) -> None:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ no-reload-on-slider write, and the curve-math read path.
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -138,7 +139,7 @@ async def test_number_entities_share_switch_device(hass) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field_key,expected",
|
||||
("field_key", "expected"),
|
||||
[
|
||||
("min_brightness", {"min": 1.0, "max": 100.0, "step": 1, "unit": "%"}),
|
||||
("max_brightness", {"min": 1.0, "max": 100.0, "step": 1, "unit": "%"}),
|
||||
|
|
@ -259,7 +260,7 @@ async def test_restore_state_survives_restart(hass) -> None:
|
|||
object.__setattr__(
|
||||
entry,
|
||||
"modified_at",
|
||||
datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc),
|
||||
datetime.datetime(2020, 1, 1, tzinfo=datetime.UTC),
|
||||
)
|
||||
|
||||
# Prime the restore cache with a saved native_value of 30.
|
||||
|
|
@ -412,8 +413,6 @@ async def test_curve_math_falls_back_on_unavailable(hass, caplog) -> None:
|
|||
al_data = hass.data[DOMAIN][entry.entry_id]
|
||||
al_switch = al_data["switch"]
|
||||
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
settings = al_switch.sun_light_settings
|
||||
assert settings.max_brightness == 88 # fell back to options
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ def time_to_float(time: dt.time | dt.datetime) -> float:
|
|||
|
||||
|
||||
def _kw(input):
|
||||
location = Location(LocationInfo(timezone=dt.timezone.utc))
|
||||
location = Location(LocationInfo(timezone=dt.UTC))
|
||||
return {
|
||||
"name": "Adaptive Lighting Simulator",
|
||||
"adapt_until_sleep": input.adapt_until_sleep(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue