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 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#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 🌍 |
 Jeff Wilson 💻 |
 Rasmus Lundsgaard 💻 |
+  Tom Matheussen 💻 |
+  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