From 10e16dc40f02262ca10f6aa1c9300403d24477fa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:05:31 -0700 Subject: [PATCH 01/27] Copy tests from https://github.com/home-assistant/core/pull/40626 --- tests/__init__.py | 1 + tests/test_config_flow.py | 131 ++++++ tests/test_init.py | 55 +++ tests/test_switch.py | 874 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1061 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_config_flow.py create mode 100644 tests/test_init.py create mode 100644 tests/test_switch.py 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..53f05c61 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,55 @@ +"""Tests for Adaptive Lighting integration.""" +from homeassistant import config_entries +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( + DEFAULT_NAME, + UNDO_UPDATE_LISTENER, +) +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 == config_entries.ENTRY_STATE_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 == config_entries.ENTRY_STATE_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..68f76fda --- /dev/null +++ b/tests/test_switch.py @@ -0,0 +1,874 @@ +"""Tests for Adaptive Lighting switches.""" +# pylint: disable=protected-access +import asyncio +import datetime +from random import randint + +import pytest + +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, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +import homeassistant.config as config_util +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 + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.demo.test_light import ENTITY_LIGHT + +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() + 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.""" + 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, + ), + ] + platform.ENTITIES.extend(lights) + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + 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 = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = hass.config.time_zone.localize(SUNRISE).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 = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = hass.config.time_zone.localize(SUNRISE).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() + + 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 not manual_control[ENTITY_LIGHT] + + # 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, + ) + + # 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, + ) + + +@pytest.mark.parametrize("wait", [True, False]) +async def test_expand_light_groups(hass, wait): + """Test expanding light groups.""" + await setup_switch(hass, {}) + lights = ["light.ceiling_lights", "light.kitchen_lights"] + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": GROUP_DOMAIN, + "entities": lights, + }, + ] + }, + ) + if wait: + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + expanded = set(_expand_light_groups(hass, ["light.light_group"])) + if wait: + assert expanded == set(lights) + else: + # Cannot expand yet because state is None + assert expanded == {"light.light_group"} + + +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() + sleep_brightness = light.brightness + sleep_color_temp = light.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() + brightness = light.brightness + color_temp = light.color_temp + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp From d14135581747f378caf931ce0f7eef40669b088b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:26:57 -0700 Subject: [PATCH 02/27] Add .github/workflows/ci.yaml --- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..1d1b037d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,57 @@ +name: CI + +# yamllint disable-line rule:truthy +on: + push: + branches: + - dev + - rc + - master + pull_request: ~ + +env: + CACHE_VERSION: 1 + PIP_CACHE_VERSION: 1 + HA_SHORT_VERSION: 2022.9 + DEFAULT_PYTHON: 3.9 + PRE_COMMIT_CACHE: ~/.cache/pre-commit + PIP_CACHE: /tmp/pip-cache + SQLALCHEMY_WARN_20: 1 + PYTHONASYNCIODEBUG: 1 + HASS_CI: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + + base: + name: Prepare dependencies + runs-on: ubuntu-20.04 + needs: info + 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: homeassistant + - 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: | + python -m pip install --upgrade pip + python -m pip install -e homeassistant/ + - name: Run pytest + timeout-minutes: 60 + run: | + pytest tests From b03e4c435570a7d8f982bce9c6683626b7259e65 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:27:55 -0700 Subject: [PATCH 03/27] Cleanup CI --- .github/workflows/ci.yaml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1d1b037d..0cdb8cef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,28 +1,8 @@ -name: CI +name: pytest -# yamllint disable-line rule:truthy on: push: - branches: - - dev - - rc - - master - pull_request: ~ - -env: - CACHE_VERSION: 1 - PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.9 - DEFAULT_PYTHON: 3.9 - PRE_COMMIT_CACHE: ~/.cache/pre-commit - PIP_CACHE: /tmp/pip-cache - SQLALCHEMY_WARN_20: 1 - PYTHONASYNCIODEBUG: 1 - HASS_CI: 1 - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + pull_request: jobs: From bf911730f0a76881d9e42c110ecfe9fbf271b1f9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:28:36 -0700 Subject: [PATCH 04/27] remove requirements for CI --- .github/workflows/ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0cdb8cef..54525a90 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,10 +6,9 @@ on: jobs: - base: + pytest: name: Prepare dependencies runs-on: ubuntu-20.04 - needs: info timeout-minutes: 60 strategy: matrix: From 377333beb22d55fa5e9f07664d5489aedb019d4c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:29:26 -0700 Subject: [PATCH 05/27] Do not duplicate tests --- .github/workflows/ci.yaml | 1 + .github/workflows/hassfest.yaml | 1 + .github/workflows/validate.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54525a90..7e3a8106 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,7 @@ name: pytest on: push: + branches: [master] pull_request: jobs: 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 * * *" From f6c9d138c5bfaaeccfed28ae39003e6c452fef3b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:30:28 -0700 Subject: [PATCH 06/27] install pytest --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e3a8106..e35521b5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,8 +29,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install -e homeassistant/ + pip install --upgrade pip + pip install --upgrade pip pytest + pip install -e homeassistant/ - name: Run pytest timeout-minutes: 60 run: | From a9428ed93645eecd39d8d1bf4b217df662386708 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:36:37 -0700 Subject: [PATCH 07/27] Fix PYTHONPATH --- .github/workflows/ci.yaml | 2 +- tests/test_config_flow.py | 2 +- tests/test_init.py | 4 ++-- tests/test_switch.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e35521b5..7d134fd5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,4 +35,4 @@ jobs: - name: Run pytest timeout-minutes: 60 run: | - pytest tests + PYTHONPATH=${PYTHONPATH}:custom_components/:homeassistant/tests pytest tests diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 53901cf9..5666cea0 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,6 @@ """Test Adaptive Lighting config flow.""" from homeassistant import data_entry_flow -from homeassistant.components.adaptive_lighting.const import ( +from adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, diff --git a/tests/test_init.py b/tests/test_init.py index 53f05c61..ed87a4ba 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,7 @@ """Tests for Adaptive Lighting integration.""" from homeassistant import config_entries -from homeassistant.components import adaptive_lighting -from homeassistant.components.adaptive_lighting.const import ( +import adaptive_lighting +from adaptive_lighting.const import ( DEFAULT_NAME, UNDO_UPDATE_LISTENER, ) diff --git a/tests/test_switch.py b/tests/test_switch.py index 68f76fda..f61170e9 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -6,7 +6,7 @@ from random import randint import pytest -from homeassistant.components.adaptive_lighting.const import ( +from adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_TURN_ON_OFF_LISTENER, @@ -31,7 +31,7 @@ from homeassistant.components.adaptive_lighting.const import ( SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, ) -from homeassistant.components.adaptive_lighting.switch import ( +from adaptive_lighting.switch import ( _attributes_have_changed, _expand_light_groups, color_difference_redmean, From 965c0e0d3d852df91e4a9923bd35075f9c1e687f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:39:01 -0700 Subject: [PATCH 08/27] Install test requirements --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d134fd5..d0789e73 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,8 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install --upgrade pip pytest + pip install --upgrade pytest + pip install -r homeassistant/requirements_test.txt pip install -e homeassistant/ - name: Run pytest timeout-minutes: 60 From e380e5ccaa09078892e37faffa11d1da62a775d0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:43:21 -0700 Subject: [PATCH 09/27] install requirements.txt --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d0789e73..05f5461b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,6 +31,7 @@ jobs: run: | pip install --upgrade pip pip install --upgrade pytest + pip install -r homeassistant/requirements.txt pip install -r homeassistant/requirements_test.txt pip install -e homeassistant/ - name: Run pytest From da1e52142d708e51f08b67b4302bfa3eb1241c21 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:46:49 -0700 Subject: [PATCH 10/27] install homeassistant/requirements_test_all.txt --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05f5461b..12074486 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ jobs: pip install --upgrade pip pip install --upgrade pytest pip install -r homeassistant/requirements.txt - pip install -r homeassistant/requirements_test.txt + pip install -r homeassistant/requirements_test_all.txt pip install -e homeassistant/ - name: Run pytest timeout-minutes: 60 From b9ff3d6f9a4df29f9089cac19d3cd338a196dc45 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:42:55 -0700 Subject: [PATCH 11/27] Try to copy over files --- .github/workflows/ci.yaml | 14 +++++++++----- test_dependencies.py | 27 +++++++++++++++++++++++++++ tests/test_config_flow.py | 2 +- tests/test_init.py | 4 ++-- tests/test_switch.py | 6 +++--- 5 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 test_dependencies.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 12074486..4e8ec83a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v3.0.2 with: repository: home-assistant/core - path: homeassistant + path: core - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.1.0 @@ -31,10 +31,14 @@ jobs: run: | pip install --upgrade pip pip install --upgrade pytest - pip install -r homeassistant/requirements.txt - pip install -r homeassistant/requirements_test_all.txt - pip install -e homeassistant/ + 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: | - PYTHONPATH=${PYTHONPATH}:custom_components/:homeassistant/tests pytest tests + cp -r custom_components/adaptive_lighting core/homeassistant/components/adaptive_lighting + cp -r tests/ core/tests/components/adaptive_lighting + cd core + pytest tests/components/adaptive_lighting diff --git a/test_dependencies.py b/test_dependencies.py new file mode 100644 index 00000000..3886b747 --- /dev/null +++ b/test_dependencies.py @@ -0,0 +1,27 @@ +from collections import defaultdict + +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/test_config_flow.py b/tests/test_config_flow.py index 5666cea0..53901cf9 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,6 @@ """Test Adaptive Lighting config flow.""" from homeassistant import data_entry_flow -from adaptive_lighting.const import ( +from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, diff --git a/tests/test_init.py b/tests/test_init.py index ed87a4ba..53f05c61 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,7 @@ """Tests for Adaptive Lighting integration.""" from homeassistant import config_entries -import adaptive_lighting -from adaptive_lighting.const import ( +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( DEFAULT_NAME, UNDO_UPDATE_LISTENER, ) diff --git a/tests/test_switch.py b/tests/test_switch.py index f61170e9..e2bb19bd 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -6,7 +6,7 @@ from random import randint import pytest -from adaptive_lighting.const import ( +from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_TURN_ON_OFF_LISTENER, @@ -31,7 +31,7 @@ from adaptive_lighting.const import ( SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, ) -from adaptive_lighting.switch import ( +from homeassistant.components.adaptive_lighting.switch import ( _attributes_have_changed, _expand_light_groups, color_difference_redmean, @@ -63,7 +63,7 @@ from homeassistant.core import Context, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch +from unittest.mock import patch from tests.common import MockConfigEntry from tests.components.demo.test_light import ENTITY_LIGHT From 850badeb1b31661726d51bc0e530fb56f21e2493 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:43:22 -0700 Subject: [PATCH 12/27] pytest exists in core/requirements_test.txt --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4e8ec83a..fa4f32a7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,6 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install --upgrade pytest pip install -r core/requirements.txt pip install -r core/requirements_test.txt pip install -e core/ From 472f516582f44039cd4f8b3b872b34b869c4e925 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:47:19 -0700 Subject: [PATCH 13/27] Copy pytest call from core --- .github/workflows/ci.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fa4f32a7..2b317092 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,4 +40,13 @@ jobs: cp -r custom_components/adaptive_lighting core/homeassistant/components/adaptive_lighting cp -r tests/ core/tests/components/adaptive_lighting cd core - pytest tests/components/adaptive_lighting + python3 -X dev -m pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + --dist=loadfile \ + --cov="homeassistant" \ + --cov-report=xml \ + -o console_output_style=count \ + -p no:sugar \ + tests/components/adaptive_lighting From 7808d1036cbbefea957c0d0f7de988dfe3784c76 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:59:50 -0700 Subject: [PATCH 14/27] add pre-commit --- .pre-commit-config.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .pre-commit-config.yaml 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 From e8446fb2325ee6f73652ce712e11069efd63eb38 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:01:10 -0700 Subject: [PATCH 15/27] Use symlinks --- .github/workflows/ci.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2b317092..33dc1d6d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,14 +37,22 @@ jobs: - name: Run pytest timeout-minutes: 60 run: | - cp -r custom_components/adaptive_lighting core/homeassistant/components/adaptive_lighting - cp -r tests/ core/tests/components/adaptive_lighting 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 \ - --dist=loadfile \ --cov="homeassistant" \ --cov-report=xml \ -o console_output_style=count \ From 0016818beb155f2a603f6f5477302c4d419cf176 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:13:33 -0700 Subject: [PATCH 16/27] Fix TZ test --- .github/workflows/ci.yaml | 1 - setup.cfg | 11 +++++++++++ tests/test_switch.py | 17 ++++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 setup.cfg diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 33dc1d6d..3067ec99 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,7 +29,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install --upgrade pip pip install -r core/requirements.txt pip install -r core/requirements_test.txt pip install -e core/ 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/tests/test_switch.py b/tests/test_switch.py index e2bb19bd..adfed407 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -3,8 +3,7 @@ import asyncio import datetime from random import randint - -import pytest +from unittest.mock import patch from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, @@ -45,9 +44,9 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, - DOMAIN as LIGHT_DOMAIN, - SERVICE_TURN_OFF, ) +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.const import ( @@ -62,8 +61,8 @@ from homeassistant.const import ( from homeassistant.core import Context, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +import pytest -from unittest.mock import patch from tests.common import MockConfigEntry from tests.components.demo.test_light import ENTITY_LIGHT @@ -247,10 +246,10 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( context = switch.create_context("test") # needs to be passed to update method min_color_temp = switch._sun_light_settings.min_color_temp - sunset = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + 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 = hass.config.time_zone.localize(SUNRISE).astimezone(dt_util.UTC) + 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) @@ -333,10 +332,10 @@ async def test_light_settings(hass): await hass.async_block_till_done() # Test with different times - sunset = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + 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 = hass.config.time_zone.localize(SUNRISE).astimezone(dt_util.UTC) + 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) From 167530d77c079483e54d7ac472cbe19889214ad0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:30:24 -0700 Subject: [PATCH 17/27] Fix test_successful_config_entry and test_unload_entry --- tests/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 53f05c61..b6f48e0b 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,10 +1,10 @@ """Tests for Adaptive Lighting integration.""" -from homeassistant import config_entries 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 @@ -33,7 +33,7 @@ async def test_successful_config_entry(hass): assert await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state == ConfigEntryState.LOADED assert UNDO_UPDATE_LISTENER in hass.data[adaptive_lighting.DOMAIN][entry.entry_id] @@ -51,5 +51,5 @@ async def test_unload_entry(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state == ConfigEntryState.NOT_LOADED assert adaptive_lighting.DOMAIN not in hass.data From 4baa564f427981871f23f9c30971f63573ef42ee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:39:53 -0700 Subject: [PATCH 18/27] Fix setting up lights --- tests/test_switch.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index adfed407..33614e6a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -140,11 +140,18 @@ async def setup_lights(hass): 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) 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 From b6309e89d367e5f6909e7e0d8e46ba6f7f97f10b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:44:24 -0700 Subject: [PATCH 19/27] Fix assert and block till done --- tests/test_switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 33614e6a..77d96341 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -49,6 +49,7 @@ 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, @@ -109,6 +110,7 @@ async def setup_switch(hass, 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 @@ -587,6 +589,7 @@ async def test_apply_service(hass): }, blocking=True, ) + await hass.async_block_till_done() # Test turn on with defaults assert hass.states.get(entity_id).state == STATE_OFF From 5c760b5f4af8a22c6b4a4e38918bc0d7f14b5615 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 15:07:07 -0700 Subject: [PATCH 20/27] call platform.init() --- tests/test_switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 77d96341..ff759537 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -149,6 +149,7 @@ async def setup_lights(hass): 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"}} ) From 01fd7f96e20743ea91645e04e388e52ef8d00fd9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 15:08:03 -0700 Subject: [PATCH 21/27] Run all pre-commit filters --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/enhancement.md | 1 - .../adaptive_lighting/__init__.py | 3 +-- .../adaptive_lighting/config_flow.py | 3 +-- custom_components/adaptive_lighting/const.py | 3 +-- custom_components/adaptive_lighting/switch.py | 27 ++++++++++--------- .../adaptive_lighting/translations/de.json | 2 +- test_dependencies.py | 2 -- tests/test_switch.py | 2 +- 9 files changed, 20 insertions(+), 25 deletions(-) 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/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..c7a39b17 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, 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/test_dependencies.py b/test_dependencies.py index 3886b747..a9c37648 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -1,5 +1,3 @@ -from collections import defaultdict - with open("core/requirements_test_all.txt") as f: lines = f.readlines() diff --git a/tests/test_switch.py b/tests/test_switch.py index ff759537..de19045f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -755,7 +755,7 @@ def test_attributes_have_changed(): @pytest.mark.parametrize("wait", [True, False]) async def test_expand_light_groups(hass, wait): """Test expanding light groups.""" - await setup_switch(hass, {}) + await setup_lights_and_switch(hass, {}) lights = ["light.ceiling_lights", "light.kitchen_lights"] await async_setup_component( hass, From 26bce85318b5d52e1d05e49a1f99f549dff958d9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 16:11:23 -0700 Subject: [PATCH 22/27] Setup demo platform --- tests/test_switch.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index de19045f..9689a05a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio import datetime +import logging from random import randint from unittest.mock import patch @@ -67,6 +68,8 @@ 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, @@ -97,6 +100,13 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE +@pytest.fixture(autouse=True) +async def setup_comp(hass): + """Set up demo component.""" + await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() + + @pytest.fixture def reset_time_zone(): """Reset time zone.""" @@ -117,6 +127,9 @@ async def setup_switch(hass, extra_data): async def setup_lights(hass): """Set up 3 light entities using the 'test' platform.""" + await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() + platform = getattr(hass.components, "test.light") while platform.ENTITIES: # Make sure it is empty @@ -442,6 +455,7 @@ async def test_manual_control(hass): ) 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( @@ -491,7 +505,8 @@ async def test_manual_control(hass): assert manual_control[ENTITY_LIGHT] await turn_light(False) await turn_light(True, brightness=increased_brightness()) - assert not manual_control[ENTITY_LIGHT] + 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]: From bc825f035c09fdeeb2253369f4be86aa4dd8efea Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 16:12:12 -0700 Subject: [PATCH 23/27] Remove fixture that is not neede --- tests/test_switch.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 9689a05a..9aeb131a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -100,13 +100,6 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE -@pytest.fixture(autouse=True) -async def setup_comp(hass): - """Set up demo component.""" - await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) - await hass.async_block_till_done() - - @pytest.fixture def reset_time_zone(): """Reset time zone.""" From 3b03482593f2d185d2721dc05afe9e3849a150fe Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 16:17:41 -0700 Subject: [PATCH 24/27] Never return an empty list, fixes #81 --- custom_components/adaptive_lighting/switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c7a39b17..b013d17e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -211,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 From 60179cbe38c3179766877d954cf7538fb8fd315a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:02:52 -0700 Subject: [PATCH 25/27] Fix test_separate_turn_on_commands --- custom_components/adaptive_lighting/switch.py | 3 +++ tests/test_switch.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b013d17e..349f36e7 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -902,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 @@ -1015,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/tests/test_switch.py b/tests/test_switch.py index 9aeb131a..5c785bce 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -879,14 +879,22 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): await switch.sleep_mode_switch.async_turn_on() await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() - sleep_brightness = light.brightness - sleep_color_temp = light.color_temp + + # 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() - brightness = light.brightness - color_temp = light.color_temp + + 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 From f1980715841a6641563b3a1ebcc91e52078cc0c3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:24:36 -0700 Subject: [PATCH 26/27] Use variable --- tests/test_switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 5c785bce..e290c335 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -120,7 +120,9 @@ async def setup_switch(hass, extra_data): async def setup_lights(hass): """Set up 3 light entities using the 'test' platform.""" - await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) await hass.async_block_till_done() platform = getattr(hass.components, "test.light") From 26617659906fcd14699abe3e874a655bfd292e1a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:25:13 -0700 Subject: [PATCH 27/27] Remove test_expand_light_groups --- tests/test_switch.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index e290c335..6773c2d1 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -762,37 +762,6 @@ def test_attributes_have_changed(): ) -@pytest.mark.parametrize("wait", [True, False]) -async def test_expand_light_groups(hass, wait): - """Test expanding light groups.""" - await setup_lights_and_switch(hass, {}) - lights = ["light.ceiling_lights", "light.kitchen_lights"] - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: [ - {"platform": "demo"}, - { - "platform": GROUP_DOMAIN, - "entities": lights, - }, - ] - }, - ) - if wait: - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - expanded = set(_expand_light_groups(hass, ["light.light_group"])) - if wait: - assert expanded == set(lights) - else: - # Cannot expand yet because state is None - assert expanded == {"light.light_group"} - - async def test_unload_switch(hass): """Test removing Adaptive Lighting.""" entry, _ = await setup_switch(hass, {})