diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e42b9e64..e6701aec 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [basnijholz, RubenKelevra] +github: [basnijholz, RubenKelevra] diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index fcd16fc6..cc515a20 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -3,4 +3,3 @@ name: 'Enhancement' about: 'Suggest an improvement to an existing feature.' labels: kind/enhancement, need/triage --- - diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..3067ec99 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,59 @@ +name: pytest + +on: + push: + branches: [master] + pull_request: + +jobs: + + pytest: + name: Prepare dependencies + runs-on: ubuntu-20.04 + timeout-minutes: 60 + strategy: + matrix: + python-version: ["3.9", "3.10"] + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + with: + repository: home-assistant/core + path: core + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install -r core/requirements.txt + pip install -r core/requirements_test.txt + pip install -e core/ + pip install $(python test_dependencies.py) + - name: Run pytest + timeout-minutes: 60 + run: | + cd core + + # Link homeassitant.components.adaptive_lighting + cd homeassistant/components + ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting + cd - + + # Link adaptive_lighting tests + cd tests/components/ + ln -fs ../../../tests adaptive_lighting + cd - + + python3 -X dev -m pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + --cov="homeassistant" \ + --cov-report=xml \ + -o console_output_style=count \ + -p no:sugar \ + tests/components/adaptive_lighting diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 18c7d193..2845b7dc 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -2,6 +2,7 @@ name: Validate with hassfest on: push: + branches: [master] pull_request: schedule: - cron: "0 0 * * *" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fc1b5f91..aec72c30 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,6 +2,7 @@ name: Validate on: push: + branches: [master] pull_request: schedule: - cron: "0 0 * * *" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..4394d4bb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-added-large-files + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + - repo: https://github.com/ambv/black + rev: 22.6.0 + hooks: + - id: black + - repo: https://github.com/asottile/pyupgrade + rev: v2.37.3 + hooks: + - id: pyupgrade + args: ["--py39-plus"] + - repo: https://github.com/timothycrosley/isort + rev: 5.10.1 + hooks: + - id: isort diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index f7be6292..33881c75 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -2,12 +2,11 @@ import logging from typing import Any -import voluptuous as vol - from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +import voluptuous as vol from .const import ( _DOMAIN_SCHEMA, diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 8fa74f5c..d0f0bf2d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,12 +1,11 @@ """Config flow for Adaptive Lighting integration.""" import logging -import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +import voluptuous as vol from .const import ( # pylint: disable=unused-import CONF_LIGHTS, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index b182ed82..105a95fc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,8 +1,7 @@ """Constants for the Adaptive Lighting integration.""" -import voluptuous as vol - from homeassistant.components.light import VALID_TRANSITION import homeassistant.helpers.config_validation as cv +import voluptuous as vol ICON = "mdi:theme-light-dark" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cd68e8d0..349f36e7 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -15,8 +15,6 @@ import math from typing import Any import astral -import voluptuous as vol - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -27,25 +25,27 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, - DOMAIN as LIGHT_DOMAIN, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_XY, +) +from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, VALID_TRANSITION, is_on, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - COLOR_MODE_HS, - COLOR_MODE_XY, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_BRIGHTNESS, - ATTR_SUPPORTED_COLOR_MODES, ) - -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DOMAIN, @@ -88,6 +88,7 @@ from homeassistant.util.color import ( color_xy_to_hs, ) import homeassistant.util.dt as dt_util +import voluptuous as vol from .const import ( ADAPT_BRIGHTNESS_SWITCH, @@ -97,7 +98,6 @@ from .const import ( ATTR_TURN_ON_OFF_LISTENER, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, - CONF_SLEEP_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, CONF_MANUAL_CONTROL, @@ -110,6 +110,7 @@ from .const import ( CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, CONF_SUNSET_OFFSET, @@ -210,6 +211,9 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): service_data_brightness.pop(ATTR_RGB_COLOR, None) service_data_brightness.pop(ATTR_COLOR_TEMP, None) service_datas.append(service_data_brightness) + + if not service_datas: # neither adapt_brightness nor adapt_color + return [service_data] return service_datas @@ -898,6 +902,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): + _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) return _LOGGER.debug( "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event @@ -1011,10 +1016,12 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs) -> None: """Turn on adaptive lighting sleep mode.""" + _LOGGER.debug("%s: Turning on", self._name) self._state = True async def async_turn_off(self, **kwargs) -> None: """Turn off adaptive lighting sleep mode.""" + _LOGGER.debug("%s: Turning off", self._name) self._state = False diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index dae5af4e..24c1d07e 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -46,4 +46,4 @@ "option_error": "Fehlerhafte Option" } } -} \ No newline at end of file +} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..284326f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[isort] +force_sort_within_sections=True +profile=black + +[flake8] +ignore = E203, E266, W503 +max-line-length = 100 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +per-file-ignores = + code_example.py: E402, E501 diff --git a/test_dependencies.py b/test_dependencies.py new file mode 100644 index 00000000..a9c37648 --- /dev/null +++ b/test_dependencies.py @@ -0,0 +1,25 @@ +with open("core/requirements_test_all.txt") as f: + lines = f.readlines() + +components = [] +packages = [] +deps = {} +for i, line in enumerate(lines): + line = line.strip() + if line.startswith("# homeassistant."): + component = line.split("# homeassistant.")[1] + components.append(component) + elif components and line: + packages.append(line) + else: + for component in components: + for package in packages: + deps.setdefault(component, []).append(package) + components = [] + packages = [] + +required = ["components.recorder", "components.mqtt", "components.zeroconf"] +to_install = [] +for r in required: + to_install.extend(deps[r]) +print(" ".join(to_install)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..5ae9fe68 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the Adaptive Lighting integration.""" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 00000000..53901cf9 --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,131 @@ +"""Test Adaptive Lighting config flow.""" +from homeassistant import data_entry_flow +from homeassistant.components.adaptive_lighting.const import ( + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + DEFAULT_NAME, + DOMAIN, + NONE_STR, + VALIDATION_TUPLES, +) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME + +from tests.common import MockConfigEntry + +DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} + + +async def test_flow_manual_configuration(hass): + """Test that config flow works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["handler"] == "adaptive_lighting" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NAME: "living room"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "living room" + + +async def test_import_success(hass): + """Test import step is successful.""" + data = DEFAULT_DATA.copy() + data[CONF_NAME] = DEFAULT_NAME + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + for key, value in data.items(): + assert result["data"][key] == value + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + data = DEFAULT_DATA.copy() + data[CONF_SUNRISE_TIME] = NONE_STR + data[CONF_SUNSET_TIME] = NONE_STR + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=data, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + for key, value in data.items(): + assert result["data"][key] == value + + +async def test_incorrect_options(hass): + """Test updating incorrect options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + data = DEFAULT_DATA.copy() + data[CONF_SUNRISE_TIME] = "yolo" + data[CONF_SUNSET_TIME] = "yolo" + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=data, + ) + + +async def test_import_twice(hass): + """Test importing twice.""" + data = DEFAULT_DATA.copy() + data[CONF_NAME] = DEFAULT_NAME + for _ in range(2): + _ = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=data, + ) + + +async def test_changing_options_when_using_yaml(hass): + """Test changing options when using YAML.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + source=SOURCE_IMPORT, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 00000000..b6f48e0b --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,55 @@ +"""Tests for Adaptive Lighting integration.""" +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( + DEFAULT_NAME, + UNDO_UPDATE_LISTENER, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_NAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_setup_with_config(hass): + """Test that we import the config and setup the integration.""" + config = { + adaptive_lighting.DOMAIN: { + adaptive_lighting.CONF_NAME: DEFAULT_NAME, + } + } + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, config) + assert adaptive_lighting.DOMAIN in hass.data + + +async def test_successful_config_entry(hass): + """Test that Adaptive Lighting is configured successfully.""" + + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + assert UNDO_UPDATE_LISTENER in hass.data[adaptive_lighting.DOMAIN][entry.entry_id] + + +async def test_unload_entry(hass): + """Test removing Adaptive Lighting.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED + assert adaptive_lighting.DOMAIN not in hass.data diff --git a/tests/test_switch.py b/tests/test_switch.py new file mode 100644 index 00000000..6773c2d1 --- /dev/null +++ b/tests/test_switch.py @@ -0,0 +1,871 @@ +"""Tests for Adaptive Lighting switches.""" +# pylint: disable=protected-access +import asyncio +import datetime +import logging +from random import randint +from unittest.mock import patch + +from homeassistant.components.adaptive_lighting.const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_TURN_ON_OFF_LISTENER, + CONF_DETECT_NON_HA_CHANGES, + CONF_INITIAL_TRANSITION, + CONF_MANUAL_CONTROL, + CONF_MIN_COLOR_TEMP, + CONF_PREFER_RGB_COLOR, + CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + CONF_TRANSITION, + CONF_TURN_ON_LIGHTS, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_NAME, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_COLOR_TEMP, + DOMAIN, + SERVICE_APPLY, + SERVICE_SET_MANUAL_CONTROL, + SLEEP_MODE_SWITCH, + UNDO_UPDATE_LISTENER, +) +from homeassistant.components.adaptive_lighting.switch import ( + _attributes_have_changed, + _expand_light_groups, + color_difference_redmean, + create_context, + is_our_context, +) +from homeassistant.components.demo.light import DemoLight +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import SERVICE_TURN_OFF +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +import homeassistant.config as config_util +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_LIGHTS, + CONF_NAME, + CONF_PLATFORM, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +import pytest + +from tests.common import MockConfigEntry +from tests.components.demo.test_light import ENTITY_LIGHT + +_LOGGER = logging.getLogger(__name__) + +SUNRISE = datetime.datetime( + year=2020, + month=10, + day=17, + hour=6, +) +SUNSET = datetime.datetime( + year=2020, + month=10, + day=17, + hour=22, +) + +LAT_LONG_TZS = [ + (39, -1, "Europe/Madrid"), + (60, 50, "GMT"), + (55, 13, "Europe/Copenhagen"), + (52.379189, 4.899431, "Europe/Amsterdam"), + (32.87336, -117.22743, "US/Pacific"), +] + +_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" +ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" +ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" +ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_adapt_brightness_{DEFAULT_NAME}" +ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" + +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + + +@pytest.fixture +def reset_time_zone(): + """Reset time zone.""" + yield + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + + +async def setup_switch(hass, extra_data): + """Create the switch entry.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME, **extra_data}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + return entry, switch + + +async def setup_lights(hass): + """Set up 3 light entities using the 'test' platform.""" + await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.light") + while platform.ENTITIES: + # Make sure it is empty + platform.ENTITIES.pop() + lights = [ + DemoLight( + unique_id="light_1", + name="Bed Light", + state=True, + ct=200, + ), + DemoLight( + unique_id="light_2", + name="Ceiling Lights", + state=True, + ct=380, + ), + DemoLight( + unique_id="light_3", + name="Kitchen Lights", + state=False, + hs_color=(345, 75), + ct=240, + ), + ] + for light in lights: + light.hass = hass + slug = light.name.lower().replace(" ", "_") + light.entity_id = f"light.{slug}" + await light.async_update_ha_state() + + platform.ENTITIES.extend(lights) + platform.init() + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + assert all(hass.states.get(light.entity_id) is not None for light in lights) + return lights + + +async def setup_lights_and_switch(hass, extra_conf=None): + """Create switch and demo lights.""" + # Setup demo lights and turn on + lights_instances = await setup_lights(hass) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) + + # Setup switch + lights = [ + "light.bed_light", + "light.ceiling_lights", + ] + assert all(hass.states.get(light) is not None for light in lights) + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: lights, + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_PREFER_RGB_COLOR: False, + CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp + **(extra_conf or {}), + }, + ) + await hass.async_block_till_done() + return switch, lights_instances + + +async def test_adaptive_lighting_switches(hass): + """Test switches created for adaptive_lighting integration.""" + entry, _ = await setup_switch(hass, {}) + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 + assert set(hass.states.async_entity_ids(SWITCH_DOMAIN)) == { + ENTITY_SWITCH, + ENTITY_SLEEP_MODE_SWITCH, + ENTITY_ADAPT_COLOR_SWITCH, + ENTITY_ADAPT_BRIGHTNESS_SWITCH, + } + assert ATTR_TURN_ON_OFF_LISTENER in hass.data[DOMAIN] + assert entry.entry_id in hass.data[DOMAIN] + assert len(hass.data[DOMAIN].keys()) == 2 + + data = hass.data[DOMAIN][entry.entry_id] + assert SLEEP_MODE_SWITCH in data + assert SWITCH_DOMAIN in data + assert ADAPT_COLOR_SWITCH in data + assert ADAPT_BRIGHTNESS_SWITCH in data + assert UNDO_UPDATE_LISTENER in data + assert len(data.keys()) == 5 + + +@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +async def test_adaptive_lighting_time_zones_with_default_settings( + hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name +): + """Test setting up the Adaptive Lighting switches with different timezones.""" + await config_util.async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone}, + ) + _, switch = await setup_switch(hass, {}) + # Shouldn't raise an exception ever + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + + +@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +async def test_adaptive_lighting_time_zones_and_sun_settings( + hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name +): + """Test setting up the Adaptive Lighting switches with different timezones. + + Also test the (sleep) brightness and color temperature settings. + """ + await config_util.async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone}, + ) + _, switch = await setup_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_color_temp = switch._sun_light_settings.min_color_temp + + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + async def patch_time_and_update(time): + with patch("homeassistant.util.dt.utcnow", return_value=time): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # At sunset the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + await patch_time_and_update(before_sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + + # One hour after sunset the brightness should be down + await patch_time_and_update(after_sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # At sunrise the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + await patch_time_and_update(before_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour after sunrise the brightness should be up + await patch_time_and_update(after_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + + # Turn on sleep mode which make the brightness and color_temp + # deterministic regardless of the time + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == DEFAULT_SLEEP_COLOR_TEMP + + +async def test_light_settings(hass): + """Test that light settings are correctly applied.""" + switch, _ = await setup_lights_and_switch(hass) + lights = switch._lights + + # Turn on "sleep mode" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + blocking=True, + ) + await hass.async_block_till_done() + light_states = [hass.states.get(light) for light in lights] + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == round( + 255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100 + ) + last_service_data = switch.turn_on_off_listener.last_service_data[ + state.entity_id + ] + assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS] + assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP] + + # Turn off "sleep mode" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + blocking=True, + ) + await hass.async_block_till_done() + + # Test with different times + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + context = switch.create_context("test") # needs to be passed to update method + + async def patch_time_and_get_updated_states(time): + with patch("homeassistant.util.dt.utcnow", return_value=time): + await switch._update_attrs_and_maybe_adapt_lights( + transition=0, context=context, force=True + ) + await hass.async_block_till_done() + return [hass.states.get(light) for light in lights] + + def assert_expected_color_temp(state): + last_service_data = switch.turn_on_off_listener.last_service_data[ + state.entity_id + ] + assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP] + + # At sunset the brightness should be max and color_temp at the smallest value + light_states = await patch_time_and_get_updated_states(sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + light_states = await patch_time_and_get_updated_states(before_sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour after sunset the brightness should be down + light_states = await patch_time_and_get_updated_states(after_sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] < 255 + assert_expected_color_temp(state) + + # At sunrise the brightness should be max and color_temp at the smallest value + light_states = await patch_time_and_get_updated_states(sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + light_states = await patch_time_and_get_updated_states(before_sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] < 255 + assert_expected_color_temp(state) + + # One hour after sunrise the brightness should be up + light_states = await patch_time_and_get_updated_states(after_sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + +async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): + """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" + switch, _ = await setup_lights_and_switch(hass) + light = "light.kitchen_lights" + assert light not in switch._lights + for state in [True, False]: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light}, + blocking=True, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + await hass.async_block_till_done() + assert light not in switch.turn_on_off_listener.lights + + +async def test_manual_control(hass): + """Test the 'manual control' tracking.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + context = switch.create_context("test") # needs to be passed to update method + manual_control = switch.turn_on_off_listener.manual_control + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + await update() + _LOGGER.debug("Turn light %s, to %s", state, kwargs) + + async def turn_switch(state, entity_id): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + async def change_manual_control(set_to, extra_service_data=None): + if extra_service_data is None: + extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT]} + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_MANUAL_CONTROL: set_to, + **extra_service_data, + }, + blocking=True, + ) + await hass.async_block_till_done() + await update() + + def increased_brightness(): + return (light._brightness + 100) % 255 + + def increased_color_temp(): + return max((light._ct + 100) % light.max_mireds, light.min_mireds) + + # Nothing is manually controlled + await update() + assert not manual_control[ENTITY_LIGHT] + # Call light.turn_on for ENTITY_LIGHT + await turn_light(True, brightness=increased_brightness()) + # Check that ENTITY_LIGHT is manually controlled + assert manual_control[ENTITY_LIGHT] + # Test adaptive_lighting.set_manual_control + await change_manual_control(False) + # Check that ENTITY_LIGHT is not manually controlled + assert not manual_control[ENTITY_LIGHT] + + # Check that toggling light off to on resets manual control + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT] + await turn_light(False) + await turn_light(True, brightness=increased_brightness()) + assert hass.states.get(ENTITY_LIGHT).state == STATE_ON + assert not manual_control[ENTITY_LIGHT], manual_control + + # Check that toggling (sleep mode) switch resets manual control + for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT] + await turn_switch(False, entity_id) + await turn_switch(True, entity_id) + assert not manual_control[ENTITY_LIGHT] + + # Check that when 'adapt_brightness' is off, changing the brightness + # doesn't mark it as manually controlled but changing color_temp + # does + await turn_light(False) # reset manually controlled status + await turn_light(True) + assert not manual_control[ENTITY_LIGHT] + await switch.adapt_brightness_switch.async_turn_off() + await turn_light(True, brightness=increased_brightness()) + assert not manual_control[ENTITY_LIGHT] + await turn_light(True, color_temp=(light._ct + 100) % 500) + assert manual_control[ENTITY_LIGHT] + await switch.adapt_brightness_switch.async_turn_on() # turn on again + + # Check that when 'adapt_color' is off, changing the color + # doesn't mark it as manually controlled but changing brightness + # does + await turn_light(False) # reset manually controlled status + await turn_light(True) + assert not manual_control[ENTITY_LIGHT] + await switch.adapt_color_switch.async_turn_off() + await turn_light(True, color_temp=increased_color_temp()) + assert not manual_control[ENTITY_LIGHT] + await turn_light(True, brightness=increased_brightness()) + assert manual_control[ENTITY_LIGHT] + + # Check that when 'adapt_color' adapt_brightness are both off + # nothing marks it as manually controlled + await turn_light(False) # reset manually controlled status + await turn_light(True) + await switch.adapt_color_switch.async_turn_off() + await switch.adapt_brightness_switch.async_turn_off() + assert not manual_control[ENTITY_LIGHT] + await turn_light(True, color_temp=increased_color_temp()) + await turn_light(True, brightness=increased_brightness()) + await turn_light( + True, + color_temp=increased_color_temp(), + brightness=increased_brightness(), + ) + assert not manual_control[ENTITY_LIGHT] + # Turn switches on again + await switch.adapt_color_switch.async_turn_on() + await switch.adapt_brightness_switch.async_turn_on() + + # Check that when no lights are specified, all are reset + await change_manual_control(True, {CONF_LIGHTS: switch._lights}) + assert all([manual_control[eid] for eid in switch._lights]) + # do not pass "lights" so reset all + await change_manual_control(False, {}) + assert all([not manual_control[eid] for eid in switch._lights]) + + +async def test_apply_service(hass): + """Test adaptive_lighting.apply service.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + entity_id = light.entity_id + assert entity_id not in switch._lights + + def increased_brightness(): + return (light._brightness + 100) % 255 + + def increased_color_temp(): + return max((light._ct + 100) % light.max_mireds, light.min_mireds) + + async def change_light(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: increased_brightness(), + ATTR_COLOR_TEMP: increased_color_temp(), + }, + blocking=True, + ) + await hass.async_block_till_done() + + async def apply(**kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: ENTITY_SWITCH, + CONF_LIGHTS: [entity_id], + CONF_TURN_ON_LIGHTS: True, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test turn on with defaults + assert hass.states.get(entity_id).state == STATE_OFF + await apply() + assert hass.states.get(entity_id).state == STATE_ON + await change_light() + + # Test only changing color + old_state = hass.states.get(entity_id).attributes + await apply(adapt_color=True, adapt_brightness=False) + new_state = hass.states.get(entity_id).attributes + assert old_state[ATTR_BRIGHTNESS] == new_state[ATTR_BRIGHTNESS] + assert old_state[ATTR_COLOR_TEMP] != new_state[ATTR_COLOR_TEMP] + + # Test only changing brightness + await change_light() + old_state = hass.states.get(entity_id).attributes + await apply(adapt_color=False, adapt_brightness=True) + new_state = hass.states.get(entity_id).attributes + assert old_state[ATTR_BRIGHTNESS] != new_state[ATTR_BRIGHTNESS] + assert old_state[ATTR_COLOR_TEMP] == new_state[ATTR_COLOR_TEMP] + + +async def test_switch_off_on_off(hass): + """Test switch rapid off_on_off.""" + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights( + transition=0, context=switch.create_context("test") + ) + await hass.async_block_till_done() + + switch, _ = await setup_lights_and_switch(hass) + + for turn_light_state_at_end in [True, False]: + # Turn light on + await turn_light(True) + # Turn light off with transition + await turn_light(False, transition=1) + + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + # Set state to on after a second (like happens IRL) + await asyncio.sleep(1e-3) + hass.states.async_set(ENTITY_LIGHT, STATE_ON) + # Set state to off after a second (like happens IRL) + await asyncio.sleep(1e-3) + hass.states.async_set(ENTITY_LIGHT, STATE_OFF) + + # Now we test whether the sleep task is there + assert ENTITY_LIGHT in switch.turn_on_off_listener.sleep_tasks + sleep_task = switch.turn_on_off_listener.sleep_tasks[ENTITY_LIGHT] + assert not sleep_task.cancelled() + + # A 'light.turn_on' event should cancel that task + await turn_light(turn_light_state_at_end) + await update() + state = hass.states.get(ENTITY_LIGHT).state + if turn_light_state_at_end: + assert sleep_task.cancelled() + assert state == STATE_ON + else: + assert state == STATE_OFF + + +async def test_significant_change(hass): + """Test significant change.""" + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(force): + await switch._update_attrs_and_maybe_adapt_lights( + transition=0, + context=switch.create_context("test"), + force=force, + ) + await hass.async_block_till_done() + + switch, (bed_light_instance, *_) = await setup_lights_and_switch(hass) + await turn_light(True) + await update(force=True) # removes manual control + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + # Change brightness by setting state (not using 'light.turn_on') + attributes = hass.states.get(ENTITY_LIGHT).attributes + new_attributes = attributes.copy() + new_brightness = (attributes[ATTR_BRIGHTNESS] + 100) % 255 + new_attributes[ATTR_BRIGHTNESS] = new_brightness + bed_light_instance._brightness = new_brightness + assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None + for _ in range(switch.turn_on_off_listener.max_cnt_significant_changes): + await update(force=False) + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + # On next update the light should be marked as manually controlled + await update(force=False) + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + +def test_color_difference_redmean(): + """Test color_difference_redmean function.""" + for _ in range(10): + rgb_1 = (randint(0, 255), randint(0, 255), randint(0, 255)) + rgb_2 = (randint(0, 255), randint(0, 255), randint(0, 255)) + color_difference_redmean(rgb_1, rgb_2) + color_difference_redmean((0, 0, 0), (255, 255, 255)) + + +def test_is_our_context(): + """Test is_our_context function.""" + context = create_context(DOMAIN, "test", 0) + assert is_our_context(context) + assert not is_our_context(None) + assert not is_our_context(Context()) + + +def test_attributes_have_changed(): + """Test _attributes_have_changed function.""" + attributes_1 = {ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0), ATTR_COLOR_TEMP: 100} + attributes_2 = { + ATTR_BRIGHTNESS: 100, + ATTR_RGB_COLOR: (255, 0, 0), + ATTR_COLOR_TEMP: 300, + } + kwargs = dict( + light="light.test", + adapt_brightness=True, + adapt_color=True, + context=Context(), + ) + assert not _attributes_have_changed( + old_attributes=attributes_1, new_attributes=attributes_1, **kwargs + ) + for key, value in attributes_2.items(): + attrs = dict(attributes_1) + attrs[key] = value + assert _attributes_have_changed( + old_attributes=attributes_1, new_attributes=attrs, **kwargs + ) + # Switch from rgb_color to color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP: 100}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0)}, + **kwargs, + ) + + +async def test_unload_switch(hass): + """Test removing Adaptive Lighting.""" + entry, _ = await setup_switch(hass, {}) + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN not in hass.data + + +@pytest.mark.parametrize("state", [STATE_ON, STATE_OFF, None]) +async def test_restore_off_state(hass, state): + """Test that the 'off' and 'on' states are propoperly restored.""" + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State(ENTITY_SWITCH, state) if state is not None else None, + ): + await hass.async_start() + await hass.async_block_till_done() + _, switch = await setup_switch(hass, {}) + if state == STATE_ON: + assert switch.is_on + elif state == STATE_OFF: + assert not switch.is_on + elif state is None: + assert switch.is_on + + for _switch, initial_state in [ + (switch.sleep_mode_switch, False), + (switch.adapt_brightness_switch, True), + (switch.adapt_color_switch, True), + ]: + if state == STATE_ON: + assert _switch.is_on + elif state == STATE_OFF: + assert not _switch.is_on + elif state is None: + if initial_state: + assert _switch.is_on + else: + assert not _switch.is_on + + +@pytest.mark.xfail(reason="Offset is larger than half a day") +async def test_offset_too_large(hass): + """Test that update fails when the offset is too large.""" + _, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12}) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + await hass.async_block_till_done() + + +async def test_turn_on_and_off_when_already_at_that_state(hass): + """Test 'switch.turn_on/off' when switch is on/off.""" + _, switch = await setup_switch(hass, {}) + + await switch.async_turn_on() + await hass.async_block_till_done() + await switch.async_turn_on() + await hass.async_block_till_done() + + await switch.async_turn_off() + await hass.async_block_till_done() + await switch.async_turn_off() + await hass.async_block_till_done() + + +async def test_async_update_at_interval(hass): + """Test '_async_update_at_interval' method.""" + _, switch = await setup_switch(hass, {}) + await switch._async_update_at_interval() + + +@pytest.mark.parametrize("separate_turn_on_commands", (True, False)) +async def test_separate_turn_on_commands(hass, separate_turn_on_commands): + """Test 'separate_turn_on_commands' argument.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, {CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands} + ) + # We just turn sleep mode on and off which should change the + # brightness and color. We don't test whether the number are exactly + # what we expect because we do this in other tests already, we merely + # check whether the brightness and color_temp change. + context = switch.create_context("test") # needs to be passed to update method + brightness = light.brightness + color_temp = light.color_temp + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # TODO: figure out why `light.brightness` is not updating + attrs = hass.states.get(light.entity_id).attributes + sleep_brightness = attrs["brightness"] + sleep_color_temp = attrs["color_temp"] + + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp + + await switch.sleep_mode_switch.async_turn_off() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + attrs = hass.states.get(light.entity_id).attributes + brightness = attrs["brightness"] + color_temp = attrs["color_temp"] + + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp