diff --git a/.all-contributorsrc b/.all-contributorsrc index 76a8cf13..004381bb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1062,6 +1062,24 @@ "contributions": [ "code" ] + }, + { + "login": "Tommatheussen", + "name": "Tom Matheussen", + "avatar_url": "https://avatars.githubusercontent.com/u/13683094?v=4", + "profile": "https://github.com/Tommatheussen", + "contributions": [ + "code" + ] + }, + { + "login": "ams2990", + "name": "ams2990", + "avatar_url": "https://avatars.githubusercontent.com/u/488907?v=4", + "profile": "https://github.com/ams2990", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.devcontainer.json b/.devcontainer.json index 0a09ed54..8d5e2bd9 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "basnijholt/adaptive_lighting", - "image": "mcr.microsoft.com/devcontainers/python:1-3.13", + "image": "mcr.microsoft.com/devcontainers/python:3-3.13", "postCreateCommand": "./scripts/setup-devcontainer && . .venv/bin/activate", "forwardPorts": [ 8123 diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index e52963b0..698d8fec 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -30,10 +30,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13.5 @@ -53,7 +53,7 @@ jobs: uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: # Upload the 'site' directory, where your app has been built path: "site" diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 4d141e56..69f93e15 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4.2.2" + - uses: "actions/checkout@v6.0.0" - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 8c4483f7..4ab58169 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -14,25 +14,25 @@ runs: using: "composite" steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }} ref: ${{ github.ref }} persist-credentials: false fetch-depth: 0 - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: home-assistant/core path: core ref: ${{ inputs.core-version }} - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.1.0 with: python-version: ${{ inputs.python-version }} - name: Set up UV - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Install dependencies shell: bash run: | diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml index d5c915c8..424f50b3 100644 --- a/.github/workflows/main-to-master-sync.yml +++ b/.github/workflows/main-to-master-sync.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: main fetch-depth: 0 diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 7f579afa..57263d00 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -9,6 +9,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index fb25eff0..b8a59b40 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -26,13 +26,23 @@ jobs: python-version: "3.13" - core-version: "2025.5.3" python-version: "3.13" - - core-version: "2025.6.1" + - core-version: "2025.6.3" + python-version: "3.13" + - core-version: "2025.7.4" + python-version: "3.13" + - core-version: "2025.8.3" + python-version: "3.13" + - core-version: "2025.9.4" + python-version: "3.13" + - core-version: "2025.10.4" + python-version: "3.13" + - core-version: "2025.11.3" python-version: "3.13" - core-version: "dev" python-version: "3.13" steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index a4422130..7cdb8e24 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 79c7fe00..3fa46b4e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,7 @@ jobs: validate_hacs: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4" + - uses: "actions/checkout@v6" - name: HACS validation uses: "hacs/action@main" with: diff --git a/Dockerfile b/Dockerfile index 3bd36ddd..c4d9c695 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,12 @@ FROM ghcr.io/astral-sh/uv:debian +# Install build dependencies for Python extensions +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + # Clone home-assistant/core RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git /core diff --git a/README.md b/README.md index 7af1feb8..9f4388c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-116-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-118-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -616,6 +616,8 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Luna Jernberg
Luna Jernberg

🌍 Jeff Wilson
Jeff Wilson

💻 Rasmus Lundsgaard
Rasmus Lundsgaard

💻 + Tom Matheussen
Tom Matheussen

💻 + ams2990
ams2990

💻 diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 13c2d7d1..0c8bad80 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -70,12 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): return True -async def async_update_options(hass, config_entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: +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( config_entry, diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py index 31225a6c..49899395 100644 --- a/custom_components/adaptive_lighting/_docs_helpers.py +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -57,8 +57,6 @@ def _type_to_str(type_: Any) -> str: # noqa: PLR0911 def generate_config_markdown_table(): - import pandas as pd - rows = [] for k, default, type_ in VALIDATION_TUPLES: description = DOCS[k] @@ -84,12 +82,11 @@ def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: def _generate_service_markdown_table( - schema: dict[str, tuple[Any, Any]], + schema: vol.Schema, alternative_docs: dict[str, str] | None = None, ): - schema = _schema_to_dict(schema) rows = [] - for k, (default, type_) in schema.items(): + for k, (default, type_) in _schema_to_dict(schema).items(): if alternative_docs is not None and k in alternative_docs: description = alternative_docs[k] else: diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index aea061d1..2c339223 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -54,7 +54,7 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: common_data = {k: service_data[k] for k in common_attrs if k in service_data} attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] - service_datas = [] + service_datas: list[dict[str, Any]] = [] for attributes in attributes_split_sequence: split_data = { @@ -106,7 +106,7 @@ async def _create_service_call_data_iterator( hass: HomeAssistant, service_datas: list[ServiceData], filter_by_state: bool, -) -> AsyncGenerator[ServiceData, None]: +) -> AsyncGenerator[ServiceData]: """Enumerates and filters a list of service datas on the fly. If filtering is enabled, every service data is filtered by the current state of @@ -141,7 +141,7 @@ class AdaptationData: entity_id: str context: Context sleep_time: float - service_call_datas: AsyncGenerator[ServiceData, None] + service_call_datas: AsyncGenerator[ServiceData] force: bool max_length: int which: Literal["brightness", "color", "both"] diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 6fdb7083..2fd93e67 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -8,8 +8,8 @@ import datetime import logging import math from dataclasses import dataclass -from datetime import timedelta -from functools import cached_property, partial +from datetime import UTC, timedelta +from functools import partial from typing import TYPE_CHECKING, Any, Literal, cast from homeassistant.util.color import ( @@ -17,9 +17,10 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) +from propcache.api import cached_property if TYPE_CHECKING: - import astral + import astral.location # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET # We re-define them here to not depend on homeassistant in this file. @@ -32,7 +33,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" _ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} -UTC = datetime.timezone.utc utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) utcnow.__doc__ = "Get now in UTC time." @@ -44,7 +44,7 @@ class SunEvents: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.Location + astral_location: astral.location.Location sunrise_time: datetime.time | None min_sunrise_time: datetime.time | None max_sunrise_time: datetime.time | None @@ -198,7 +198,7 @@ class SunLightSettings: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.Location + astral_location: astral.location.Location adapt_until_sleep: bool max_brightness: int max_color_temp: int @@ -296,7 +296,7 @@ class SunLightSettings: ) return clamp(brightness, self.min_brightness, self.max_brightness) - def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float: + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: """Calculate the brightness in %.""" if is_sleep: return self.sleep_brightness @@ -331,7 +331,7 @@ class SunLightSettings: ) -> dict[str, Any]: """Calculate the brightness and color.""" sun_position = self.sun.sun_position(dt) - rgb_color: tuple[float, float, float] + rgb_color: tuple[int, int, int] # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) force_rgb_color = False brightness_pct = self.brightness_pct(dt, is_sleep) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 3922d000..00214fdb 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Adaptive Lighting integration.""" import logging +from typing import Any import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -40,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict[str, Any]): """Handle configuration by YAML file.""" await self.async_set_unique_id(user_input[CONF_NAME]) # Keep a list of switches that are configured via YAML diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index c87d481f..21ea67b3 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -4,6 +4,7 @@ import logging from collections.abc import Awaitable, Callable from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import device_registry, entity_registry from homeassistant.util.read_only_dict import ReadOnlyDict from .adaptation_utils import ServiceData @@ -11,6 +12,25 @@ from .adaptation_utils import ServiceData _LOGGER = logging.getLogger(__name__) +def area_entities(hass: HomeAssistant, area_id: str): + """Get all entities linked to an area.""" + ent_reg = entity_registry.async_get(hass) + entity_ids = [ + entry.entity_id + for entry in entity_registry.async_entries_for_area(ent_reg, area_id) + ] + dev_reg = device_registry.async_get(hass) + entity_ids.extend( + [ + entity.entity_id + for device in device_registry.async_entries_for_area(dev_reg, area_id) + for entity in entity_registry.async_entries_for_device(ent_reg, device.id) + if entity.area_id is None + ], + ) + return entity_ids + + def setup_service_call_interceptor( hass: HomeAssistant, domain: str, diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 92100afa..856abfe8 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.26.0" + "version": "1.27.0" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b01cd4b4..b423fc3c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -75,7 +75,6 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location -from homeassistant.helpers.template import area_entities from homeassistant.util import slugify from homeassistant.util.color import ( color_temperature_to_rgb, @@ -153,7 +152,7 @@ from .const import ( apply_service_schema, replace_none_str, ) -from .hass_utils import setup_service_call_interceptor +from .hass_utils import area_entities, setup_service_call_interceptor from .helpers import ( clamp, color_difference_redmean, @@ -1589,7 +1588,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self.hass = hass data = validate(config_entry) self._icon = icon - self._state: bool | None = None + self._state: bool = initial_state self._which = which self._config_name = data[CONF_NAME] self._unique_id = f"{self._config_name}_{slugify(self._which)}" @@ -2706,7 +2705,6 @@ class _AsyncSingleShotTimer: async def _run(self): """Run the timer. Don't call this directly, use start() instead.""" - self.start_time = dt_util.utcnow() await asyncio.sleep(self.delay) if self.callback: if asyncio.iscoroutinefunction(self.callback): @@ -2722,6 +2720,10 @@ class _AsyncSingleShotTimer: """Start the timer.""" if self.task is not None and not self.task.done(): self.task.cancel() + # Set start_time before creating task to avoid race condition + # where is_running() returns True but start_time is still None + # See: https://github.com/basnijholt/adaptive-lighting/issues/1272 + self.start_time = dt_util.utcnow() self.task = asyncio.create_task(self._run()) def cancel(self): diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index b9e2de1f..92a41411 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -6,6 +6,22 @@ if grep -q 'mypy-dev==1.14.0a3' core/requirements_test.txt; then # mypy-dev==1.14.0a3 seems to not be available anymore, HA 2024.12 is affected sed -i 's/mypy-dev==1.14.0a3/mypy-dev==1.14.0a7/' core/requirements_test.txt fi +if grep -q 'mypy-dev==1.16.0a1' core/requirements_test.txt; then + # mypy-dev==1.16.0a1 seems to not be available anymore, HA 2025.2 is affected + sed -i 's/mypy-dev==1.16.0a1/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi +if grep -q 'mypy-dev==1.16.0a3' core/requirements_test.txt; then + # mypy-dev==1.16.0a3 seems to not be available anymore, HA 2025.3 is affected + sed -i 's/mypy-dev==1.16.0a3/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi +if grep -q 'mypy-dev==1.16.0a7' core/requirements_test.txt; then + # mypy-dev==1.16.0a7 seems to not be available anymore, HA 2025.4 is affected + sed -i 's/mypy-dev==1.16.0a7/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi +if grep -q 'mypy-dev==1.16.0a8' core/requirements_test.txt; then + # mypy-dev==1.16.0a8 seems to not be available anymore, HA 2025.5 and 2025.6 is affected + sed -i 's/mypy-dev==1.16.0a8/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi uv pip install -r core/requirements.txt uv pip install -r core/requirements_test.txt diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..cce77d96 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,26 @@ +"""Pytest configuration for adaptive-lighting tests.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_template_deprecation_issue(): + """Mock the template deprecation issue creation. + + The template component's legacy platform syntax creates deprecation + issues that require translations. Since adaptive-lighting tests use + template lights as test fixtures (not testing the template integration + itself), we mock the issue creation to avoid translation validation errors. + """ + # Patch the create_legacy_template_issue function in the template helpers + # to be a no-op when called for the deprecated_legacy_templates issue + try: + with patch( + "homeassistant.components.template.helpers.create_legacy_template_issue", + ): + yield + except (ImportError, ModuleNotFoundError, AttributeError): + # Older HA versions don't have this function + yield diff --git a/tests/test_switch.py b/tests/test_switch.py index cc03fa24..1016b591 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -65,6 +65,7 @@ from homeassistant.components.adaptive_lighting.switch import ( CONF_INTERCEPT, AdaptiveLightingManager, AdaptiveSwitch, + SimpleSwitch, _attributes_have_changed, color_difference_redmean, create_context, @@ -82,7 +83,16 @@ from homeassistant.components.light import ( ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.template.light import LightTemplate + +try: + # HA >= 2025.8 + from homeassistant.components.template.light import ( + StateLightEntity as LightTemplate, + ) +except ImportError: + # HA < 2025.8 + from homeassistant.components.template.light import LightTemplate + from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, @@ -2266,3 +2276,76 @@ async def test_brightness_mode(hass, brightness_mode, dark, light): # After sunrise the brightness should be light_brightness await patch_time_and_update(after_sunrise) assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) + + +async def test_simple_switch_initial_state_not_none(hass): + """Test that SimpleSwitch._state is not None after __init__. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1264 + + When an entity is disabled in Home Assistant, async_added_to_hass() is never + called. Previously, SimpleSwitch._state was initialized to None and only set + to True/False in async_added_to_hass(). This caused an infinite loop in + AdaptiveSwitch._setup_listeners() which waits for all SimpleSwitch._state + to be not None. + + The fix is to initialize _state to the initial_state value in __init__. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create a SimpleSwitch without calling async_added_to_hass + # (simulating a disabled entity) + switch = SimpleSwitch( + which="Test", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Before the fix: _state would be None, causing infinite loop + # After the fix: _state should be the initial_state value + assert switch._state is not None, ( + "SimpleSwitch._state should not be None after __init__. " + "This would cause an infinite loop in _setup_listeners when the entity is disabled." + ) + assert switch._state is True # Should be the initial_state value + + +async def test_simple_switch_state_after_async_added_to_hass(hass): + """Test that SimpleSwitch._state is properly set after async_added_to_hass. + + This ensures the fix for #1264 doesn't break normal entity initialization. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create switches with different initial states + switch_true = SimpleSwitch( + which="Test True", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + switch_false = SimpleSwitch( + which="Test False", + initial_state=False, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Verify initial state is set correctly + assert switch_true._state is True + assert switch_false._state is False + + # Call async_added_to_hass (simulating normal entity setup) + # Since there's no last state, it should use the initial_state + await switch_true.async_added_to_hass() + await switch_false.async_added_to_hass() + + # State should still be correct after async_added_to_hass + assert switch_true._state is True + assert switch_false._state is False