From d6cbe1cf4a39ab7960c7a4e7b9fbd52ae1457937 Mon Sep 17 00:00:00 2001 From: amitfin Date: Mon, 5 Oct 2020 12:00:59 +0300 Subject: [PATCH 01/27] Add files via upload --- .../components/input_schedule/__init__.py | 341 ++++++++++++++++++ .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 10561 bytes .../components/input_schedule/manifest.json | 7 + .../components/input_schedule/services.yaml | 32 ++ .../components/input_schedule/strings.json | 1 + .../input_schedule/translations/af.json | 3 + .../input_schedule/translations/ar.json | 3 + .../input_schedule/translations/bg.json | 3 + .../input_schedule/translations/bs.json | 3 + .../input_schedule/translations/ca.json | 3 + .../input_schedule/translations/cs.json | 3 + .../input_schedule/translations/cy.json | 3 + .../input_schedule/translations/da.json | 3 + .../input_schedule/translations/de.json | 3 + .../input_schedule/translations/el.json | 3 + .../input_schedule/translations/en.json | 3 + .../input_schedule/translations/es-419.json | 3 + .../input_schedule/translations/es.json | 3 + .../input_schedule/translations/et.json | 3 + .../input_schedule/translations/eu.json | 3 + .../input_schedule/translations/fi.json | 3 + .../input_schedule/translations/fr.json | 3 + .../input_schedule/translations/he.json | 3 + .../input_schedule/translations/hi.json | 3 + .../input_schedule/translations/hr.json | 3 + .../input_schedule/translations/hu.json | 3 + .../input_schedule/translations/hy.json | 3 + .../input_schedule/translations/id.json | 3 + .../input_schedule/translations/is.json | 3 + .../input_schedule/translations/it.json | 3 + .../input_schedule/translations/ko.json | 3 + .../input_schedule/translations/lb.json | 3 + .../input_schedule/translations/lv.json | 3 + .../input_schedule/translations/nb.json | 3 + .../input_schedule/translations/nl.json | 3 + .../input_schedule/translations/nn.json | 3 + .../input_schedule/translations/no.json | 3 + .../input_schedule/translations/pl.json | 3 + .../input_schedule/translations/pt-BR.json | 3 + .../input_schedule/translations/pt.json | 3 + .../input_schedule/translations/ro.json | 3 + .../input_schedule/translations/ru.json | 3 + .../input_schedule/translations/sk.json | 3 + .../input_schedule/translations/sl.json | 3 + .../input_schedule/translations/sv.json | 3 + .../input_schedule/translations/te.json | 3 + .../input_schedule/translations/th.json | 3 + .../input_schedule/translations/tr.json | 3 + .../input_schedule/translations/uk.json | 3 + .../input_schedule/translations/vi.json | 3 + .../input_schedule/translations/zh-Hans.json | 3 + .../input_schedule/translations/zh-Hant.json | 3 + 52 files changed, 522 insertions(+) create mode 100644 homeassistant/components/input_schedule/__init__.py create mode 100644 homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc create mode 100644 homeassistant/components/input_schedule/manifest.json create mode 100644 homeassistant/components/input_schedule/services.yaml create mode 100644 homeassistant/components/input_schedule/strings.json create mode 100644 homeassistant/components/input_schedule/translations/af.json create mode 100644 homeassistant/components/input_schedule/translations/ar.json create mode 100644 homeassistant/components/input_schedule/translations/bg.json create mode 100644 homeassistant/components/input_schedule/translations/bs.json create mode 100644 homeassistant/components/input_schedule/translations/ca.json create mode 100644 homeassistant/components/input_schedule/translations/cs.json create mode 100644 homeassistant/components/input_schedule/translations/cy.json create mode 100644 homeassistant/components/input_schedule/translations/da.json create mode 100644 homeassistant/components/input_schedule/translations/de.json create mode 100644 homeassistant/components/input_schedule/translations/el.json create mode 100644 homeassistant/components/input_schedule/translations/en.json create mode 100644 homeassistant/components/input_schedule/translations/es-419.json create mode 100644 homeassistant/components/input_schedule/translations/es.json create mode 100644 homeassistant/components/input_schedule/translations/et.json create mode 100644 homeassistant/components/input_schedule/translations/eu.json create mode 100644 homeassistant/components/input_schedule/translations/fi.json create mode 100644 homeassistant/components/input_schedule/translations/fr.json create mode 100644 homeassistant/components/input_schedule/translations/he.json create mode 100644 homeassistant/components/input_schedule/translations/hi.json create mode 100644 homeassistant/components/input_schedule/translations/hr.json create mode 100644 homeassistant/components/input_schedule/translations/hu.json create mode 100644 homeassistant/components/input_schedule/translations/hy.json create mode 100644 homeassistant/components/input_schedule/translations/id.json create mode 100644 homeassistant/components/input_schedule/translations/is.json create mode 100644 homeassistant/components/input_schedule/translations/it.json create mode 100644 homeassistant/components/input_schedule/translations/ko.json create mode 100644 homeassistant/components/input_schedule/translations/lb.json create mode 100644 homeassistant/components/input_schedule/translations/lv.json create mode 100644 homeassistant/components/input_schedule/translations/nb.json create mode 100644 homeassistant/components/input_schedule/translations/nl.json create mode 100644 homeassistant/components/input_schedule/translations/nn.json create mode 100644 homeassistant/components/input_schedule/translations/no.json create mode 100644 homeassistant/components/input_schedule/translations/pl.json create mode 100644 homeassistant/components/input_schedule/translations/pt-BR.json create mode 100644 homeassistant/components/input_schedule/translations/pt.json create mode 100644 homeassistant/components/input_schedule/translations/ro.json create mode 100644 homeassistant/components/input_schedule/translations/ru.json create mode 100644 homeassistant/components/input_schedule/translations/sk.json create mode 100644 homeassistant/components/input_schedule/translations/sl.json create mode 100644 homeassistant/components/input_schedule/translations/sv.json create mode 100644 homeassistant/components/input_schedule/translations/te.json create mode 100644 homeassistant/components/input_schedule/translations/th.json create mode 100644 homeassistant/components/input_schedule/translations/tr.json create mode 100644 homeassistant/components/input_schedule/translations/uk.json create mode 100644 homeassistant/components/input_schedule/translations/vi.json create mode 100644 homeassistant/components/input_schedule/translations/zh-Hans.json create mode 100644 homeassistant/components/input_schedule/translations/zh-Hant.json diff --git a/homeassistant/components/input_schedule/__init__.py b/homeassistant/components/input_schedule/__init__.py new file mode 100644 index 00000000000000..ad2401800c74bb --- /dev/null +++ b/homeassistant/components/input_schedule/__init__.py @@ -0,0 +1,341 @@ +"""Support to set a daily schedule (on and off times during the day).""" +import datetime +import logging +import typing + +import voluptuous as vol + +import homeassistant +from homeassistant.const import ( + ATTR_EDITABLE, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import callback +from homeassistant.helpers import collection, event +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "input_schedule" + +ATTR_START = "start" +ATTR_END = "end" +ATTR_STATUS = "status" + +SERVICE_SET_ON = "set_on" +SERVICE_SET_OFF = "set_off" +SERVICE_RESET = "reset" + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: cv.schema_with_slug_keys( + vol.Any( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, + None, + ) + ) + }, + extra=vol.ALLOW_EXTRA, +) + +SERVICE_SET_SCHEMA = vol.Schema( + { + vol.Required(ATTR_START): cv.time, + vol.Required(ATTR_END): cv.time, + }, + extra=vol.ALLOW_EXTRA, +) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_ICON): cv.icon, +} + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, +} + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up an input schedule.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() + + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, InputSchedule.from_yaml + ) + + storage_collection = ScheduleStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, InputSchedule + ) + + await yaml_collection.async_load( + [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) + if conf is None: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **conf} for id_, conf in conf.get(DOMAIN, {}).items()] + ) + + component.async_register_entity_service( + SERVICE_SET_ON, SERVICE_SET_SCHEMA, "async_set_on" + ) + component.async_register_entity_service( + SERVICE_SET_OFF, SERVICE_SET_SCHEMA, "async_set_off" + ) + component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + return True + + +class ScheduleStorageCollection(collection.StorageCollection): + """Input storage based collection.""" + + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) + + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + self.UPDATE_SCHEMA(update_data) + return {**data, **update_data} + + +class InputSchedule(RestoreEntity): + """Representation of a slider.""" + + def __init__(self, config: typing.Dict): + """Initialize an input schedule.""" + self._config = config + self._on_periods = [] + self.editable = True + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputSchedule": + """Return entity instance initialized from yaml storage.""" + input_schedule = cls(config) + input_schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + input_schedule.editable = False + return input_schedule + + @property + def name(self): + """Return the name of the input slider.""" + return self._config.get(CONF_NAME) + + @property + def icon(self): + """Return the icon to be used for this entity.""" + return self._config.get(CONF_ICON) + + @property + def state(self): + """Return the list of on periods as a single string.""" + return self._to_string() + + @property + def state_attributes(self): + """Return the state attributes.""" + return { + ATTR_EDITABLE: self.editable, + ATTR_STATUS: self._status(), + } + + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id of the entity.""" + return self._config[CONF_ID] + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state and state.state: + self._from_string(state.state) + await self._schedule_update() + + def _to_string(self) -> str: + return ", ".join( + f"{period.start.isoformat()} - {period.end.isoformat()}" + for period in self._on_periods + ) + + def _from_string(self, value) -> None: + self._on_periods = [] + value = value.replace(" ", "") + for period in value.split(","): + start, end = period.split("-") + self._on_periods.append(Period(_read_time(start), _read_time(end))) + + def _status(self): + """Return true if the current time is in an on period.""" + now = datetime.datetime.now().time() + for period in self._on_periods: + if _is_in_period(now, period): + return STATE_ON + return STATE_OFF + + async def async_set_on(self, start, end): + """Add on period.""" + start = start.replace(microsecond=0, tzinfo=None) + end = end.replace(microsecond=0, tzinfo=None) + if end <= start: + raise vol.Invalid("Start time must be earlier than end time") + + # Merge overlapping and adjusting periods. + for period in self._on_periods: + if _is_in_period(start, period) or start == period.end: + start = period.start + if _is_in_period(end, period) or end == period.start: + end = period.end + + on_periods = [Period(start, end)] + + # Copy non overlapping periods. + for period in self._on_periods: + if period.end <= start or period.start >= end: + on_periods.append(period) + + on_periods.sort(key=lambda period: period.start) + + self._on_periods = on_periods + self.async_write_ha_state() + await self._schedule_update() + + async def async_set_off(self, start, end): + """Add off period (subtracting the on periods).""" + start = start.replace(microsecond=0, tzinfo=None) + end = end.replace(microsecond=0, tzinfo=None) + if end <= start: + raise vol.Invalid("Start time must be earlier than end time") + + on_periods = [] + + # Trim and split periods. + for period in self._on_periods: + if _is_in_period(start, period): + self._on_periods.remove(period) + on_periods.append(Period(period.start, start)) + if _is_in_period(end, period): + on_periods.append(Period(end, period.end)) + break + for period in self._on_periods: + if _is_in_period(end, period): + self._on_periods.remove(period) + on_periods.append(Period(period.start, end)) + break + + # Copy non overlapping periods. + for period in self._on_periods: + if period.end <= start or period.start >= end: + on_periods.append(period) + + on_periods.sort(key=lambda period: period.start) + + self._on_periods = on_periods + self.async_write_ha_state() + await self._schedule_update() + + async def async_reset(self): + """Delete all on periods.""" + self._on_periods = [] + self.async_write_ha_state() + await self._schedule_update() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() + + async def _schedule_update(self): + now = datetime.datetime.now() + time = now.time() + midnight = datetime.time.fromisoformat("00:00:00") + prev_end = midnight + for period in self._on_periods: + if _is_in_period(time, period): + next_change = datetime.datetime.combine(now.date(), period.end) + break + if prev_end <= time < period.start: + next_change = datetime.datetime.combine(now.date(), period.start) + break + prev_end = period.end + else: + next_change = datetime.datetime.combine( + datetime.date.today() + datetime.timedelta(days=1), midnight + ) + event.async_track_point_in_time( + self.hass, self.async_write_ha_state, next_change + ) + + +class Period: + """Simple time range.""" + + def __init__(self, start, end): + """Initialize the range.""" + self.start = start + self.end = end + + +def _read_time(value): + return datetime.time.fromisoformat(value).replace(microsecond=0, tzinfo=None) + + +def _is_in_period(value, period): + return period.start <= value < period.end diff --git a/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfc234a458fbee5777bf053de564ccfeff801b6e GIT binary patch literal 10561 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 Date: Mon, 5 Oct 2020 12:16:14 +0300 Subject: [PATCH 02/27] Add files via upload --- tests/components/input_schedule/__init__.py | 1 + .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 202 bytes .../test_init.cpython-38-pytest-6.0.2.pyc | Bin 0 -> 21459 bytes tests/components/input_schedule/test_init.py | 459 ++++++++++++++++++ 4 files changed, 460 insertions(+) create mode 100644 tests/components/input_schedule/__init__.py create mode 100644 tests/components/input_schedule/__pycache__/__init__.cpython-38.pyc create mode 100644 tests/components/input_schedule/__pycache__/test_init.cpython-38-pytest-6.0.2.pyc create mode 100644 tests/components/input_schedule/test_init.py diff --git a/tests/components/input_schedule/__init__.py b/tests/components/input_schedule/__init__.py new file mode 100644 index 00000000000000..9e143367b3177c --- /dev/null +++ b/tests/components/input_schedule/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_schedule component.""" diff --git a/tests/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/tests/components/input_schedule/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5a19ad07f421f19bbefc2c9f14fcf156d0b1893 GIT binary patch literal 202 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 CCpV-3 literal 0 HcmV?d00001 diff --git a/tests/components/input_schedule/test_init.py b/tests/components/input_schedule/test_init.py new file mode 100644 index 00000000000000..db40f5a5de9b41 --- /dev/null +++ b/tests/components/input_schedule/test_init.py @@ -0,0 +1,459 @@ +"""The tests for the Input schedule component.""" +import datetime + +import pytest + +from homeassistant.components.input_schedule import ( + ATTR_END, + ATTR_START, + ATTR_STATUS, + DOMAIN, + SERVICE_RELOAD, + SERVICE_RESET, + SERVICE_SET_OFF, + SERVICE_SET_ON, +) +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +# pylint: disable=protected-access +from tests.async_mock import patch +from tests.common import mock_restore_cache + + +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + "id": "from_storage", + "name": "from storage", + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + +async def set_on(hass, entity_id, start, end): + """Add an on period.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_START: start, ATTR_END: end}, + blocking=True, + ) + + +async def set_off(hass, entity_id, start, end): + """Add an off period.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_OFF, + {ATTR_ENTITY_ID: entity_id, ATTR_START: start, ATTR_END: end}, + blocking=True, + ) + + +async def reset(hass, entity_id): + """Remove all on periods.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RESET, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": {ATTR_NAME: "from yaml"}}}) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_EDITABLE] + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state.attributes[ATTR_FRIENDLY_NAME] == "from yaml" + assert not state.attributes[ATTR_EDITABLE] + + +async def test_set_on(hass, caplog): + """Test set_on method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_schedule.test" + + test_cases = [ + ("simple", [("01:02:03", "04:05:06")], "01:02:03 - 04:05:06"), + ( + "multiple", + (("10:00:00", "11:00:00"), ("02:00:00", "03:00:00")), + "02:00:00 - 03:00:00, 10:00:00 - 11:00:00", + ), + ( + "overlapping", + (("01:00:00", "03:00:00"), ("02:00:00", "04:00:00")), + "01:00:00 - 04:00:00", + ), + ( + "adjusted", + (("01:00:00", "02:00:00"), ("02:00:00", "03:00:00")), + "01:00:00 - 03:00:00", + ), + ( + "subset", + (("01:00:00", "04:00:00"), ("02:00:00", "03:00:00")), + "01:00:00 - 04:00:00", + ), + ( + "superset", + (("02:00:00", "03:00:00"), ("01:00:00", "04:00:00")), + "01:00:00 - 04:00:00", + ), + ( + "merge", + ( + ("01:00:00", "02:00:00"), + ("03:00:00", "04:00:00"), + ("02:00:00", "03:00:00"), + ), + "01:00:00 - 04:00:00", + ), + ] + + for test_case in test_cases: + await reset(hass, entity_id) + state = hass.states.get(entity_id) + assert state.state == "" + + for period in test_case[1]: + start = datetime.time.fromisoformat(period[0]) + end = datetime.time.fromisoformat(period[1]) + await set_on(hass, entity_id, start, end) + + state = hass.states.get(entity_id) + assert ( + state.state == test_case[2] + ), f"{test_case[0]} failed: state is '{state.state}' but expecting '{test_case[2]}''" + + +async def test_set_off(hass, caplog): + """Test set_off method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_schedule.test" + + test_cases = [ + ( + "simple", + [("01:02:03", "04:05:06")], + ("02:03:04", "04:05:06"), + "01:02:03 - 02:03:04", + ), + ( + "superset", + [("01:00:00", "05:00:00")], + ("00:00:00", "10:00:00"), + "", + ), + ( + "subset", + [("01:00:00", "05:00:00")], + ("02:00:00", "04:00:00"), + "01:00:00 - 02:00:00, 04:00:00 - 05:00:00", + ), + ( + "adjusted", + [("01:00:00", "02:00:00")], + ("02:00:00", "03:00:00"), + "01:00:00 - 02:00:00", + ), + ] + + for test_case in test_cases: + await reset(hass, entity_id) + state = hass.states.get(entity_id) + assert state.state == "" + + for on_period in test_case[1]: + start = datetime.time.fromisoformat(on_period[0]) + end = datetime.time.fromisoformat(on_period[1]) + await set_on(hass, entity_id, start, end) + + start = datetime.time.fromisoformat(test_case[2][0]) + end = datetime.time.fromisoformat(test_case[2][1]) + await set_off(hass, entity_id, start, end) + + state = hass.states.get(entity_id) + assert ( + state.state == test_case[3] + ), f"{test_case[0]} failed: state is '{state.state}' but expecting '{test_case[3]}''" + + +async def test_status(hass, caplog): + """Test is_on attribute.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_schedule.test" + + assert hass.states.get(entity_id).attributes[ATTR_STATUS] == STATE_OFF + + now = datetime.datetime.now() + in_2_minutes = now + datetime.timedelta(minutes=2) + + start = now.time() + end = in_2_minutes.time() + + if end < start: + # The rare case of day overlap - skip the test + return + + await set_on(hass, entity_id, start, end) + assert hass.states.get(entity_id).attributes[ATTR_STATUS] == STATE_ON + + +async def test_restore_state(hass): + """Ensure states are restored on startup.""" + mock_restore_cache( + hass, + ( + State("input_schedule.a", "01:02:03 - 04:05:06"), + State("input_schedule.b", "07:08:09 - 10:11:12"), + ), + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"a": {}, "b": {}}}, + ) + + state = hass.states.get("input_schedule.a") + assert state + assert state.state == "01:02:03 - 04:05:06" + + state = hass.states.get("input_schedule.b") + assert state + assert state.state == "07:08:09 - 10:11:12" + + +async def test_input_scheudle_context(hass, hass_admin_user): + """Test that input_schedule context works.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"x": {}}}) + + state = hass.states.get(f"{DOMAIN}.x") + assert state is not None + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_ON, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_START: datetime.time.fromisoformat("01:02:03"), + ATTR_END: datetime.time.fromisoformat("04:05:06"), + }, + True, + Context(user_id=hass_admin_user.id), + ) + + state2 = hass.states.get(f"{DOMAIN}.x") + assert state2 is not None + assert state.state != state2.state + assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": {ATTR_NAME: "before"}, + "test_3": {}, + } + }, + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get(f"{DOMAIN}.test_1") + state_2 = hass.states.get(f"{DOMAIN}.test_2") + state_3 = hass.states.get(f"{DOMAIN}.test_3") + + assert state_1 is not None + assert state_1.attributes[ATTR_FRIENDLY_NAME] == "before" + assert state_2 is None + assert state_3 is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_1": {ATTR_NAME: "after"}, + "test_2": {}, + } + }, + ): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get(f"{DOMAIN}.test_1") + assert state_1.attributes[ATTR_FRIENDLY_NAME] == "after" + state_2 = hass.states.get(f"{DOMAIN}.test_2") + state_3 = hass.states.get(f"{DOMAIN}.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": {}}}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == "" From 2049917790067a9c6a243df62f5e370728f1bee5 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Tue, 6 Oct 2020 18:37:14 +0000 Subject: [PATCH 03/27] Changed state to be on/off --- .../components/input_schedule/__init__.py | 86 +-- .../__pycache__/__init__.cpython-38.pyc | Bin 10561 -> 10894 bytes tests/components/input_schedule/__init__.py | 1 + tests/components/input_schedule/test_init.py | 575 ++++++++++++++++++ 4 files changed, 625 insertions(+), 37 deletions(-) create mode 100644 tests/components/input_schedule/__init__.py create mode 100644 tests/components/input_schedule/test_init.py diff --git a/homeassistant/components/input_schedule/__init__.py b/homeassistant/components/input_schedule/__init__.py index ad2401800c74bb..3d10ebbdd01a64 100644 --- a/homeassistant/components/input_schedule/__init__.py +++ b/homeassistant/components/input_schedule/__init__.py @@ -29,7 +29,7 @@ ATTR_START = "start" ATTR_END = "end" -ATTR_STATUS = "status" +ATTR_ON_PERIODS = "on_periods" SERVICE_SET_ON = "set_on" SERVICE_SET_OFF = "set_off" @@ -157,13 +157,14 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic class InputSchedule(RestoreEntity): - """Representation of a slider.""" + """Representation of a schedule.""" def __init__(self, config: typing.Dict): """Initialize an input schedule.""" self._config = config self._on_periods = [] self.editable = True + self._event_unsub = None @classmethod def from_yaml(cls, config: typing.Dict) -> "InputSchedule": @@ -173,9 +174,14 @@ def from_yaml(cls, config: typing.Dict) -> "InputSchedule": input_schedule.editable = False return input_schedule + @property + def should_poll(self): + """No polling needed.""" + return False + @property def name(self): - """Return the name of the input slider.""" + """Return the name of the input schedule.""" return self._config.get(CONF_NAME) @property @@ -185,15 +191,19 @@ def icon(self): @property def state(self): - """Return the list of on periods as a single string.""" - return self._to_string() + """Return 'on' when we are in an on period.""" + now = datetime.datetime.now().time() + for period in self._on_periods: + if _is_in_period(now, period): + return STATE_ON + return STATE_OFF @property def state_attributes(self): """Return the state attributes.""" return { ATTR_EDITABLE: self.editable, - ATTR_STATUS: self._status(), + ATTR_ON_PERIODS: self._on_periods_to_attribute(), } @property @@ -205,30 +215,21 @@ async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() state = await self.async_get_last_state() - if state and state.state: - self._from_string(state.state) - await self._schedule_update() + if state and state.attributes.get(ATTR_ON_PERIODS): + self._on_periods_from_attribute(state.attributes[ATTR_ON_PERIODS]) + self._update_state() - def _to_string(self) -> str: - return ", ".join( - f"{period.start.isoformat()} - {period.end.isoformat()}" + def _on_periods_to_attribute(self): + return [ + {ATTR_START: period.start.isoformat(), ATTR_END: period.end.isoformat()} for period in self._on_periods - ) + ] - def _from_string(self, value) -> None: - self._on_periods = [] - value = value.replace(" ", "") - for period in value.split(","): - start, end = period.split("-") - self._on_periods.append(Period(_read_time(start), _read_time(end))) - - def _status(self): - """Return true if the current time is in an on period.""" - now = datetime.datetime.now().time() - for period in self._on_periods: - if _is_in_period(now, period): - return STATE_ON - return STATE_OFF + def _on_periods_from_attribute(self, periods): + self._on_periods = [ + Period(_read_time(period[ATTR_START]), _read_time(period[ATTR_END])) + for period in periods + ] async def async_set_on(self, start, end): """Add on period.""" @@ -254,8 +255,7 @@ async def async_set_on(self, start, end): on_periods.sort(key=lambda period: period.start) self._on_periods = on_periods - self.async_write_ha_state() - await self._schedule_update() + self._update_state() async def async_set_off(self, start, end): """Add off period (subtracting the on periods).""" @@ -288,21 +288,26 @@ async def async_set_off(self, start, end): on_periods.sort(key=lambda period: period.start) self._on_periods = on_periods - self.async_write_ha_state() - await self._schedule_update() + self._update_state() async def async_reset(self): """Delete all on periods.""" self._on_periods = [] - self.async_write_ha_state() - await self._schedule_update() + self._update_state() async def async_update_config(self, config: typing.Dict) -> None: """Handle when the config is updated.""" self._config = config - self.async_write_ha_state() + self._update_state() + + def _schedule_update(self): + if self._event_unsub: + self._event_unsub() + self._event_unsub = None + + if not self._on_periods: + return - async def _schedule_update(self): now = datetime.datetime.now() time = now.time() midnight = datetime.time.fromisoformat("00:00:00") @@ -319,10 +324,17 @@ async def _schedule_update(self): next_change = datetime.datetime.combine( datetime.date.today() + datetime.timedelta(days=1), midnight ) - event.async_track_point_in_time( - self.hass, self.async_write_ha_state, next_change + + self._event_unsub = event.async_track_point_in_time( + self.hass, self._update_state, next_change ) + @callback + def _update_state(self, now=None): + """Update the state to reflect the current time.""" + self._schedule_update() + self.async_write_ha_state() + class Period: """Simple time range.""" diff --git a/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc index bfc234a458fbee5777bf053de564ccfeff801b6e..5e9f1a1f611909101eabe6585cd78d3bf54bcc59 100644 GIT binary patch delta 3546 zcma)8YiwLc6~1%#-rei|Ho!}{=%W7 zxsVYel2Z@oBCxu_9!bWb*xYc*OOUCXxR+KqNcuUmhwb;WD+(t4wh z`iy?sU<^>dk)VynAPpGXnVQ+bdRcR2CsQj!PB#(y~bx~(0Iz}iqoi-1MDDl zJw$^vbd6NO4FcGj)Hv+aKQSO}E+ zSfX@S3`{ZB3WnogXr*mXZ(Eg(QQA&B)+A$$#;FefC#XCnB|1fq{KYZ<)Y#RkrlyZgkC_uEPT1OpI-Vf5mP%%`7m}%q3EB3h3fU}6S?NOFR@nl}TjCG$ zfbhGfz1~9JoM${;ptt5+*JYBr^^v-l5KC@B*tUT|GmNCtPO9FJX&3DVSX*ch?FEl0 zi_rnvNBh?ZjNN)=honeG8vvHz!@%IB_DMF+SrL|K1=1x7fdQh3Zw7R~?K2(p%u>Esx*&cY=xz8OcosDflI$kpuYr5y zrua?Mfk6V6M3F;)5tu6ja;}gxpF#nPWc_@lZ^B zBUm6C#&oKX{{V9MSGL($0=Eow8fZExHv=t6(NA zi>EJ?ELICu%!QlCuf$S#y%wd2N4(@+F&NqBMHp9cnrFmZq^E17Ofs&DVyRWaN6J#g zjde?n!f7V3gB4Fz{2($+H1X%i6DziNYo1~aojizq zKtD{JYY=Ki8ikVr{BVt@kTjb=Bmm+)1*O;n0-1TXZJAiW)C{S}RmpOJrl6LV*X2vf zl;p&dP;IXRJY<8(X&wS!yDnWU%oKPomb7fmX&f6LW7H1r&>h4i-RtAIEZokceAWK3?kkl%pL9#8xo6 z4U`CCgvXNt9{(39T!4dA2O?#WGO^?+I6ImPwbe(kQf&8gPWrRGCTB@%TIn3)OE8Lu zmqGGnXDvAFU!Xj|<9}O2u=<)9P02DsM_x2{5;E3TPYT}#TUVpP-|xb=~e5UMJ9%fh zz3oZaOGHO(xA<4v@5OuV2i>n?=QYvP8y4qULq7gG*1v?}iZjBOLDk_r@UMtm$KJD7 zp_=gVH?YHL6xUEVWW#W6H)pxR0^`@wj1kvjM+)&bQG9a95o~gp@7)cm0uvU8L_EGn zOvZ;BgJ5#*1I#F-D!w1@Zjft};&3&Z2ljy4i;f65+yY$5{QUB`ZHuv{HZ4- z-qfRHP~6l2Xfq!GjhdMm6n0P6$SofuqYLHepSh` ztL$1NninB|ejSFVWLCd9SK0P~U z85-Q<3nmbeBiW4lJbTG9Q*+7uEPL!Q*#UF&hTll#yKs;8fg7YEqCq-x69rhQ5g$>U zD=SQb3W7rIr@Ih(;J zmt#zoIEUAVj=5!M9MZtIAqqR3EWVsinTtFP_sm@K(W}OR%SjEj{2<Q7IOcMW`HADv=6F~*5}&W5z~%5d6pP}O?)FnJq1uaL7{qs>IKXQFc)%l*Fch!y zP|-jK+cw3gX>RypTBoK4Rjpkn;)CuHvLfO=Z>Z;Coae=ld-l6ggNGAjnh>7#sZB_5 z0=@!C<>=_EU{5IAp&qi13lx|?;BBlfB(tT*8N)9_;~5;vd3zClS%}_|;OppD0Rh`q zMcqjkhcWA%f2cFo6HoJL?BPs`OydZ=?OiBjOY>H#P%74P*QnUrH!t&gQR%xxd?MJN z5J&sdKIhh{T{?MjyMLemE%bI5;fa)=<+H*yFi0|DXy8l$7wv@0om>q_1j6En0}aiW cp#j*7p9~-Asq?`bBq)A6FsJVK*J;Xs0W@=Q)Bpeg delta 3269 zcmZ`*Z){sv6@T~o`Ps1@$FUtJvE$^`iQTwO({^dst+d(FCfy2C%96BWkFJ*Md(+zW zzuxCWC3TvP1dWZhT6Xy(K_zU&2mVNWpdXMX*dLH!d;qB$0`xW^B=!L|2@MHNqDYi; zp53feBhEeV-22YC_nzN9zjNP>lW)#-p6u-ODDZb7w310}`r_oDn>$i2l@UgZ&dp~I ztr5l6L!~fPh*Ih?BKC+@>@D>feYWl@_LrhY)Yjd_?o!N%*?LEDpcFUaw(cnomWGU> zl5XgwZN@gDaN0=l;U+OgnAb=$pOIpn#&+g6Mp>7UW&vXd>o#`sHLWnllRVPc#kIyb zwB6j>Bs@~sW9uEZzBf-9ZPF&m6Rs5=V!`Xwm;h}IZ1(Y#ZL{Cjx3}~>-LiWa#>ess zY~%n>8jrA$@u+Qyt7wgbYy4ASdWeNs_&RBV8*E@}QRA@P|HKU$lU(OR{4v&Z)4?LF zcSAKEe?nn>tbapc{XAHhT6Y*nU?j?7FcRZY=pTjt0E^CM_W5~%<|9A%$+)ZbS5)5^VEr{=`^w2xooj`Hk-Qu zx+hmD7J06guaxCGbkxb0dATO<($8s{SCIp(rxb@1Sp4H7_o^dgr zCL%6h_r)U-u&k51l2OhnAPGaA%H_Z)ama56borY=()(R7GBpqim3IU0lOM?UyAR4A z1_#K9d@DE@ZqMlup9d4M1BFx02BVr<&X)L(<YSXnTwo6gV7Bcq^>mXr<}Ee}Tac^u%sjx!yTmm(vBN9v^D5JzCgDr!waaQDupavZA0lk&C50iwx| zA`hJv=fKJGWfn!H)ZUvF$e6kFoGlf3W~s`uHJ+y8Ghk-hexFz!nM|&jHO)*$4B>DR zMG3@jp_z8oq5$}6ePHAWf=qs*}Z+bNJ~Wt}t$0IJTe&@nxS zh~N7|M4Zq}Y$*_R<+;IC!XO_CN`o}0K!S&xYjzZRuEf}SRIGX@iMmDUe^&bb-`uk~ zJ2k)@{}2!tw4>0>Sr9fY0Ez}s11s$+0Uhvd0=CbMt8fK@1Q4KU1X=jY|%ag8}}B_@=nq-QGpllVi0z=5DhZL55JY@ z13285mhzp@)58<@cqf*)o?o>2ms=77%7~8Z>v>a7hY$B+IS_~x-T-d;I%)%BfS5R9;Gu5oX25%2XrbHF`Z6xnQ z55crNm2#$CWgf^|u^Zlh0L@Iuw+Dt*H=t>A@6Ury*&UCOzFWKE9RvgL-3wr1a8mmc zxQH*yH+sVIPqC0syomjmP<#d9>beGM0+=bTgR-)#J!SOl4d|vjKaNwpj6Qb0V~mz2 zc&W0??cjtRCX%rg`X*|xp!noaCBCvx%d-bm4VE|e$lB0ej4IaNf=m8qz{eVYY# zS+ENoy;tZy7#a>bTASKh4r(dH72FGsqd0Nv>d@d8ozw^9ZrH4rUD1gtU7^awC-G|V%GN3fT1oZ zU(SLX_zIz2{9awHQ&n*&7l>dm-f=+@SIa6<%6c!EP0Z3QF)5L9g zvAkXYS<4kG;tJrxkoLlD9y#X4wKClYO|Otw!^KAYH0I^~Or zkszXxcHKv%b@_Ut50d|niJ^dfh!Y4UUar7^!QmmepC^Y$X*wn!9vLDTd1^#|n!!uK zQQ)N`3Lq@^i7H^?+{D*VRORiF_{>FAlPK`J zq6y6$2jM0v4QP~vU6gp#`x^8ks?Y7${4TfK9djq#TF@yUO-_<4@>24eGXu^~$&S>1 z*!5$nz7VGFaSRSG0-RM`vB`2JH8zOcNvka|5l8V26i9vegt>}o*1ZOuIdr${I1y{| z@2Sb4O|tKx9TivM(MtBiNSssm8Dl^Fgjhg}=TYoK@eGP-5SC}TQe3LmmMZXta8lmb zUX_l~vgU+w3u74+_pY5J1x%3Kzg6Qf%C!VCVs34 zF6Yua$hurjpZ40+#VEuE;L$7po$l(##8HVGe-zt@#|ITWNPKeJj`Pm6zr(Hm7fG!p AApigX diff --git a/tests/components/input_schedule/__init__.py b/tests/components/input_schedule/__init__.py new file mode 100644 index 00000000000000..9e143367b3177c --- /dev/null +++ b/tests/components/input_schedule/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_schedule component.""" diff --git a/tests/components/input_schedule/test_init.py b/tests/components/input_schedule/test_init.py new file mode 100644 index 00000000000000..7c99f621a577f0 --- /dev/null +++ b/tests/components/input_schedule/test_init.py @@ -0,0 +1,575 @@ +"""The tests for the Input schedule component.""" +import datetime + +import pytest + +from homeassistant.components.input_schedule import ( + ATTR_END, + ATTR_ON_PERIODS, + ATTR_START, + DOMAIN, + SERVICE_RELOAD, + SERVICE_RESET, + SERVICE_SET_OFF, + SERVICE_SET_ON, +) +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +# pylint: disable=protected-access +from tests.async_mock import patch +from tests.common import mock_restore_cache + + +@pytest.fixture(name="storage_setup") +def storage_setup_fixture(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + "id": "from_storage", + "name": "from storage", + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + +async def set_on(hass, entity_id, start, end): + """Add an on period.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_START: start, ATTR_END: end}, + blocking=True, + ) + + +async def set_off(hass, entity_id, start, end): + """Add an off period.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_OFF, + {ATTR_ENTITY_ID: entity_id, ATTR_START: start, ATTR_END: end}, + blocking=True, + ) + + +async def reset(hass, entity_id): + """Remove all on periods.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RESET, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": {ATTR_NAME: "from yaml"}}}) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_EDITABLE] + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state.attributes[ATTR_FRIENDLY_NAME] == "from yaml" + assert not state.attributes[ATTR_EDITABLE] + + +async def test_set_on(hass, caplog): + """Test set_on method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_schedule.test" + + test_cases = [ + ( + "simple", + [("01:02:03", "04:05:06")], + [ + { + ATTR_START: "01:02:03", + ATTR_END: "04:05:06", + }, + ], + ), + ( + "multiple", + (("10:00:00", "11:00:00"), ("02:00:00", "03:00:00")), + [ + { + ATTR_START: "02:00:00", + ATTR_END: "03:00:00", + }, + { + ATTR_START: "10:00:00", + ATTR_END: "11:00:00", + }, + ], + ), + ( + "overlapping", + (("01:00:00", "03:00:00"), ("02:00:00", "04:00:00")), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "04:00:00", + }, + ], + ), + ( + "adjusted", + (("01:00:00", "02:00:00"), ("02:00:00", "03:00:00")), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "03:00:00", + }, + ], + ), + ( + "subset", + (("01:00:00", "04:00:00"), ("02:00:00", "03:00:00")), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "04:00:00", + }, + ], + ), + ( + "superset", + (("02:00:00", "03:00:00"), ("01:00:00", "04:00:00")), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "04:00:00", + }, + ], + ), + ( + "merge", + ( + ("01:00:00", "02:00:00"), + ("03:00:00", "04:00:00"), + ("02:00:00", "03:00:00"), + ), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "04:00:00", + }, + ], + ), + ] + + for test_case in test_cases: + await reset(hass, entity_id) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + for period in test_case[1]: + start = datetime.time.fromisoformat(period[0]) + end = datetime.time.fromisoformat(period[1]) + await set_on(hass, entity_id, start, end) + + state = hass.states.get(entity_id) + assert ( + state.attributes[ATTR_ON_PERIODS] == test_case[2] + ), f"'{test_case[0]}' test case failed: state is '{state.attributes[ATTR_ON_PERIODS]}' but expecting '{test_case[2]}''" + + +async def test_set_off(hass, caplog): + """Test set_off method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_schedule.test" + + test_cases = [ + ( + "simple", + [("01:02:03", "04:05:06")], + ("02:03:04", "04:05:06"), + [ + { + ATTR_START: "01:02:03", + ATTR_END: "02:03:04", + }, + ], + ), + ( + "superset", + [("01:00:00", "05:00:00")], + ("00:00:00", "10:00:00"), + [], + ), + ( + "subset", + [("01:00:00", "05:00:00")], + ("02:00:00", "04:00:00"), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "02:00:00", + }, + { + ATTR_START: "04:00:00", + ATTR_END: "05:00:00", + }, + ], + ), + ( + "adjusted", + [("01:00:00", "02:00:00")], + ("02:00:00", "03:00:00"), + [ + { + ATTR_START: "01:00:00", + ATTR_END: "02:00:00", + }, + ], + ), + ] + + for test_case in test_cases: + await reset(hass, entity_id) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + for on_period in test_case[1]: + start = datetime.time.fromisoformat(on_period[0]) + end = datetime.time.fromisoformat(on_period[1]) + await set_on(hass, entity_id, start, end) + + start = datetime.time.fromisoformat(test_case[2][0]) + end = datetime.time.fromisoformat(test_case[2][1]) + await set_off(hass, entity_id, start, end) + + state = hass.states.get(entity_id) + assert ( + state.attributes[ATTR_ON_PERIODS] == test_case[3] + ), f"'{test_case[0]}' test case failed: state is '{state.attributes[ATTR_ON_PERIODS]}' but expecting '{test_case[3]}''" + + +async def test_state(hass, caplog): + """Test state attribute.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_schedule.test" + + assert hass.states.get(entity_id).state == STATE_OFF + + now = datetime.datetime.now() + in_2_minutes = now + datetime.timedelta(minutes=2) + + start = now.time() + end = in_2_minutes.time() + + if end < start: + # The rare case of day overlap - skip the test + return + + await set_on(hass, entity_id, start, end) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_state_update(hass, caplog): + """Test next update time.""" + with patch( + "homeassistant.helpers.event.async_track_point_in_time" + ) as async_track_point_in_time: + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = f"{DOMAIN}.test" + + # No update if there are no on periods. + assert async_track_point_in_time.call_count == 0 + + now = datetime.datetime.now().replace(microsecond=0) + in_5_minutes = now + datetime.timedelta(minutes=5) + in_10_minutes = now + datetime.timedelta(minutes=10) + previous_5_minutes = now + datetime.timedelta(minutes=-5) + next_midnight = datetime.datetime.combine( + now.date() + datetime.timedelta(days=1), + datetime.time.fromisoformat("00:00:00"), + ) + + if in_10_minutes.time() < previous_5_minutes.time(): + # The rare case of day overlap - skip the test + return + + # State is on => update is at the end of the range. + await set_on(hass, entity_id, now.time(), in_5_minutes.time()) + next_update = async_track_point_in_time.call_args[0][2] + assert next_update == in_5_minutes + + # State if off => update is at the beginning of the next range. + await reset(hass, entity_id) + await set_on(hass, entity_id, in_5_minutes.time(), in_10_minutes.time()) + next_update = async_track_point_in_time.call_args[0][2] + assert next_update == in_5_minutes + + # State is off, and range is eariler in the day => update in midnight. + await reset(hass, entity_id) + await set_on(hass, entity_id, previous_5_minutes.time(), now.time()) + next_update = async_track_point_in_time.call_args[0][2] + assert next_update == next_midnight + + +async def test_restore_state(hass): + """Ensure states are restored on startup.""" + a_on_periods = [ + { + ATTR_START: "01:02:03", + ATTR_END: "02:03:04", + }, + ] + b_on_periods = [ + { + ATTR_START: "07:08:09", + ATTR_END: "10:11:12", + }, + ] + mock_restore_cache( + hass, + ( + State("input_schedule.a", "", {ATTR_ON_PERIODS: a_on_periods}), + State("input_schedule.b", "", {ATTR_ON_PERIODS: b_on_periods}), + ), + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"a": {}, "b": {}}}, + ) + + state = hass.states.get("input_schedule.a") + assert state + assert state.attributes[ATTR_ON_PERIODS] == a_on_periods + + state = hass.states.get("input_schedule.b") + assert state + assert state.attributes[ATTR_ON_PERIODS] == b_on_periods + + +async def test_input_scheudle_context(hass, hass_admin_user): + """Test that input_schedule context works.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"x": {}}}) + + state = hass.states.get(f"{DOMAIN}.x") + assert state is not None + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_ON, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_START: datetime.time.fromisoformat("01:02:03"), + ATTR_END: datetime.time.fromisoformat("04:05:06"), + }, + True, + Context(user_id=hass_admin_user.id), + ) + + state2 = hass.states.get(f"{DOMAIN}.x") + assert state2 is not None + assert state.attributes[ATTR_ON_PERIODS] != state2.attributes[ATTR_ON_PERIODS] + assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": {ATTR_NAME: "before"}, + "test_3": {}, + } + }, + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get(f"{DOMAIN}.test_1") + state_2 = hass.states.get(f"{DOMAIN}.test_2") + state_3 = hass.states.get(f"{DOMAIN}.test_3") + + assert state_1 is not None + assert state_1.attributes[ATTR_FRIENDLY_NAME] == "before" + assert state_2 is None + assert state_3 is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_1": {ATTR_NAME: "after"}, + "test_2": {}, + } + }, + ): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get(f"{DOMAIN}.test_1") + assert state_1.attributes[ATTR_FRIENDLY_NAME] == "after" + state_2 = hass.states.get(f"{DOMAIN}.test_2") + state_3 = hass.states.get(f"{DOMAIN}.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": {}}}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == STATE_OFF From 8d90f4e62466df5ef5b7ea231fd13994c6098b3f Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Tue, 6 Oct 2020 18:39:40 +0000 Subject: [PATCH 04/27] Updated the cache file --- .../__pycache__/__init__.cpython-38.pyc | Bin 10894 -> 10894 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc index 5e9f1a1f611909101eabe6585cd78d3bf54bcc59..ed0241fa1c00e27d9aae0d6bad5108a29c74d844 100644 GIT binary patch delta 18 XcmeAR?F;1$<>g`k0)rhJIa{>=EHVU* delta 18 YcmeAR?F;1$<>g`kf;pQua<*y#04|&a^8f$< From 30363acf00278500bc9bea11fe115be69d1a7e24 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Tue, 6 Oct 2020 18:56:25 +0000 Subject: [PATCH 05/27] Updating cache --- .../test_init.cpython-38-pytest-6.0.2.pyc | Bin 21459 -> 23986 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/components/input_schedule/__pycache__/test_init.cpython-38-pytest-6.0.2.pyc b/tests/components/input_schedule/__pycache__/test_init.cpython-38-pytest-6.0.2.pyc index 46186c081835c41fdcdf0caab62299503dfefd95..edd645457970e9eba54003ca41689770013e2282 100644 GIT binary patch literal 23986 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Ӕp7njPqP_-@zjAA+ouT34rC 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 ZU9{)zAF@a7yX`&pg#DyFY>(Iz{~x%$u66(b delta 8405 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 From 11d2e48a285b4e789bbaa542d46c48b553ec60ae Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 7 Oct 2020 08:58:47 +0000 Subject: [PATCH 06/27] Update CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 549f4193c0929b..fcca5ed6932dcb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,6 +206,7 @@ homeassistant/components/influxdb/* @fabaff @mdegat01 homeassistant/components/input_boolean/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core +homeassistant/components/input_schedule/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core homeassistant/components/insteon/* @teharris1 From 818370d98e9d49e6c37560d370654b7ea660fe30 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 7 Oct 2020 10:25:39 +0000 Subject: [PATCH 07/27] Fixed setting off an entire on period --- .../components/input_schedule/__init__.py | 3 ++- .../__pycache__/__init__.cpython-38.pyc | Bin 10894 -> 10906 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 202 -> 201 bytes .../test_init.cpython-38-pytest-6.0.2.pyc | Bin 23986 -> 24016 bytes tests/components/input_schedule/test_init.py | 6 ++++++ 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/input_schedule/__init__.py b/homeassistant/components/input_schedule/__init__.py index 3d10ebbdd01a64..f753564832cd1a 100644 --- a/homeassistant/components/input_schedule/__init__.py +++ b/homeassistant/components/input_schedule/__init__.py @@ -270,7 +270,8 @@ async def async_set_off(self, start, end): for period in self._on_periods: if _is_in_period(start, period): self._on_periods.remove(period) - on_periods.append(Period(period.start, start)) + if start > period.start: + on_periods.append(Period(period.start, start)) if _is_in_period(end, period): on_periods.append(Period(end, period.end)) break diff --git a/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc index ed0241fa1c00e27d9aae0d6bad5108a29c74d844..5f36de1c2244b1cee08f2b0e30c4c0098e837d02 100644 GIT binary patch delta 346 zcmeARofXO(%FD$71bWkJML*ax%ut_cqu?Aq% zA!?_gm<~153TP-3T;qvCup*#FrCJt{ku{9jjK$BuRx@R@7M%lG2XS8Exycf8E4dXJ z85oK>fgY>Uo$RO}GWmg=6SpvsThs?4>L=^UyNMeBnG8&PjC_n7j9g4C%v_9Ij6957 zlRM;BF{({AR@lO*z4?WLA`_#*WG>~ujJ}&WR5TbFl_ndiUS|xS%&8W{7&19Vt(7r$ V@*}lk#>CBG>c-5hW_+9+ECBf?R5Jho delta 320 zcmbOg+84?j%FD$71O_{5;_qo~CF$Pc0P-|t3nfy+zm@#2z9t30Yprs&1T3dXZW=)F~k_9ib zDBgv@94Z(gxhQ%OJKlQq3wZV5HxTg~)Y&L19tM8Te||hX|GjS0BmBEN?ttlSwE zC^!83A3;}(^hD=+2MuB4hj^4f`=lS^agNP$s|$>B5<6Wy(b%ClKXxZ*+Ze(iP)329 z6Bt5&UU4Sn9ju&`$pqNkKXxazV`pkFrAErio@0kcnE)j=E3*o>d!RTQYr-lN#TnPA zRVnJAjSGnr7q?1dk2`Fhq6s9qKXX;Ay5-ul-?PDvQ1qYkE31md;9T`6MTx~Rx3+QO za(9RaFVr<@Q&%;uiT5r&e{6))i&Q?#>fA`Ty+rIdH@AW~oN_~@YLiM&yi%=MGeoQ+ z4&0i#$M6l*Y>861OxBFeow(QEl=s3n03%`3wrJCZ%onXKA5vF`hD<8hgp2ZNeO-gD z{MsxS@LYaBz2L!XS-(tl_$E(XZEKK~_pfOf4&;NSOD1^o?aCb;PD=aME-qdtGo(ct Lgpx(#5uebXNBonL delta 545 zcmZ9HJ#W-N5Qca5eD>Pd@m=gYX9MRDj(i+KC=yXX&;W;kA_4?TfjC5pk*!N43eHNk z7NUV76eqTzNQmqPN(5U*&`{9w59s(0hyvCD0iqb`nRgzIW2q7PJjrQc&yCFY>!9_O(^iq--IDr-WnTd_e!sd`_n$#2wX-f4x#Fn17|ISlL zjMR{E>-?i6qlJf&uoJ*3!7?|YrGY$~DEyLanqwF_+Gq%?gfTn}HPnf zvCmd^<{sFWDx+d;Pvha;TASn!9>I=qhM@USW=OlyluN$ZjXAkY?w!lsthq5+1j5A` zPD;qVKYc9k_?_<7@7~0toc!nf%3^t|eR^h=liU_m*qb!>g+HdFXZo7zQ(u32Ouq5? zneHyFF4FlF9v5b{ZHM5Tur}IRbingE)thv>@KS$`ixI(&{NZ=4S4eM@UQfo+O|=!h z26!%GzAOj)WbKP_N^Nt$4zp?}rWUNIy}5w_>*{;2VZu}8F0BMmsD7W-;j3D`wroIM oJzg~^d{a-ZU$DSapVrpvFs~NxzM##kti?{S1;*JG7O;@ Date: Wed, 7 Oct 2020 11:07:09 +0000 Subject: [PATCH 08/27] Add "input_schedule" to "default_config". --- homeassistant/components/default_config/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index c25b9b82c38dd0..86256ae1d17dc4 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -25,6 +25,7 @@ "input_datetime", "input_text", "input_number", + "input_schedule", "input_select" ], "codeowners": [] From 7ce12c8c7459f1ac67d1d6b64ee28992d8c32768 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 7 Oct 2020 11:42:55 +0000 Subject: [PATCH 09/27] Renamed "schedule" to "timetable" --- CODEOWNERS | 2 +- .../components/default_config/manifest.json | 4 +-- .../__pycache__/__init__.cpython-38.pyc | Bin 10906 -> 0 bytes .../components/input_schedule/strings.json | 1 - .../input_schedule/translations/en.json | 3 -- .../__init__.py | 32 +++++++++--------- .../manifest.json | 6 ++-- .../services.yaml | 14 ++++---- .../components/input_timetable/strings.json | 1 + .../translations/af.json | 0 .../translations/ar.json | 0 .../translations/bg.json | 0 .../translations/bs.json | 0 .../translations/ca.json | 0 .../translations/cs.json | 0 .../translations/cy.json | 0 .../translations/da.json | 0 .../translations/de.json | 0 .../translations/el.json | 0 .../input_timetable/translations/en.json | 3 ++ .../translations/es-419.json | 0 .../translations/es.json | 0 .../translations/et.json | 0 .../translations/eu.json | 0 .../translations/fi.json | 0 .../translations/fr.json | 0 .../translations/he.json | 0 .../translations/hi.json | 0 .../translations/hr.json | 0 .../translations/hu.json | 0 .../translations/hy.json | 0 .../translations/id.json | 0 .../translations/is.json | 0 .../translations/it.json | 0 .../translations/ko.json | 0 .../translations/lb.json | 0 .../translations/lv.json | 0 .../translations/nb.json | 0 .../translations/nl.json | 0 .../translations/nn.json | 0 .../translations/no.json | 0 .../translations/pl.json | 0 .../translations/pt-BR.json | 0 .../translations/pt.json | 0 .../translations/ro.json | 0 .../translations/ru.json | 0 .../translations/sk.json | 0 .../translations/sl.json | 0 .../translations/sv.json | 0 .../translations/te.json | 0 .../translations/th.json | 0 .../translations/tr.json | 0 .../translations/uk.json | 0 .../translations/vi.json | 0 .../translations/zh-Hans.json | 0 .../translations/zh-Hant.json | 0 .../__pycache__/__init__.cpython-38.pyc | Bin 201 -> 0 bytes .../test_init.cpython-38-pytest-6.0.2.pyc | Bin 24016 -> 0 bytes .../__init__.py | 0 .../test_init.py | 20 +++++------ 60 files changed, 43 insertions(+), 43 deletions(-) delete mode 100644 homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc delete mode 100644 homeassistant/components/input_schedule/strings.json delete mode 100644 homeassistant/components/input_schedule/translations/en.json rename homeassistant/components/{input_schedule => input_timetable}/__init__.py (92%) rename homeassistant/components/{input_schedule => input_timetable}/manifest.json (64%) rename homeassistant/components/{input_schedule => input_timetable}/services.yaml (62%) create mode 100644 homeassistant/components/input_timetable/strings.json rename homeassistant/components/{input_schedule => input_timetable}/translations/af.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/ar.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/bg.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/bs.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/ca.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/cs.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/cy.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/da.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/de.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/el.json (100%) create mode 100644 homeassistant/components/input_timetable/translations/en.json rename homeassistant/components/{input_schedule => input_timetable}/translations/es-419.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/es.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/et.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/eu.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/fi.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/fr.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/he.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/hi.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/hr.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/hu.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/hy.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/id.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/is.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/it.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/ko.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/lb.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/lv.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/nb.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/nl.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/nn.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/no.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/pl.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/pt-BR.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/pt.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/ro.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/ru.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/sk.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/sl.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/sv.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/te.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/th.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/tr.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/uk.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/vi.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/zh-Hans.json (100%) rename homeassistant/components/{input_schedule => input_timetable}/translations/zh-Hant.json (100%) delete mode 100644 tests/components/input_schedule/__pycache__/__init__.cpython-38.pyc delete mode 100644 tests/components/input_schedule/__pycache__/test_init.cpython-38-pytest-6.0.2.pyc rename tests/components/{input_schedule => input_timetable}/__init__.py (100%) rename tests/components/{input_schedule => input_timetable}/test_init.py (96%) diff --git a/CODEOWNERS b/CODEOWNERS index fcca5ed6932dcb..421381bf620596 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,9 +206,9 @@ homeassistant/components/influxdb/* @fabaff @mdegat01 homeassistant/components/input_boolean/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core -homeassistant/components/input_schedule/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core +homeassistant/components/input_timetable/* @home-assistant/core homeassistant/components/insteon/* @teharris1 homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 86256ae1d17dc4..f165ff6e1579a2 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -25,8 +25,8 @@ "input_datetime", "input_text", "input_number", - "input_schedule", - "input_select" + "input_select", + "input_timetable" ], "codeowners": [] } diff --git a/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/homeassistant/components/input_schedule/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 5f36de1c2244b1cee08f2b0e30c4c0098e837d02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10906 zcmb7KOK=-UdY%_R0DMyvCF%*udcm?G?XF+zm9?fxN@7-`9Fn%zll9Jm=q5QNUhEl2 zOB9%OmP#zUqGY!==B*DvI?WcF?)>j zvBG$9!k#D|vJY|DC`=X)8=9^ar|ctK6E7SsPTSL*A1E9v9=DHkK2bPPJZYcge6nz= zc-lT)Oxx+=8T*W`NuzOAKGW3gb0TG*7lZZ%F=Ss9!}cXHVrRst{j3el=KFRs7xrQ*BMpJinwEwyoe`MI-;GVI@3-SWDc@gbjkQc+e zYqZoP4* z%WsRxPfT%GOg*sd*Kca#h&cK{6G!F5=FFaHzk!-*aSS!bGb<}g&ivfM%FG+L=Y!L&+?EFW57@R9CG#x@N6BN`=O5dL2P&tFv4;xqMW_MEn!u z1MLHS%@6}3@nC4zdZ;y~{bT-dkz{(PbONQ2Ix3xXH4(1t)aL2U^yV3!<80mXpAkck zV$c!8;O>#y^fFkwS^)#6`5@bDovn>!cXGvov(HhrLs?>)+`8Ymc?Zx}qMB&A`ZB4NO-!fkmgTPNSBS z-^^XPUshXQC6|}pl|0z=$_Drn{1Ngcsv^eru5>fw6~}Q)uJ1V6%1)5FRzQ1ddp(b# zJ(@L+PwjNh+bQK8Fk`iny;dkgoV@GEv|2Ahu7rPJkpniplPyXdOei*=e*=XDDg!y*f|D z3rKuC`+N57zI&#aO6(-72xkLYu^?5yJ>h`GKjdKp7qB#O=Dx>ZRw0JHB;t44^@(dCu9OEWj;oww(gmKR{G23r;1nSWPhi2P?M z0dmspV+RI#N3MG1{1(n|cy?)?EZB{O`P*~L!SGve&UF-{{bD(^Pj#-7%_B3$Odg!*p&=(=z*-`p;zZ`*(0)6uk;ADoF-2mjPSUQWp!bT=(N7#y zvMNh0St_5S982YOKou(0=cAw?WIQ;pBQXSEqh1~bM1(Tp)M{49$0(2Ju1iAZ4STv@1aOXPq|Sao;82+EVL{1lHzQdrj-FjP$) zgX^`EZ|4dwI!Xp}AsR`$UiuzYX8~7QEFGOGrtxH9O)X%c9xWP7Iu%uh3GVsgz$>9q@Au*{B5i8J*U(A*7A3Dy@+OTD_jf8}2CO+Z?<;~DDoOPw zw4++2oF(YZZ_}H7GtN2I8YC_s;qged!{%WjDPt0PV$xz!%A>mLkC%%Y@4t|A%k*Xp zvI|@TBy8Dn3E4&s&~JzACS+11MDhWUv7JIbB?dV^i2RTk=KK)yBVv^E!^n?`an6s( zBu0evPitZV^BfYBtPjRN(AISJ@)Gt$=;PUk1bcS&Wa!b^ha+!pD)Q!zFl}d;wzJ|H z(0f#z6X)UQP0M5AlDHr)KF~qq@ecrC?Gs?9jCvUhuAN+xfG8d;YmU4sczIwiFS4Q} zDx|-=921hfw)ByT2Zw47<1YsR1LWa8BQ@E3fIrA79_ZdeLbgI@~W$kBe^BQI#_^CpCChsi2n*DRt=gF*XdK6>cR)vRzqk3 z9_WDLG%*74#w?9Q2(gZZ?lI0MUkfTnpVoe^{lcspykNsY4*P1>UW~*vL@V$+$i2)< zWLo0Wjl6?K7W@^R?lwbJB*%!5&zSS+V20BPjym!uR@WN+}a@M>6h?n@wV|Dd-~ zt`>wteg2wAAwJQ}+H^M&9jy+KLEFw`5Qwb9&1k^sjjYv$Xk$s2lV%}irMyeXuY+k# z^*iVsf;Q6hN>I{6t3@OH4PLZ}oaiQ!UVkO(E_YDpf}bg%vMSS6GS#qRpcIBRJkuNXJ_ ziXhPGXnEC=yIsY(QRt54q{Vzj$b7^peaTLSJ%M`u2~Aq^(W6P)ZMqJCsG@QAs3X~S zAK8Y^aUz|gM$C(d8lXf`19iQwZz7uI$C{Ma8gVN7v1yJTp*+x}I6C+ z-I`l1L-IpG%L(Ww%2Q}9yO>EJR#ydw1&2ZxhTqefap)~_&k3#-avrcA@f_UpHY0@U zql6^{$2t$4HE6pT7(IeQJU|Om7SZjo9wZ$&bfCs@LLMh2sXQeAj3?v+V+{Ta{1TwM zZa(Nkbk|OI@xU0yqnJFREs*#*pb@e}-4PQy8WKScUY$dRBPJ(yft|7C2-h?btV7PC zoErtOnC?t0NU+{-$FE`cGu@0>++GL%! zm{5-(uj7k0nver;^eNhCpKyaQjn14iDYw_P)m9LHlS9gY({Q7N!#cHwQB<9hT}t|F z>wlnre_O9TeXZU6+izpl?_#WolkI^}M|-_<-b0&zr#V|YY3cJ_WvC^5 z$FcDi`q7&;`reG=%?s3G*Vul+9|%q1y1r{g2J-B#NpEQ;@6V`x-Pnq$_v=<8fuJ>f zvTfuylN7jqpoxJ;{^MRXDJ~4l<_-t|aIH%ZnQaetSR*(6B7jbQC22h&$_x5DyYmeD#sU zAaSjbE3S&%^(ScaKX|m-bKR#G>K{a_Ad{V35_W@fGq>bUW-u5)fU|(mwR#JK1Op2t zjva)exrHiL3QUUn5w(ctDiyjsu{>Nlsr#rxq=+#AksG#ViVst-x(0p;q1D^F7IUEmX!Q!! zHDYxu^5QMk=k3D?mPU`Vd^v)pe3g3a(ezMIY0k~b|(eV~-KkgcR ze@uWnnh*W{ew*8%-&t@rMr3~ePoA0h`5$Svkh*Q;H&b2wj1@6I4|eeLbIi}U1`KsV z`Vzb`Ty5bxF1ihD^SYPGhTN`xk52Ick}t!y>i4PjA5iiEB|WGA5Ors8JnBPaYR^CQ zbl27nGOYRteGpFjLmHP@RXw7FEeNqG-N6;h_hiVae?;$yT|<8SW6JesRp!z@5~c`# zSQ9@9+ zAETofWE9_GBH3ADA|0Z%n=e`bF9KrFs<`02+(mR>Y}M7iYf}j(kwTco;}MP^t*R!m zsTyx;w8fjccZvXjZ)_UmUJ#y*?qvJhz7#U{TTJBElvHh1Wqa)?dg$f_dxGmYx_p{9%-ThvBme0 zK;-$6egt>5)Yczvj9?|Bc*g4SCW&|mV64Sj;_B5``ClEwdJ*|WJbA~Wq z2(e!!4tW8y03X7N#KGJYElfZdEf&g2d>?~lv0zu?`Xt>v;BLVJSOIYD@!EO^?<)$- zPJ|*`jMO8g&%ebXUG~-3ogi;U$u;`L16Ss&O2PAC7{GGz7Z@yvT$%d{SBP+KLRY5c ziST)KZw2q_owj@+QdH|vH{CvszL+`55ni=~=@dZ?wL-}|lzf+xA0P<^+1Dvbf1@lo z8WrGp%CHswj*`@Bjgoar$UjmQBthbCh2oC6f_j(gwkh!_AqcMsWhyfItWFhaBlS~C zXyF_g=EcAZ3mvVzEEHPK7BBjT)b2bbFC+O&JbdI77#~i0Dy3Qgx9r+Ww%(tPilz4 z(~7t>+XX~SG@=2SQOKu@bS8yU5n9DSM(uD%IF%G;I1FHAXt{{JT0xe5K-I$lKR}~* z+6+KieKd4B+F{W}ko*!_^&UC(?2s%Tm$#SJx6&GtwwG7jrB(5EKRPvq0-{rZ@$@^9 zO%sllCX7cML0x3*z|aBF7*;!^an#Q!Ap;Ke6a&nx_6oUrA1~h{Drgrqoy!MAk#?DP zm{bfwy*kJueOsPFN#BA-4`X5>9BH5xq^XIoeo&efU#??LQiPeCp@cB)q+?iCf)uVl zsujOlu6iK`nZv8#P}d!77WNC59kc*Nl=9C<_`-^BTRDox@w8x~x1W;XWy#_07tx(A z-@%7!_hR1$G&3xx!vLOd1JpdxH-4Z#F!J~83;hQPBQyJV^!#f8`)FnM@mf1-bz@a{;B(yo#8@GZJ}k#fw--=N$jO7{I}3h=1cDS3ku;tTaQ zlHf31f)sPkeb?V`yh3%|fxqJg=1gfv{gN8QaY_Kbtk8nNncKJTzT?b)cV%e?m!|mL z+rrJ(9eiR*ZdY9;1zRsQLsQ(M#fh?1L&<=W bool: - """Set up an input schedule.""" + """Set up an input timetable.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -83,16 +83,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.attach_entity_component_collection( - component, yaml_collection, InputSchedule.from_yaml + component, yaml_collection, InputTimeTable.from_yaml ) - storage_collection = ScheduleStorageCollection( + storage_collection = TimeTableStorageCollection( Store(hass, STORAGE_VERSION, STORAGE_KEY), logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) collection.attach_entity_component_collection( - component, storage_collection, InputSchedule + component, storage_collection, InputTimeTable ) await yaml_collection.async_load( @@ -135,7 +135,7 @@ async def reload_service_handler(service_call: ServiceCallType) -> None: return True -class ScheduleStorageCollection(collection.StorageCollection): +class TimeTableStorageCollection(collection.StorageCollection): """Input storage based collection.""" CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) @@ -156,23 +156,23 @@ async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dic return {**data, **update_data} -class InputSchedule(RestoreEntity): - """Representation of a schedule.""" +class InputTimeTable(RestoreEntity): + """Representation of a timetable.""" def __init__(self, config: typing.Dict): - """Initialize an input schedule.""" + """Initialize an input timetable.""" self._config = config self._on_periods = [] self.editable = True self._event_unsub = None @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputSchedule": + def from_yaml(cls, config: typing.Dict) -> "InputTimeTable": """Return entity instance initialized from yaml storage.""" - input_schedule = cls(config) - input_schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" - input_schedule.editable = False - return input_schedule + input_timetable = cls(config) + input_timetable.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + input_timetable.editable = False + return input_timetable @property def should_poll(self): @@ -181,7 +181,7 @@ def should_poll(self): @property def name(self): - """Return the name of the input schedule.""" + """Return the name of the input timetable.""" return self._config.get(CONF_NAME) @property diff --git a/homeassistant/components/input_schedule/manifest.json b/homeassistant/components/input_timetable/manifest.json similarity index 64% rename from homeassistant/components/input_schedule/manifest.json rename to homeassistant/components/input_timetable/manifest.json index 8f55786749e73c..8a5cc7b910541a 100644 --- a/homeassistant/components/input_schedule/manifest.json +++ b/homeassistant/components/input_timetable/manifest.json @@ -1,7 +1,7 @@ { - "domain": "input_schedule", - "name": "Input Schedule", - "documentation": "https://www.home-assistant.io/integrations/input_schedule", + "domain": "input_timetable", + "name": "Input Timetable", + "documentation": "https://www.home-assistant.io/integrations/input_timetable", "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/input_schedule/services.yaml b/homeassistant/components/input_timetable/services.yaml similarity index 62% rename from homeassistant/components/input_schedule/services.yaml rename to homeassistant/components/input_timetable/services.yaml index 6f23c72ab518b2..ae7bdc28433886 100644 --- a/homeassistant/components/input_schedule/services.yaml +++ b/homeassistant/components/input_timetable/services.yaml @@ -2,8 +2,8 @@ set_on: description: Adds a time range when state will be on. fields: entity_id: - description: Entity id of the input schedule that should be set. - example: input_schedule.lights + description: Entity id of the input timetable that should be set. + example: input_timetable.lights start: description: The start time of the time range. example: '"05:04:20"' @@ -14,8 +14,8 @@ set_off: description: Adds a time range when state will be off. fields: entity_id: - description: Entity id of the input schedule that should be set. - example: input_schedule.lights + description: Entity id of the input timetable that should be set. + example: input_timetable.lights start: description: The start time of the time range. example: '"05:04:20"' @@ -26,7 +26,7 @@ reset: description: Resets the time causing it to be always off. fields: entity_id: - description: Entity id of the input schedule that should be set. - example: input_schedule.lights + description: Entity id of the input timetable that should be reset. + example: input_timetable.lights reload: - description: Reload the input_schedule configuration. + description: Reload the input_timetable configuration. diff --git a/homeassistant/components/input_timetable/strings.json b/homeassistant/components/input_timetable/strings.json new file mode 100644 index 00000000000000..f2e36b09f93440 --- /dev/null +++ b/homeassistant/components/input_timetable/strings.json @@ -0,0 +1 @@ +{ "title": "Input timetable" } diff --git a/homeassistant/components/input_schedule/translations/af.json b/homeassistant/components/input_timetable/translations/af.json similarity index 100% rename from homeassistant/components/input_schedule/translations/af.json rename to homeassistant/components/input_timetable/translations/af.json diff --git a/homeassistant/components/input_schedule/translations/ar.json b/homeassistant/components/input_timetable/translations/ar.json similarity index 100% rename from homeassistant/components/input_schedule/translations/ar.json rename to homeassistant/components/input_timetable/translations/ar.json diff --git a/homeassistant/components/input_schedule/translations/bg.json b/homeassistant/components/input_timetable/translations/bg.json similarity index 100% rename from homeassistant/components/input_schedule/translations/bg.json rename to homeassistant/components/input_timetable/translations/bg.json diff --git a/homeassistant/components/input_schedule/translations/bs.json b/homeassistant/components/input_timetable/translations/bs.json similarity index 100% rename from homeassistant/components/input_schedule/translations/bs.json rename to homeassistant/components/input_timetable/translations/bs.json diff --git a/homeassistant/components/input_schedule/translations/ca.json b/homeassistant/components/input_timetable/translations/ca.json similarity index 100% rename from homeassistant/components/input_schedule/translations/ca.json rename to homeassistant/components/input_timetable/translations/ca.json diff --git a/homeassistant/components/input_schedule/translations/cs.json b/homeassistant/components/input_timetable/translations/cs.json similarity index 100% rename from homeassistant/components/input_schedule/translations/cs.json rename to homeassistant/components/input_timetable/translations/cs.json diff --git a/homeassistant/components/input_schedule/translations/cy.json b/homeassistant/components/input_timetable/translations/cy.json similarity index 100% rename from homeassistant/components/input_schedule/translations/cy.json rename to homeassistant/components/input_timetable/translations/cy.json diff --git a/homeassistant/components/input_schedule/translations/da.json b/homeassistant/components/input_timetable/translations/da.json similarity index 100% rename from homeassistant/components/input_schedule/translations/da.json rename to homeassistant/components/input_timetable/translations/da.json diff --git a/homeassistant/components/input_schedule/translations/de.json b/homeassistant/components/input_timetable/translations/de.json similarity index 100% rename from homeassistant/components/input_schedule/translations/de.json rename to homeassistant/components/input_timetable/translations/de.json diff --git a/homeassistant/components/input_schedule/translations/el.json b/homeassistant/components/input_timetable/translations/el.json similarity index 100% rename from homeassistant/components/input_schedule/translations/el.json rename to homeassistant/components/input_timetable/translations/el.json diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json new file mode 100644 index 00000000000000..b3691261ae1268 --- /dev/null +++ b/homeassistant/components/input_timetable/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Input timetable" +} \ No newline at end of file diff --git a/homeassistant/components/input_schedule/translations/es-419.json b/homeassistant/components/input_timetable/translations/es-419.json similarity index 100% rename from homeassistant/components/input_schedule/translations/es-419.json rename to homeassistant/components/input_timetable/translations/es-419.json diff --git a/homeassistant/components/input_schedule/translations/es.json b/homeassistant/components/input_timetable/translations/es.json similarity index 100% rename from homeassistant/components/input_schedule/translations/es.json rename to homeassistant/components/input_timetable/translations/es.json diff --git a/homeassistant/components/input_schedule/translations/et.json b/homeassistant/components/input_timetable/translations/et.json similarity index 100% rename from homeassistant/components/input_schedule/translations/et.json rename to homeassistant/components/input_timetable/translations/et.json diff --git a/homeassistant/components/input_schedule/translations/eu.json b/homeassistant/components/input_timetable/translations/eu.json similarity index 100% rename from homeassistant/components/input_schedule/translations/eu.json rename to homeassistant/components/input_timetable/translations/eu.json diff --git a/homeassistant/components/input_schedule/translations/fi.json b/homeassistant/components/input_timetable/translations/fi.json similarity index 100% rename from homeassistant/components/input_schedule/translations/fi.json rename to homeassistant/components/input_timetable/translations/fi.json diff --git a/homeassistant/components/input_schedule/translations/fr.json b/homeassistant/components/input_timetable/translations/fr.json similarity index 100% rename from homeassistant/components/input_schedule/translations/fr.json rename to homeassistant/components/input_timetable/translations/fr.json diff --git a/homeassistant/components/input_schedule/translations/he.json b/homeassistant/components/input_timetable/translations/he.json similarity index 100% rename from homeassistant/components/input_schedule/translations/he.json rename to homeassistant/components/input_timetable/translations/he.json diff --git a/homeassistant/components/input_schedule/translations/hi.json b/homeassistant/components/input_timetable/translations/hi.json similarity index 100% rename from homeassistant/components/input_schedule/translations/hi.json rename to homeassistant/components/input_timetable/translations/hi.json diff --git a/homeassistant/components/input_schedule/translations/hr.json b/homeassistant/components/input_timetable/translations/hr.json similarity index 100% rename from homeassistant/components/input_schedule/translations/hr.json rename to homeassistant/components/input_timetable/translations/hr.json diff --git a/homeassistant/components/input_schedule/translations/hu.json b/homeassistant/components/input_timetable/translations/hu.json similarity index 100% rename from homeassistant/components/input_schedule/translations/hu.json rename to homeassistant/components/input_timetable/translations/hu.json diff --git a/homeassistant/components/input_schedule/translations/hy.json b/homeassistant/components/input_timetable/translations/hy.json similarity index 100% rename from homeassistant/components/input_schedule/translations/hy.json rename to homeassistant/components/input_timetable/translations/hy.json diff --git a/homeassistant/components/input_schedule/translations/id.json b/homeassistant/components/input_timetable/translations/id.json similarity index 100% rename from homeassistant/components/input_schedule/translations/id.json rename to homeassistant/components/input_timetable/translations/id.json diff --git a/homeassistant/components/input_schedule/translations/is.json b/homeassistant/components/input_timetable/translations/is.json similarity index 100% rename from homeassistant/components/input_schedule/translations/is.json rename to homeassistant/components/input_timetable/translations/is.json diff --git a/homeassistant/components/input_schedule/translations/it.json b/homeassistant/components/input_timetable/translations/it.json similarity index 100% rename from homeassistant/components/input_schedule/translations/it.json rename to homeassistant/components/input_timetable/translations/it.json diff --git a/homeassistant/components/input_schedule/translations/ko.json b/homeassistant/components/input_timetable/translations/ko.json similarity index 100% rename from homeassistant/components/input_schedule/translations/ko.json rename to homeassistant/components/input_timetable/translations/ko.json diff --git a/homeassistant/components/input_schedule/translations/lb.json b/homeassistant/components/input_timetable/translations/lb.json similarity index 100% rename from homeassistant/components/input_schedule/translations/lb.json rename to homeassistant/components/input_timetable/translations/lb.json diff --git a/homeassistant/components/input_schedule/translations/lv.json b/homeassistant/components/input_timetable/translations/lv.json similarity index 100% rename from homeassistant/components/input_schedule/translations/lv.json rename to homeassistant/components/input_timetable/translations/lv.json diff --git a/homeassistant/components/input_schedule/translations/nb.json b/homeassistant/components/input_timetable/translations/nb.json similarity index 100% rename from homeassistant/components/input_schedule/translations/nb.json rename to homeassistant/components/input_timetable/translations/nb.json diff --git a/homeassistant/components/input_schedule/translations/nl.json b/homeassistant/components/input_timetable/translations/nl.json similarity index 100% rename from homeassistant/components/input_schedule/translations/nl.json rename to homeassistant/components/input_timetable/translations/nl.json diff --git a/homeassistant/components/input_schedule/translations/nn.json b/homeassistant/components/input_timetable/translations/nn.json similarity index 100% rename from homeassistant/components/input_schedule/translations/nn.json rename to homeassistant/components/input_timetable/translations/nn.json diff --git a/homeassistant/components/input_schedule/translations/no.json b/homeassistant/components/input_timetable/translations/no.json similarity index 100% rename from homeassistant/components/input_schedule/translations/no.json rename to homeassistant/components/input_timetable/translations/no.json diff --git a/homeassistant/components/input_schedule/translations/pl.json b/homeassistant/components/input_timetable/translations/pl.json similarity index 100% rename from homeassistant/components/input_schedule/translations/pl.json rename to homeassistant/components/input_timetable/translations/pl.json diff --git a/homeassistant/components/input_schedule/translations/pt-BR.json b/homeassistant/components/input_timetable/translations/pt-BR.json similarity index 100% rename from homeassistant/components/input_schedule/translations/pt-BR.json rename to homeassistant/components/input_timetable/translations/pt-BR.json diff --git a/homeassistant/components/input_schedule/translations/pt.json b/homeassistant/components/input_timetable/translations/pt.json similarity index 100% rename from homeassistant/components/input_schedule/translations/pt.json rename to homeassistant/components/input_timetable/translations/pt.json diff --git a/homeassistant/components/input_schedule/translations/ro.json b/homeassistant/components/input_timetable/translations/ro.json similarity index 100% rename from homeassistant/components/input_schedule/translations/ro.json rename to homeassistant/components/input_timetable/translations/ro.json diff --git a/homeassistant/components/input_schedule/translations/ru.json b/homeassistant/components/input_timetable/translations/ru.json similarity index 100% rename from homeassistant/components/input_schedule/translations/ru.json rename to homeassistant/components/input_timetable/translations/ru.json diff --git a/homeassistant/components/input_schedule/translations/sk.json b/homeassistant/components/input_timetable/translations/sk.json similarity index 100% rename from homeassistant/components/input_schedule/translations/sk.json rename to homeassistant/components/input_timetable/translations/sk.json diff --git a/homeassistant/components/input_schedule/translations/sl.json b/homeassistant/components/input_timetable/translations/sl.json similarity index 100% rename from homeassistant/components/input_schedule/translations/sl.json rename to homeassistant/components/input_timetable/translations/sl.json diff --git a/homeassistant/components/input_schedule/translations/sv.json b/homeassistant/components/input_timetable/translations/sv.json similarity index 100% rename from homeassistant/components/input_schedule/translations/sv.json rename to homeassistant/components/input_timetable/translations/sv.json diff --git a/homeassistant/components/input_schedule/translations/te.json b/homeassistant/components/input_timetable/translations/te.json similarity index 100% rename from homeassistant/components/input_schedule/translations/te.json rename to homeassistant/components/input_timetable/translations/te.json diff --git a/homeassistant/components/input_schedule/translations/th.json b/homeassistant/components/input_timetable/translations/th.json similarity index 100% rename from homeassistant/components/input_schedule/translations/th.json rename to homeassistant/components/input_timetable/translations/th.json diff --git a/homeassistant/components/input_schedule/translations/tr.json b/homeassistant/components/input_timetable/translations/tr.json similarity index 100% rename from homeassistant/components/input_schedule/translations/tr.json rename to homeassistant/components/input_timetable/translations/tr.json diff --git a/homeassistant/components/input_schedule/translations/uk.json b/homeassistant/components/input_timetable/translations/uk.json similarity index 100% rename from homeassistant/components/input_schedule/translations/uk.json rename to homeassistant/components/input_timetable/translations/uk.json diff --git a/homeassistant/components/input_schedule/translations/vi.json b/homeassistant/components/input_timetable/translations/vi.json similarity index 100% rename from homeassistant/components/input_schedule/translations/vi.json rename to homeassistant/components/input_timetable/translations/vi.json diff --git a/homeassistant/components/input_schedule/translations/zh-Hans.json b/homeassistant/components/input_timetable/translations/zh-Hans.json similarity index 100% rename from homeassistant/components/input_schedule/translations/zh-Hans.json rename to homeassistant/components/input_timetable/translations/zh-Hans.json diff --git a/homeassistant/components/input_schedule/translations/zh-Hant.json b/homeassistant/components/input_timetable/translations/zh-Hant.json similarity index 100% rename from homeassistant/components/input_schedule/translations/zh-Hant.json rename to homeassistant/components/input_timetable/translations/zh-Hant.json diff --git a/tests/components/input_schedule/__pycache__/__init__.cpython-38.pyc b/tests/components/input_schedule/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 5e713ae154d2179033053c04c0d76f78416a3e09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201 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 diff --git a/tests/components/input_schedule/__init__.py b/tests/components/input_timetable/__init__.py similarity index 100% rename from tests/components/input_schedule/__init__.py rename to tests/components/input_timetable/__init__.py diff --git a/tests/components/input_schedule/test_init.py b/tests/components/input_timetable/test_init.py similarity index 96% rename from tests/components/input_schedule/test_init.py rename to tests/components/input_timetable/test_init.py index 1c1e00aed94750..bd96433c3c03e4 100644 --- a/tests/components/input_schedule/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -1,9 +1,9 @@ -"""The tests for the Input schedule component.""" +"""The tests for the Input timetable component.""" import datetime import pytest -from homeassistant.components.input_schedule import ( +from homeassistant.components.input_timetable import ( ATTR_END, ATTR_ON_PERIODS, ATTR_START, @@ -116,7 +116,7 @@ async def test_editable_state_attribute(hass, storage_setup): async def test_set_on(hass, caplog): """Test set_on method.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_schedule.test" + entity_id = "input_timetable.test" test_cases = [ ( @@ -218,7 +218,7 @@ async def test_set_on(hass, caplog): async def test_set_off(hass, caplog): """Test set_off method.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_schedule.test" + entity_id = "input_timetable.test" test_cases = [ ( @@ -295,7 +295,7 @@ async def test_set_off(hass, caplog): async def test_state(hass, caplog): """Test state attribute.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_schedule.test" + entity_id = "input_timetable.test" assert hass.states.get(entity_id).state == STATE_OFF @@ -372,8 +372,8 @@ async def test_restore_state(hass): mock_restore_cache( hass, ( - State("input_schedule.a", "", {ATTR_ON_PERIODS: a_on_periods}), - State("input_schedule.b", "", {ATTR_ON_PERIODS: b_on_periods}), + State("input_timetable.a", "", {ATTR_ON_PERIODS: a_on_periods}), + State("input_timetable.b", "", {ATTR_ON_PERIODS: b_on_periods}), ), ) @@ -385,17 +385,17 @@ async def test_restore_state(hass): {DOMAIN: {"a": {}, "b": {}}}, ) - state = hass.states.get("input_schedule.a") + state = hass.states.get("input_timetable.a") assert state assert state.attributes[ATTR_ON_PERIODS] == a_on_periods - state = hass.states.get("input_schedule.b") + state = hass.states.get("input_timetable.b") assert state assert state.attributes[ATTR_ON_PERIODS] == b_on_periods async def test_input_scheudle_context(hass, hass_admin_user): - """Test that input_schedule context works.""" + """Test that input_timetable context works.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"x": {}}}) state = hass.states.get(f"{DOMAIN}.x") From 9706b60bd6e06df3273bb99df3098d6fb715dedb Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 7 Oct 2020 11:47:46 +0000 Subject: [PATCH 10/27] Fixed typos --- tests/components/input_timetable/__init__.py | 2 +- tests/components/input_timetable/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/input_timetable/__init__.py b/tests/components/input_timetable/__init__.py index 9e143367b3177c..f77a4d71049c5b 100644 --- a/tests/components/input_timetable/__init__.py +++ b/tests/components/input_timetable/__init__.py @@ -1 +1 @@ -"""Tests for the input_schedule component.""" +"""Tests for the input_timetable component.""" diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index bd96433c3c03e4..6a5fdfb80b85c7 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -1,4 +1,4 @@ -"""The tests for the Input timetable component.""" +"""The tests for the input timetable component.""" import datetime import pytest From 52938e61fe6efff10212080cf5b43015df016fdd Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 7 Oct 2020 18:42:22 +0000 Subject: [PATCH 11/27] Fixed translation --- homeassistant/components/input_timetable/strings.json | 10 +++++++++- .../components/input_timetable/translations/af.json | 3 --- .../components/input_timetable/translations/ar.json | 3 --- .../components/input_timetable/translations/bg.json | 3 --- .../components/input_timetable/translations/bs.json | 3 --- .../components/input_timetable/translations/ca.json | 3 --- .../components/input_timetable/translations/cs.json | 3 --- .../components/input_timetable/translations/cy.json | 3 --- .../components/input_timetable/translations/da.json | 3 --- .../components/input_timetable/translations/de.json | 3 --- .../components/input_timetable/translations/el.json | 3 --- .../components/input_timetable/translations/en.json | 6 ++++++ .../input_timetable/translations/es-419.json | 3 --- .../components/input_timetable/translations/es.json | 3 --- .../components/input_timetable/translations/et.json | 3 --- .../components/input_timetable/translations/eu.json | 3 --- .../components/input_timetable/translations/fi.json | 3 --- .../components/input_timetable/translations/fr.json | 3 --- .../components/input_timetable/translations/he.json | 3 --- .../components/input_timetable/translations/hi.json | 3 --- .../components/input_timetable/translations/hr.json | 3 --- .../components/input_timetable/translations/hu.json | 3 --- .../components/input_timetable/translations/hy.json | 3 --- .../components/input_timetable/translations/id.json | 3 --- .../components/input_timetable/translations/is.json | 3 --- .../components/input_timetable/translations/it.json | 3 --- .../components/input_timetable/translations/ko.json | 3 --- .../components/input_timetable/translations/lb.json | 3 --- .../components/input_timetable/translations/lv.json | 3 --- .../components/input_timetable/translations/nb.json | 3 --- .../components/input_timetable/translations/nl.json | 3 --- .../components/input_timetable/translations/nn.json | 3 --- .../components/input_timetable/translations/no.json | 3 --- .../components/input_timetable/translations/pl.json | 3 --- .../components/input_timetable/translations/pt-BR.json | 3 --- .../components/input_timetable/translations/pt.json | 3 --- .../components/input_timetable/translations/ro.json | 3 --- .../components/input_timetable/translations/ru.json | 3 --- .../components/input_timetable/translations/sk.json | 3 --- .../components/input_timetable/translations/sl.json | 3 --- .../components/input_timetable/translations/sv.json | 3 --- .../components/input_timetable/translations/te.json | 3 --- .../components/input_timetable/translations/th.json | 3 --- .../components/input_timetable/translations/tr.json | 3 --- .../components/input_timetable/translations/uk.json | 3 --- .../components/input_timetable/translations/vi.json | 3 --- .../input_timetable/translations/zh-Hans.json | 3 --- .../input_timetable/translations/zh-Hant.json | 3 --- 48 files changed, 15 insertions(+), 139 deletions(-) delete mode 100644 homeassistant/components/input_timetable/translations/af.json delete mode 100644 homeassistant/components/input_timetable/translations/ar.json delete mode 100644 homeassistant/components/input_timetable/translations/bg.json delete mode 100644 homeassistant/components/input_timetable/translations/bs.json delete mode 100644 homeassistant/components/input_timetable/translations/ca.json delete mode 100644 homeassistant/components/input_timetable/translations/cs.json delete mode 100644 homeassistant/components/input_timetable/translations/cy.json delete mode 100644 homeassistant/components/input_timetable/translations/da.json delete mode 100644 homeassistant/components/input_timetable/translations/de.json delete mode 100644 homeassistant/components/input_timetable/translations/el.json delete mode 100644 homeassistant/components/input_timetable/translations/es-419.json delete mode 100644 homeassistant/components/input_timetable/translations/es.json delete mode 100644 homeassistant/components/input_timetable/translations/et.json delete mode 100644 homeassistant/components/input_timetable/translations/eu.json delete mode 100644 homeassistant/components/input_timetable/translations/fi.json delete mode 100644 homeassistant/components/input_timetable/translations/fr.json delete mode 100644 homeassistant/components/input_timetable/translations/he.json delete mode 100644 homeassistant/components/input_timetable/translations/hi.json delete mode 100644 homeassistant/components/input_timetable/translations/hr.json delete mode 100644 homeassistant/components/input_timetable/translations/hu.json delete mode 100644 homeassistant/components/input_timetable/translations/hy.json delete mode 100644 homeassistant/components/input_timetable/translations/id.json delete mode 100644 homeassistant/components/input_timetable/translations/is.json delete mode 100644 homeassistant/components/input_timetable/translations/it.json delete mode 100644 homeassistant/components/input_timetable/translations/ko.json delete mode 100644 homeassistant/components/input_timetable/translations/lb.json delete mode 100644 homeassistant/components/input_timetable/translations/lv.json delete mode 100644 homeassistant/components/input_timetable/translations/nb.json delete mode 100644 homeassistant/components/input_timetable/translations/nl.json delete mode 100644 homeassistant/components/input_timetable/translations/nn.json delete mode 100644 homeassistant/components/input_timetable/translations/no.json delete mode 100644 homeassistant/components/input_timetable/translations/pl.json delete mode 100644 homeassistant/components/input_timetable/translations/pt-BR.json delete mode 100644 homeassistant/components/input_timetable/translations/pt.json delete mode 100644 homeassistant/components/input_timetable/translations/ro.json delete mode 100644 homeassistant/components/input_timetable/translations/ru.json delete mode 100644 homeassistant/components/input_timetable/translations/sk.json delete mode 100644 homeassistant/components/input_timetable/translations/sl.json delete mode 100644 homeassistant/components/input_timetable/translations/sv.json delete mode 100644 homeassistant/components/input_timetable/translations/te.json delete mode 100644 homeassistant/components/input_timetable/translations/th.json delete mode 100644 homeassistant/components/input_timetable/translations/tr.json delete mode 100644 homeassistant/components/input_timetable/translations/uk.json delete mode 100644 homeassistant/components/input_timetable/translations/vi.json delete mode 100644 homeassistant/components/input_timetable/translations/zh-Hans.json delete mode 100644 homeassistant/components/input_timetable/translations/zh-Hant.json diff --git a/homeassistant/components/input_timetable/strings.json b/homeassistant/components/input_timetable/strings.json index f2e36b09f93440..9eca15963ace7e 100644 --- a/homeassistant/components/input_timetable/strings.json +++ b/homeassistant/components/input_timetable/strings.json @@ -1 +1,9 @@ -{ "title": "Input timetable" } +{ + "title": "Input timetable", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/input_timetable/translations/af.json b/homeassistant/components/input_timetable/translations/af.json deleted file mode 100644 index 8b97397f314a22..00000000000000 --- a/homeassistant/components/input_timetable/translations/af.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Invoer ??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/ar.json b/homeassistant/components/input_timetable/translations/ar.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/ar.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/bg.json b/homeassistant/components/input_timetable/translations/bg.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/bg.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/bs.json b/homeassistant/components/input_timetable/translations/bs.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/bs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/ca.json b/homeassistant/components/input_timetable/translations/ca.json deleted file mode 100644 index d9a7595465e70e..00000000000000 --- a/homeassistant/components/input_timetable/translations/ca.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Entrada ??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/cs.json b/homeassistant/components/input_timetable/translations/cs.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/cs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/cy.json b/homeassistant/components/input_timetable/translations/cy.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/cy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/da.json b/homeassistant/components/input_timetable/translations/da.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/da.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/de.json b/homeassistant/components/input_timetable/translations/de.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/de.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/el.json b/homeassistant/components/input_timetable/translations/el.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/el.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json index b3691261ae1268..2084f04361942d 100644 --- a/homeassistant/components/input_timetable/translations/en.json +++ b/homeassistant/components/input_timetable/translations/en.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + }, "title": "Input timetable" } \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/es-419.json b/homeassistant/components/input_timetable/translations/es-419.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/es-419.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/es.json b/homeassistant/components/input_timetable/translations/es.json deleted file mode 100644 index 38dda914c15f90..00000000000000 --- a/homeassistant/components/input_timetable/translations/es.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Entrada de ??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/et.json b/homeassistant/components/input_timetable/translations/et.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/et.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/eu.json b/homeassistant/components/input_timetable/translations/eu.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/eu.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/fi.json b/homeassistant/components/input_timetable/translations/fi.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/fi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/fr.json b/homeassistant/components/input_timetable/translations/fr.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/fr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/he.json b/homeassistant/components/input_timetable/translations/he.json deleted file mode 100644 index 3038e91cdfc4ef..00000000000000 --- a/homeassistant/components/input_timetable/translations/he.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u05E7\u05DC\u05D8%20\u05DC\u05D5\u05D7%20\u05D6\u05DE\u05E0\u05D9\u05DD" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/hi.json b/homeassistant/components/input_timetable/translations/hi.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/hi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/hr.json b/homeassistant/components/input_timetable/translations/hr.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/hr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/hu.json b/homeassistant/components/input_timetable/translations/hu.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/hu.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/hy.json b/homeassistant/components/input_timetable/translations/hy.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/hy.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/id.json b/homeassistant/components/input_timetable/translations/id.json deleted file mode 100644 index e18c2e811810f9..00000000000000 --- a/homeassistant/components/input_timetable/translations/id.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Input ??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/is.json b/homeassistant/components/input_timetable/translations/is.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/is.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/it.json b/homeassistant/components/input_timetable/translations/it.json deleted file mode 100644 index e18c2e811810f9..00000000000000 --- a/homeassistant/components/input_timetable/translations/it.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Input ??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/ko.json b/homeassistant/components/input_timetable/translations/ko.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/ko.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/lb.json b/homeassistant/components/input_timetable/translations/lb.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/lb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/lv.json b/homeassistant/components/input_timetable/translations/lv.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/lv.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/nb.json b/homeassistant/components/input_timetable/translations/nb.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/nb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/nl.json b/homeassistant/components/input_timetable/translations/nl.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/nl.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/nn.json b/homeassistant/components/input_timetable/translations/nn.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/nn.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/no.json b/homeassistant/components/input_timetable/translations/no.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/no.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/pl.json b/homeassistant/components/input_timetable/translations/pl.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/pl.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/pt-BR.json b/homeassistant/components/input_timetable/translations/pt-BR.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/pt-BR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/pt.json b/homeassistant/components/input_timetable/translations/pt.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/pt.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/ro.json b/homeassistant/components/input_timetable/translations/ro.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/ro.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/ru.json b/homeassistant/components/input_timetable/translations/ru.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/ru.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/sk.json b/homeassistant/components/input_timetable/translations/sk.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/sk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/sl.json b/homeassistant/components/input_timetable/translations/sl.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/sl.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/sv.json b/homeassistant/components/input_timetable/translations/sv.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/sv.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/te.json b/homeassistant/components/input_timetable/translations/te.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/te.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/th.json b/homeassistant/components/input_timetable/translations/th.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/th.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/tr.json b/homeassistant/components/input_timetable/translations/tr.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/tr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/uk.json b/homeassistant/components/input_timetable/translations/uk.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/uk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/vi.json b/homeassistant/components/input_timetable/translations/vi.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/vi.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/zh-Hans.json b/homeassistant/components/input_timetable/translations/zh-Hans.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/zh-Hans.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file diff --git a/homeassistant/components/input_timetable/translations/zh-Hant.json b/homeassistant/components/input_timetable/translations/zh-Hant.json deleted file mode 100644 index 7adbc0bc656be1..00000000000000 --- a/homeassistant/components/input_timetable/translations/zh-Hant.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "??" -} \ No newline at end of file From 117c3d035b4b2920804bff1478e0f76a9b16e733 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Thu, 8 Oct 2020 18:52:16 +0000 Subject: [PATCH 12/27] Fixed "reload" --- homeassistant/components/input_timetable/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py index 6afab94d08d176..6973e2a07bfbf8 100644 --- a/homeassistant/components/input_timetable/__init__.py +++ b/homeassistant/components/input_timetable/__init__.py @@ -113,7 +113,10 @@ async def reload_service_handler(service_call: ServiceCallType) -> None: if conf is None: conf = {DOMAIN: {}} await yaml_collection.async_load( - [{CONF_ID: id_, **conf} for id_, conf in conf.get(DOMAIN, {}).items()] + [ + {CONF_ID: id_, **(conf or {})} + for id_, conf in conf.get(DOMAIN, {}).items() + ] ) component.async_register_entity_service( From e7f8ff291fb357d086d6e964a91ceaf499cc6fa7 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Sat, 10 Oct 2020 22:10:35 +0000 Subject: [PATCH 13/27] Move to events model --- .../components/input_timetable/__init__.py | 217 ++++++------ .../components/input_timetable/services.yaml | 29 +- .../input_timetable/translations/en.json | 4 +- tests/components/input_timetable/test_init.py | 310 ++++++++---------- 4 files changed, 251 insertions(+), 309 deletions(-) diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py index 6973e2a07bfbf8..5ce93f2d581078 100644 --- a/homeassistant/components/input_timetable/__init__.py +++ b/homeassistant/components/input_timetable/__init__.py @@ -1,5 +1,6 @@ """Support to set a timetable (on and off times during the day).""" import datetime +import enum import logging import typing @@ -8,6 +9,7 @@ import homeassistant from homeassistant.const import ( ATTR_EDITABLE, + ATTR_STATE, CONF_ICON, CONF_ID, CONF_NAME, @@ -16,7 +18,7 @@ STATE_ON, ) from homeassistant.core import callback -from homeassistant.helpers import collection, event +from homeassistant.helpers import collection, event as event_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -27,14 +29,22 @@ DOMAIN = "input_timetable" -ATTR_START = "start" -ATTR_END = "end" -ATTR_ON_PERIODS = "on_periods" +ATTR_TIME = "time" +ATTR_TIMETABLE = "timetable" -SERVICE_SET_ON = "set_on" -SERVICE_SET_OFF = "set_off" +SERVICE_SET = "set" +SERVICE_UNSET = "unset" SERVICE_RESET = "reset" +MIDNIGHT = datetime.time.fromisoformat("00:00:00") + + +class StateType(enum.Enum): + """Possible options for states.""" + + on = STATE_ON + off = STATE_OFF + CONFIG_SCHEMA = vol.Schema( { @@ -53,11 +63,21 @@ SERVICE_SET_SCHEMA = vol.Schema( { - vol.Required(ATTR_START): cv.time, - vol.Required(ATTR_END): cv.time, + vol.Required(ATTR_TIME): cv.time, + vol.Required(ATTR_STATE): cv.enum(StateType), + }, + extra=vol.ALLOW_EXTRA, +) +SERVICE_UNSET_SCHEMA = vol.Schema( + { + vol.Required(ATTR_TIME): cv.time, }, extra=vol.ALLOW_EXTRA, ) +SERVICE_RESET_SCHEMA = vol.Schema( + {}, + extra=vol.ALLOW_EXTRA, +) RELOAD_SERVICE_SCHEMA = vol.Schema({}) STORAGE_KEY = DOMAIN @@ -107,6 +127,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + component.async_register_entity_service( + SERVICE_SET, SERVICE_SET_SCHEMA, "async_set" + ) + component.async_register_entity_service( + SERVICE_UNSET, SERVICE_UNSET_SCHEMA, "async_unset" + ) + component.async_register_entity_service( + SERVICE_RESET, SERVICE_RESET_SCHEMA, "async_reset" + ) + async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" conf = await component.async_prepare_reload(skip_reset=True) @@ -119,14 +149,6 @@ async def reload_service_handler(service_call: ServiceCallType) -> None: ] ) - component.async_register_entity_service( - SERVICE_SET_ON, SERVICE_SET_SCHEMA, "async_set_on" - ) - component.async_register_entity_service( - SERVICE_SET_OFF, SERVICE_SET_SCHEMA, "async_set_off" - ) - component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") - homeassistant.helpers.service.async_register_admin_service( hass, DOMAIN, @@ -165,7 +187,7 @@ class InputTimeTable(RestoreEntity): def __init__(self, config: typing.Dict): """Initialize an input timetable.""" self._config = config - self._on_periods = [] + self._timetable = [] self.editable = True self._event_unsub = None @@ -194,19 +216,23 @@ def icon(self): @property def state(self): - """Return 'on' when we are in an on period.""" + """Return the state based on the timetable events.""" + if not self._timetable: + return STATE_OFF now = datetime.datetime.now().time() - for period in self._on_periods: - if _is_in_period(now, period): - return STATE_ON - return STATE_OFF + prev = StateEvent(MIDNIGHT, self._timetable[-1].state) + for event in self._timetable: + if prev.time <= now < event.time: + break + prev = event + return prev.state.value @property def state_attributes(self): """Return the state attributes.""" return { ATTR_EDITABLE: self.editable, - ATTR_ON_PERIODS: self._on_periods_to_attribute(), + ATTR_TIMETABLE: self._timetable_to_attribute(), } @property @@ -218,85 +244,55 @@ async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() state = await self.async_get_last_state() - if state and state.attributes.get(ATTR_ON_PERIODS): - self._on_periods_from_attribute(state.attributes[ATTR_ON_PERIODS]) + if state and state.attributes.get(ATTR_TIMETABLE): + self._timetable_from_attribute(state.attributes[ATTR_TIMETABLE]) self._update_state() - def _on_periods_to_attribute(self): + def _timetable_to_attribute(self): return [ - {ATTR_START: period.start.isoformat(), ATTR_END: period.end.isoformat()} - for period in self._on_periods + {ATTR_TIME: event.time.isoformat(), ATTR_STATE: event.state.value} + for event in self._timetable ] - def _on_periods_from_attribute(self, periods): - self._on_periods = [ - Period(_read_time(period[ATTR_START]), _read_time(period[ATTR_END])) - for period in periods + def _timetable_from_attribute(self, value): + self._timetable = [ + StateEvent( + datetime.time.fromisoformat(item[ATTR_TIME]).replace( + microsecond=0, tzinfo=None + ), + StateType(item[ATTR_STATE]), + ) + for item in value ] + self._sort_timetable() - async def async_set_on(self, start, end): - """Add on period.""" - start = start.replace(microsecond=0, tzinfo=None) - end = end.replace(microsecond=0, tzinfo=None) - if end <= start: - raise vol.Invalid("Start time must be earlier than end time") - - # Merge overlapping and adjusting periods. - for period in self._on_periods: - if _is_in_period(start, period) or start == period.end: - start = period.start - if _is_in_period(end, period) or end == period.start: - end = period.end + def _sort_timetable(self): + self._timetable.sort(key=lambda event: event.time) - on_periods = [Period(start, end)] - - # Copy non overlapping periods. - for period in self._on_periods: - if period.end <= start or period.start >= end: - on_periods.append(period) - - on_periods.sort(key=lambda period: period.start) - - self._on_periods = on_periods + async def async_set(self, time, state): + """Add an state change event to the timetable.""" + time = time.replace(microsecond=0, tzinfo=None) + for event in self._timetable: + if event.time == time: + event.state = state + break + else: + self._timetable.append(StateEvent(time, state)) + self._sort_timetable() self._update_state() - async def async_set_off(self, start, end): - """Add off period (subtracting the on periods).""" - start = start.replace(microsecond=0, tzinfo=None) - end = end.replace(microsecond=0, tzinfo=None) - if end <= start: - raise vol.Invalid("Start time must be earlier than end time") - - on_periods = [] - - # Trim and split periods. - for period in self._on_periods: - if _is_in_period(start, period): - self._on_periods.remove(period) - if start > period.start: - on_periods.append(Period(period.start, start)) - if _is_in_period(end, period): - on_periods.append(Period(end, period.end)) + async def async_unset(self, time): + """Remove a state change event.""" + time = time.replace(microsecond=0, tzinfo=None) + for event in self._timetable: + if event.time == time: + self._timetable.remove(event) break - for period in self._on_periods: - if _is_in_period(end, period): - self._on_periods.remove(period) - on_periods.append(Period(period.start, end)) - break - - # Copy non overlapping periods. - for period in self._on_periods: - if period.end <= start or period.start >= end: - on_periods.append(period) - - on_periods.sort(key=lambda period: period.start) - - self._on_periods = on_periods self._update_state() async def async_reset(self): - """Delete all on periods.""" - self._on_periods = [] + """Remove all state changes.""" + self._timetable.clear() self._update_state() async def async_update_config(self, config: typing.Dict) -> None: @@ -309,27 +305,28 @@ def _schedule_update(self): self._event_unsub() self._event_unsub = None - if not self._on_periods: + if not self._timetable or len(self._timetable) == 1: return now = datetime.datetime.now() time = now.time() - midnight = datetime.time.fromisoformat("00:00:00") - prev_end = midnight - for period in self._on_periods: - if _is_in_period(time, period): - next_change = datetime.datetime.combine(now.date(), period.end) - break - if prev_end <= time < period.start: - next_change = datetime.datetime.combine(now.date(), period.start) + today = now.date() + prev = MIDNIGHT + for event in self._timetable: + if prev <= time < event.time: + next_change = datetime.datetime.combine( + today, + event.time, + ) break - prev_end = period.end + prev = event.time else: next_change = datetime.datetime.combine( - datetime.date.today() + datetime.timedelta(days=1), midnight + today + datetime.timedelta(days=1), + self._timetable[0].time, ) - self._event_unsub = event.async_track_point_in_time( + self._event_unsub = event_helper.async_track_point_in_time( self.hass, self._update_state, next_change ) @@ -340,18 +337,10 @@ def _update_state(self, now=None): self.async_write_ha_state() -class Period: - """Simple time range.""" - - def __init__(self, start, end): - """Initialize the range.""" - self.start = start - self.end = end - - -def _read_time(value): - return datetime.time.fromisoformat(value).replace(microsecond=0, tzinfo=None) - +class StateEvent: + """State event properties (time, and state value).""" -def _is_in_period(value, period): - return period.start <= value < period.end + def __init__(self, time, state): + """Initialize the object.""" + self.time = time + self.state = state diff --git a/homeassistant/components/input_timetable/services.yaml b/homeassistant/components/input_timetable/services.yaml index ae7bdc28433886..cc6d8431b9cfb6 100644 --- a/homeassistant/components/input_timetable/services.yaml +++ b/homeassistant/components/input_timetable/services.yaml @@ -1,29 +1,26 @@ -set_on: - description: Adds a time range when state will be on. +set: + description: Adds a state change at the specified time. fields: entity_id: description: Entity id of the input timetable that should be set. example: input_timetable.lights - start: - description: The start time of the time range. + time: + description: The time of the change. example: '"05:04:20"' - end: - description: The end time of the time range. - example: '"06:24:50"' -set_off: - description: Adds a time range when state will be off. + state: + description: The new state (on or off). + example: "on" +unset: + description: Removes a state change. fields: entity_id: - description: Entity id of the input timetable that should be set. + description: Entity id of the input timetable that should be reset. example: input_timetable.lights - start: - description: The start time of the time range. + time: + description: The time of the state change to remove. example: '"05:04:20"' - end: - description: The end time of the time range. - example: '"06:24:50"' reset: - description: Resets the time causing it to be always off. + description: Removes all state changes. fields: entity_id: description: Entity id of the input timetable that should be reset. diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json index 2084f04361942d..5bc5158a035941 100644 --- a/homeassistant/components/input_timetable/translations/en.json +++ b/homeassistant/components/input_timetable/translations/en.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "[%key:common::state::off%]", - "on": "[%key:common::state::on%]" + "off": "off", + "on": "on" } }, "title": "Input timetable" diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index 6a5fdfb80b85c7..7a32f6abc6ab0a 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -4,14 +4,14 @@ import pytest from homeassistant.components.input_timetable import ( - ATTR_END, - ATTR_ON_PERIODS, - ATTR_START, + ATTR_STATE, + ATTR_TIME, + ATTR_TIMETABLE, DOMAIN, SERVICE_RELOAD, SERVICE_RESET, - SERVICE_SET_OFF, - SERVICE_SET_ON, + SERVICE_SET, + SERVICE_UNSET, ) from homeassistant.const import ( ATTR_EDITABLE, @@ -62,28 +62,28 @@ async def _storage(items=None, config=None): return _storage -async def set_on(hass, entity_id, start, end): - """Add an on period.""" +async def call_set(hass, entity_id, time, state): + """Add a state change event.""" await hass.services.async_call( DOMAIN, - SERVICE_SET_ON, - {ATTR_ENTITY_ID: entity_id, ATTR_START: start, ATTR_END: end}, + SERVICE_SET, + {ATTR_ENTITY_ID: entity_id, ATTR_TIME: time, ATTR_STATE: state}, blocking=True, ) -async def set_off(hass, entity_id, start, end): - """Add an off period.""" +async def call_unset(hass, entity_id, time): + """Remove a state change.""" await hass.services.async_call( DOMAIN, - SERVICE_SET_OFF, - {ATTR_ENTITY_ID: entity_id, ATTR_START: start, ATTR_END: end}, + SERVICE_UNSET, + {ATTR_ENTITY_ID: entity_id, ATTR_TIME: time}, blocking=True, ) -async def reset(hass, entity_id): - """Remove all on periods.""" +async def call_reset(hass, entity_id): + """Remove all state changes.""" await hass.services.async_call( DOMAIN, SERVICE_RESET, @@ -113,183 +113,121 @@ async def test_editable_state_attribute(hass, storage_setup): assert not state.attributes[ATTR_EDITABLE] -async def test_set_on(hass, caplog): - """Test set_on method.""" +async def test_set(hass, caplog): + """Test set method.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) entity_id = "input_timetable.test" test_cases = [ ( - "simple", - [("01:02:03", "04:05:06")], + "single", + [("01:02:03", STATE_ON)], [ { - ATTR_START: "01:02:03", - ATTR_END: "04:05:06", + ATTR_TIME: "01:02:03", + ATTR_STATE: STATE_ON, }, ], ), ( "multiple", - (("10:00:00", "11:00:00"), ("02:00:00", "03:00:00")), + [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], [ { - ATTR_START: "02:00:00", - ATTR_END: "03:00:00", + ATTR_TIME: "01:02:03", + ATTR_STATE: STATE_OFF, }, { - ATTR_START: "10:00:00", - ATTR_END: "11:00:00", + ATTR_TIME: "04:05:06", + ATTR_STATE: STATE_ON, }, ], ), ( - "overlapping", - (("01:00:00", "03:00:00"), ("02:00:00", "04:00:00")), + "override", + [("01:02:03", STATE_ON), ("01:02:03", STATE_OFF)], [ { - ATTR_START: "01:00:00", - ATTR_END: "04:00:00", - }, - ], - ), - ( - "adjusted", - (("01:00:00", "02:00:00"), ("02:00:00", "03:00:00")), - [ - { - ATTR_START: "01:00:00", - ATTR_END: "03:00:00", - }, - ], - ), - ( - "subset", - (("01:00:00", "04:00:00"), ("02:00:00", "03:00:00")), - [ - { - ATTR_START: "01:00:00", - ATTR_END: "04:00:00", - }, - ], - ), - ( - "superset", - (("02:00:00", "03:00:00"), ("01:00:00", "04:00:00")), - [ - { - ATTR_START: "01:00:00", - ATTR_END: "04:00:00", - }, - ], - ), - ( - "merge", - ( - ("01:00:00", "02:00:00"), - ("03:00:00", "04:00:00"), - ("02:00:00", "03:00:00"), - ), - [ - { - ATTR_START: "01:00:00", - ATTR_END: "04:00:00", + ATTR_TIME: "01:02:03", + ATTR_STATE: STATE_OFF, }, ], ), ] for test_case in test_cases: - await reset(hass, entity_id) - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - for period in test_case[1]: - start = datetime.time.fromisoformat(period[0]) - end = datetime.time.fromisoformat(period[1]) - await set_on(hass, entity_id, start, end) - + await call_reset(hass, entity_id) + for event in test_case[1]: + time = datetime.time.fromisoformat(event[0]) + state = event[1] + await call_set(hass, entity_id, time, state) state = hass.states.get(entity_id) assert ( - state.attributes[ATTR_ON_PERIODS] == test_case[2] - ), f"'{test_case[0]}' test case failed: state is '{state.attributes[ATTR_ON_PERIODS]}' but expecting '{test_case[2]}''" + state.attributes[ATTR_TIMETABLE] == test_case[2] + ), f"'{test_case[0]}' test case failed: timetable is '{state.attributes[ATTR_TIMETABLE]}' but expecting '{test_case[2]}''" -async def test_set_off(hass, caplog): - """Test set_off method.""" +async def test_unset(hass, caplog): + """Test unset method.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) entity_id = "input_timetable.test" test_cases = [ ( - "simple", - [("01:02:03", "04:05:06")], - ("02:03:04", "04:05:06"), - [ - { - ATTR_START: "01:02:03", - ATTR_END: "02:03:04", - }, - ], - ), - ( - "entire", - [("01:02:03", "04:05:06")], - ("01:02:03", "04:05:06"), - [], - ), - ( - "superset", - [("01:00:00", "05:00:00")], - ("00:00:00", "10:00:00"), + "single", + [("01:02:03", STATE_ON)], + "01:02:03", [], ), ( - "subset", - [("01:00:00", "05:00:00")], - ("02:00:00", "04:00:00"), + "multiple", + [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], + "04:05:06", [ { - ATTR_START: "01:00:00", - ATTR_END: "02:00:00", - }, - { - ATTR_START: "04:00:00", - ATTR_END: "05:00:00", + ATTR_TIME: "01:02:03", + ATTR_STATE: STATE_OFF, }, ], ), ( - "adjusted", - [("01:00:00", "02:00:00")], - ("02:00:00", "03:00:00"), + "none", + [("01:02:03", STATE_ON)], + "04:05:06", [ { - ATTR_START: "01:00:00", - ATTR_END: "02:00:00", + ATTR_TIME: "01:02:03", + ATTR_STATE: STATE_ON, }, ], ), ] for test_case in test_cases: - await reset(hass, entity_id) + await call_reset(hass, entity_id) + for event in test_case[1]: + time = datetime.time.fromisoformat(event[0]) + state = event[1] + await call_set(hass, entity_id, time, state) + time = datetime.time.fromisoformat(test_case[2]) + await call_unset(hass, entity_id, time) state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - for on_period in test_case[1]: - start = datetime.time.fromisoformat(on_period[0]) - end = datetime.time.fromisoformat(on_period[1]) - await set_on(hass, entity_id, start, end) + assert ( + state.attributes[ATTR_TIMETABLE] == test_case[3] + ), f"'{test_case[0]}' test case failed: timetable is '{state.attributes[ATTR_TIMETABLE]}' but expecting '{test_case[3]}''" - start = datetime.time.fromisoformat(test_case[2][0]) - end = datetime.time.fromisoformat(test_case[2][1]) - await set_off(hass, entity_id, start, end) - state = hass.states.get(entity_id) - assert ( - state.attributes[ATTR_ON_PERIODS] == test_case[3] - ), f"'{test_case[0]}' test case failed: state is '{state.attributes[ATTR_ON_PERIODS]}' but expecting '{test_case[3]}''" +async def test_reset(hass, caplog): + """Test reset method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_timetable.test" + await call_set(hass, entity_id, "01:02:03", STATE_ON) + await call_set(hass, entity_id, "04:05:06", STATE_OFF) + await call_set(hass, entity_id, "07:08:09", STATE_ON) + await call_set(hass, entity_id, "10:11:12", STATE_OFF) + assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 4 + await call_reset(hass, entity_id) + assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 0 async def test_state(hass, caplog): @@ -299,18 +237,34 @@ async def test_state(hass, caplog): assert hass.states.get(entity_id).state == STATE_OFF - now = datetime.datetime.now() - in_2_minutes = now + datetime.timedelta(minutes=2) + now = datetime.datetime.now().replace(microsecond=0) + in_5_minutes = now + datetime.timedelta(minutes=5) + in_10_minutes = now + datetime.timedelta(minutes=10) + previous_10_minutes = now + datetime.timedelta(minutes=-10) + previous_5_minutes = now + datetime.timedelta(minutes=-5) - start = now.time() - end = in_2_minutes.time() + await call_set(hass, entity_id, previous_5_minutes.time().isoformat(), STATE_ON) + assert hass.states.get(entity_id).state == STATE_ON + await call_reset(hass, entity_id) + + await call_set(hass, entity_id, in_5_minutes.time().isoformat(), STATE_ON) + assert hass.states.get(entity_id).state == STATE_ON + await call_reset(hass, entity_id) - if end < start: - # The rare case of day overlap - skip the test - return + await call_set(hass, entity_id, previous_10_minutes.time().isoformat(), STATE_ON) + await call_set(hass, entity_id, previous_5_minutes.time().isoformat(), STATE_OFF) + assert hass.states.get(entity_id).state == STATE_OFF + await call_reset(hass, entity_id) - await set_on(hass, entity_id, start, end) + await call_set(hass, entity_id, in_5_minutes.time().isoformat(), STATE_ON) + await call_set(hass, entity_id, in_10_minutes.time().isoformat(), STATE_OFF) + assert hass.states.get(entity_id).state == STATE_OFF + await call_reset(hass, entity_id) + + await call_set(hass, entity_id, previous_5_minutes.time().isoformat(), STATE_ON) + await call_set(hass, entity_id, in_5_minutes.time().isoformat(), STATE_OFF) assert hass.states.get(entity_id).state == STATE_ON + await call_reset(hass, entity_id) async def test_state_update(hass, caplog): @@ -321,59 +275,61 @@ async def test_state_update(hass, caplog): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) entity_id = f"{DOMAIN}.test" - # No update if there are no on periods. - assert async_track_point_in_time.call_count == 0 - now = datetime.datetime.now().replace(microsecond=0) in_5_minutes = now + datetime.timedelta(minutes=5) in_10_minutes = now + datetime.timedelta(minutes=10) previous_5_minutes = now + datetime.timedelta(minutes=-5) - next_midnight = datetime.datetime.combine( - now.date() + datetime.timedelta(days=1), - datetime.time.fromisoformat("00:00:00"), - ) + previous_10_minutes = now + datetime.timedelta(minutes=-10) - if in_10_minutes.time() < previous_5_minutes.time(): - # The rare case of day overlap - skip the test - return + # No events => no updates. + assert async_track_point_in_time.call_count == 0 - # State is on => update is at the end of the range. - await set_on(hass, entity_id, now.time(), in_5_minutes.time()) + # One event => no updates. + await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) + assert async_track_point_in_time.call_count == 0 + await call_reset(hass, entity_id) + + # Between 2 events. + await call_set(hass, entity_id, previous_5_minutes.time(), STATE_ON) + await call_set(hass, entity_id, in_5_minutes.time(), STATE_OFF) next_update = async_track_point_in_time.call_args[0][2] assert next_update == in_5_minutes + await call_reset(hass, entity_id) - # State if off => update is at the beginning of the next range. - await reset(hass, entity_id) - await set_on(hass, entity_id, in_5_minutes.time(), in_10_minutes.time()) + # After any event. + await call_set(hass, entity_id, previous_10_minutes.time(), STATE_ON) + await call_set(hass, entity_id, previous_5_minutes.time(), STATE_OFF) next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes + assert next_update == previous_10_minutes + datetime.timedelta(days=1) + await call_reset(hass, entity_id) - # State is off, and range is eariler in the day => update in midnight. - await reset(hass, entity_id) - await set_on(hass, entity_id, previous_5_minutes.time(), now.time()) + # Before any event. + await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) + await call_set(hass, entity_id, in_10_minutes.time(), STATE_OFF) next_update = async_track_point_in_time.call_args[0][2] - assert next_update == next_midnight + assert next_update == in_5_minutes + await call_reset(hass, entity_id) async def test_restore_state(hass): """Ensure states are restored on startup.""" - a_on_periods = [ + a_timetable = [ { - ATTR_START: "01:02:03", - ATTR_END: "02:03:04", + ATTR_TIME: "01:02:03", + ATTR_STATE: STATE_ON, }, ] - b_on_periods = [ + b_timetable = [ { - ATTR_START: "07:08:09", - ATTR_END: "10:11:12", + ATTR_TIME: "07:08:09", + ATTR_STATE: STATE_OFF, }, ] mock_restore_cache( hass, ( - State("input_timetable.a", "", {ATTR_ON_PERIODS: a_on_periods}), - State("input_timetable.b", "", {ATTR_ON_PERIODS: b_on_periods}), + State("input_timetable.a", "", {ATTR_TIMETABLE: a_timetable}), + State("input_timetable.b", "", {ATTR_TIMETABLE: b_timetable}), ), ) @@ -387,11 +343,11 @@ async def test_restore_state(hass): state = hass.states.get("input_timetable.a") assert state - assert state.attributes[ATTR_ON_PERIODS] == a_on_periods + assert state.attributes[ATTR_TIMETABLE] == a_timetable state = hass.states.get("input_timetable.b") assert state - assert state.attributes[ATTR_ON_PERIODS] == b_on_periods + assert state.attributes[ATTR_TIMETABLE] == b_timetable async def test_input_scheudle_context(hass, hass_admin_user): @@ -403,11 +359,11 @@ async def test_input_scheudle_context(hass, hass_admin_user): await hass.services.async_call( DOMAIN, - SERVICE_SET_ON, + SERVICE_SET, { ATTR_ENTITY_ID: state.entity_id, - ATTR_START: datetime.time.fromisoformat("01:02:03"), - ATTR_END: datetime.time.fromisoformat("04:05:06"), + ATTR_TIME: datetime.time.fromisoformat("01:02:03"), + ATTR_STATE: STATE_ON, }, True, Context(user_id=hass_admin_user.id), @@ -415,7 +371,7 @@ async def test_input_scheudle_context(hass, hass_admin_user): state2 = hass.states.get(f"{DOMAIN}.x") assert state2 is not None - assert state.attributes[ATTR_ON_PERIODS] != state2.attributes[ATTR_ON_PERIODS] + assert state.attributes[ATTR_TIMETABLE] != state2.attributes[ATTR_TIMETABLE] assert state2.context.user_id == hass_admin_user.id From f806eb8f2a62bd5910d65da8b6741511a1158b92 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 14 Oct 2020 18:04:43 +0000 Subject: [PATCH 14/27] Use capital letters for the states --- homeassistant/components/input_timetable/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json index 5bc5158a035941..6e37a00a85ea24 100644 --- a/homeassistant/components/input_timetable/translations/en.json +++ b/homeassistant/components/input_timetable/translations/en.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "off", - "on": "on" + "off": "Off", + "on": "On" } }, "title": "Input timetable" From 45a20998e1dff33a48f0313e365c5b46d6454c00 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Wed, 14 Oct 2020 18:04:43 +0000 Subject: [PATCH 15/27] Re-trigger checks --- homeassistant/components/input_timetable/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json index 5bc5158a035941..6e37a00a85ea24 100644 --- a/homeassistant/components/input_timetable/translations/en.json +++ b/homeassistant/components/input_timetable/translations/en.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "off", - "on": "on" + "off": "Off", + "on": "On" } }, "title": "Input timetable" From 42b86a766c3dd37dc254e5e05a6412ff80a63c3c Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Fri, 16 Oct 2020 06:51:29 +0000 Subject: [PATCH 16/27] Add "reconfig" service --- .../components/input_timetable/__init__.py | 30 ++++++++++++++--- tests/components/input_timetable/test_init.py | 33 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py index 5ce93f2d581078..fdf9103479409c 100644 --- a/homeassistant/components/input_timetable/__init__.py +++ b/homeassistant/components/input_timetable/__init__.py @@ -31,10 +31,12 @@ ATTR_TIME = "time" ATTR_TIMETABLE = "timetable" +ATTR_CONFIG = "config" SERVICE_SET = "set" SERVICE_UNSET = "unset" SERVICE_RESET = "reset" +SERVICE_RECONFIG = "reconfig" MIDNIGHT = datetime.time.fromisoformat("00:00:00") @@ -78,6 +80,12 @@ class StateType(enum.Enum): {}, extra=vol.ALLOW_EXTRA, ) +SERVICE_RECONFIG_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), + }, + extra=vol.ALLOW_EXTRA, +) RELOAD_SERVICE_SCHEMA = vol.Schema({}) STORAGE_KEY = DOMAIN @@ -136,6 +144,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_RESET, SERVICE_RESET_SCHEMA, "async_reset" ) + component.async_register_entity_service( + SERVICE_RECONFIG, SERVICE_RECONFIG_SCHEMA, "async_reconfig" + ) async def reload_service_handler(service_call: ServiceCallType) -> None: """Reload yaml entities.""" @@ -254,15 +265,15 @@ def _timetable_to_attribute(self): for event in self._timetable ] - def _timetable_from_attribute(self, value): + def _timetable_from_attribute(self, timetable): self._timetable = [ StateEvent( - datetime.time.fromisoformat(item[ATTR_TIME]).replace( + datetime.time.fromisoformat(event[ATTR_TIME]).replace( microsecond=0, tzinfo=None ), - StateType(item[ATTR_STATE]), + StateType(event[ATTR_STATE]), ) - for item in value + for event in timetable ] self._sort_timetable() @@ -295,6 +306,17 @@ async def async_reset(self): self._timetable.clear() self._update_state() + async def async_reconfig(self, config): + """Override the timetable with the new list.""" + self._timetable = [ + StateEvent( + event[ATTR_TIME].replace(microsecond=0, tzinfo=None), event[ATTR_STATE] + ) + for event in config + ] + self._sort_timetable() + self._update_state() + async def async_update_config(self, config: typing.Dict) -> None: """Handle when the config is updated.""" self._config = config diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index 7a32f6abc6ab0a..93863810cdbcfb 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -4,10 +4,12 @@ import pytest from homeassistant.components.input_timetable import ( + ATTR_CONFIG, ATTR_STATE, ATTR_TIME, ATTR_TIMETABLE, DOMAIN, + SERVICE_RECONFIG, SERVICE_RELOAD, SERVICE_RESET, SERVICE_SET, @@ -92,6 +94,16 @@ async def call_reset(hass, entity_id): ) +async def call_reconfig(hass, entity_id, config): + """Override the timetable with the new list.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RECONFIG, + {ATTR_ENTITY_ID: entity_id, ATTR_CONFIG: config}, + blocking=True, + ) + + async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() @@ -230,6 +242,27 @@ async def test_reset(hass, caplog): assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 0 +async def test_reconfig(hass, caplog): + """Test reconfig method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_timetable.test" + await call_set(hass, entity_id, "01:02:03", STATE_ON) + assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 1 + await call_reconfig( + hass, + entity_id, + [ + {ATTR_TIME: "07:08:09", ATTR_STATE: STATE_OFF}, + {ATTR_TIME: "04:05:06", ATTR_STATE: STATE_ON}, + ], + ) + assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 2 + assert ( + hass.states.get(entity_id).attributes[ATTR_TIMETABLE][0][ATTR_TIME] + == "04:05:06" + ) + + async def test_state(hass, caplog): """Test state attribute.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) From ac3e77c7bf9c5b90b6abe2c7e5007ab90cd712f9 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Fri, 16 Oct 2020 07:59:12 +0000 Subject: [PATCH 17/27] Add "reconfig" description --- homeassistant/components/input_timetable/__init__.py | 7 +++---- homeassistant/components/input_timetable/services.yaml | 10 ++++++++++ tests/components/input_timetable/test_init.py | 3 +-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py index fdf9103479409c..538091bc1f68fe 100644 --- a/homeassistant/components/input_timetable/__init__.py +++ b/homeassistant/components/input_timetable/__init__.py @@ -31,7 +31,6 @@ ATTR_TIME = "time" ATTR_TIMETABLE = "timetable" -ATTR_CONFIG = "config" SERVICE_SET = "set" SERVICE_UNSET = "unset" @@ -82,7 +81,7 @@ class StateType(enum.Enum): ) SERVICE_RECONFIG_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), + vol.Required(ATTR_TIMETABLE): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), }, extra=vol.ALLOW_EXTRA, ) @@ -306,13 +305,13 @@ async def async_reset(self): self._timetable.clear() self._update_state() - async def async_reconfig(self, config): + async def async_reconfig(self, timetable): """Override the timetable with the new list.""" self._timetable = [ StateEvent( event[ATTR_TIME].replace(microsecond=0, tzinfo=None), event[ATTR_STATE] ) - for event in config + for event in timetable ] self._sort_timetable() self._update_state() diff --git a/homeassistant/components/input_timetable/services.yaml b/homeassistant/components/input_timetable/services.yaml index cc6d8431b9cfb6..c2204e274b91d2 100644 --- a/homeassistant/components/input_timetable/services.yaml +++ b/homeassistant/components/input_timetable/services.yaml @@ -25,5 +25,15 @@ reset: entity_id: description: Entity id of the input timetable that should be reset. example: input_timetable.lights +reconfig: + description: Overrides the timetable with the provided list. + fields: + entity_id: + description: Entity id of the input timetable that should be set. + example: input_timetable.lights + timetable: + description: List of state changes. + example: + [{ time: "01:02:03", state: "on" }, { time: "04:05:06", state: "off" }] reload: description: Reload the input_timetable configuration. diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index 93863810cdbcfb..fb757671825abb 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -4,7 +4,6 @@ import pytest from homeassistant.components.input_timetable import ( - ATTR_CONFIG, ATTR_STATE, ATTR_TIME, ATTR_TIMETABLE, @@ -99,7 +98,7 @@ async def call_reconfig(hass, entity_id, config): await hass.services.async_call( DOMAIN, SERVICE_RECONFIG, - {ATTR_ENTITY_ID: entity_id, ATTR_CONFIG: config}, + {ATTR_ENTITY_ID: entity_id, ATTR_TIMETABLE: config}, blocking=True, ) From 23604fd3b84c7f5a86a2f77ef8c86e09b701e193 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Fri, 16 Oct 2020 07:59:12 +0000 Subject: [PATCH 18/27] Rerun tests --- homeassistant/components/input_timetable/__init__.py | 7 +++---- homeassistant/components/input_timetable/services.yaml | 10 ++++++++++ tests/components/input_timetable/test_init.py | 3 +-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py index fdf9103479409c..538091bc1f68fe 100644 --- a/homeassistant/components/input_timetable/__init__.py +++ b/homeassistant/components/input_timetable/__init__.py @@ -31,7 +31,6 @@ ATTR_TIME = "time" ATTR_TIMETABLE = "timetable" -ATTR_CONFIG = "config" SERVICE_SET = "set" SERVICE_UNSET = "unset" @@ -82,7 +81,7 @@ class StateType(enum.Enum): ) SERVICE_RECONFIG_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), + vol.Required(ATTR_TIMETABLE): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), }, extra=vol.ALLOW_EXTRA, ) @@ -306,13 +305,13 @@ async def async_reset(self): self._timetable.clear() self._update_state() - async def async_reconfig(self, config): + async def async_reconfig(self, timetable): """Override the timetable with the new list.""" self._timetable = [ StateEvent( event[ATTR_TIME].replace(microsecond=0, tzinfo=None), event[ATTR_STATE] ) - for event in config + for event in timetable ] self._sort_timetable() self._update_state() diff --git a/homeassistant/components/input_timetable/services.yaml b/homeassistant/components/input_timetable/services.yaml index cc6d8431b9cfb6..c2204e274b91d2 100644 --- a/homeassistant/components/input_timetable/services.yaml +++ b/homeassistant/components/input_timetable/services.yaml @@ -25,5 +25,15 @@ reset: entity_id: description: Entity id of the input timetable that should be reset. example: input_timetable.lights +reconfig: + description: Overrides the timetable with the provided list. + fields: + entity_id: + description: Entity id of the input timetable that should be set. + example: input_timetable.lights + timetable: + description: List of state changes. + example: + [{ time: "01:02:03", state: "on" }, { time: "04:05:06", state: "off" }] reload: description: Reload the input_timetable configuration. diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index 93863810cdbcfb..fb757671825abb 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -4,7 +4,6 @@ import pytest from homeassistant.components.input_timetable import ( - ATTR_CONFIG, ATTR_STATE, ATTR_TIME, ATTR_TIMETABLE, @@ -99,7 +98,7 @@ async def call_reconfig(hass, entity_id, config): await hass.services.async_call( DOMAIN, SERVICE_RECONFIG, - {ATTR_ENTITY_ID: entity_id, ATTR_CONFIG: config}, + {ATTR_ENTITY_ID: entity_id, ATTR_TIMETABLE: config}, blocking=True, ) From a3e23d5f7c647557f6d14208ec2bd57d0cf04b0d Mon Sep 17 00:00:00 2001 From: amitfin Date: Thu, 22 Oct 2020 23:02:40 +0300 Subject: [PATCH 19/27] Update manifest.json --- homeassistant/components/default_config/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 27c076dd3d659e..f7ca925d66fe63 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -26,7 +26,7 @@ "input_text", "input_number", "input_select", - "input_timetable" + "input_timetable", "counter", "timer" ], From 06988c48f8a34120981397ded3b4a50e9dee4123 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Sat, 24 Oct 2020 17:59:29 +0000 Subject: [PATCH 20/27] Use @pytest.mark.parametrize --- tests/components/input_timetable/test_init.py | 84 +++++++++---------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index fb757671825abb..e0a85901f91d39 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -124,14 +124,10 @@ async def test_editable_state_attribute(hass, storage_setup): assert not state.attributes[ATTR_EDITABLE] -async def test_set(hass, caplog): - """Test set method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - - test_cases = [ +@pytest.mark.parametrize( + ["config", "timetable"], + [ ( - "single", [("01:02:03", STATE_ON)], [ { @@ -141,7 +137,6 @@ async def test_set(hass, caplog): ], ), ( - "multiple", [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], [ { @@ -155,7 +150,6 @@ async def test_set(hass, caplog): ], ), ( - "override", [("01:02:03", STATE_ON), ("01:02:03", STATE_OFF)], [ { @@ -164,34 +158,34 @@ async def test_set(hass, caplog): }, ], ), - ] - - for test_case in test_cases: - await call_reset(hass, entity_id) - for event in test_case[1]: - time = datetime.time.fromisoformat(event[0]) - state = event[1] - await call_set(hass, entity_id, time, state) - state = hass.states.get(entity_id) - assert ( - state.attributes[ATTR_TIMETABLE] == test_case[2] - ), f"'{test_case[0]}' test case failed: timetable is '{state.attributes[ATTR_TIMETABLE]}' but expecting '{test_case[2]}''" - - -async def test_unset(hass, caplog): - """Test unset method.""" + ], + ids=[ + "single", + "multiple", + "override", + ], +) +async def test_set(hass, config, timetable): + """Test set method.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) entity_id = "input_timetable.test" + for event in config: + time = datetime.time.fromisoformat(event[0]) + state = event[1] + await call_set(hass, entity_id, time, state) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TIMETABLE] == timetable + - test_cases = [ +@pytest.mark.parametrize( + ["config", "unset", "timetable"], + [ ( - "single", [("01:02:03", STATE_ON)], "01:02:03", [], ), ( - "multiple", [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], "04:05:06", [ @@ -202,7 +196,6 @@ async def test_unset(hass, caplog): ], ), ( - "none", [("01:02:03", STATE_ON)], "04:05:06", [ @@ -212,20 +205,25 @@ async def test_unset(hass, caplog): }, ], ), - ] - - for test_case in test_cases: - await call_reset(hass, entity_id) - for event in test_case[1]: - time = datetime.time.fromisoformat(event[0]) - state = event[1] - await call_set(hass, entity_id, time, state) - time = datetime.time.fromisoformat(test_case[2]) - await call_unset(hass, entity_id, time) - state = hass.states.get(entity_id) - assert ( - state.attributes[ATTR_TIMETABLE] == test_case[3] - ), f"'{test_case[0]}' test case failed: timetable is '{state.attributes[ATTR_TIMETABLE]}' but expecting '{test_case[3]}''" + ], + ids=[ + "single", + "multiple", + "none", + ], +) +async def test_unset(hass, config, unset, timetable): + """Test unset method.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = "input_timetable.test" + for event in config: + time = datetime.time.fromisoformat(event[0]) + state = event[1] + await call_set(hass, entity_id, time, state) + time = datetime.time.fromisoformat(unset) + await call_unset(hass, entity_id, time) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TIMETABLE] == timetable async def test_reset(hass, caplog): From 0bd2f0c43a9dd981c5b4f9db34fcf3c59ce49d2c Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Sat, 24 Oct 2020 23:25:52 +0000 Subject: [PATCH 21/27] Use dt_util --- homeassistant/components/input_timetable/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py index 538091bc1f68fe..276e1dfd4c28cb 100644 --- a/homeassistant/components/input_timetable/__init__.py +++ b/homeassistant/components/input_timetable/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -37,7 +38,7 @@ SERVICE_RESET = "reset" SERVICE_RECONFIG = "reconfig" -MIDNIGHT = datetime.time.fromisoformat("00:00:00") +MIDNIGHT = datetime.time() class StateType(enum.Enum): @@ -229,7 +230,7 @@ def state(self): """Return the state based on the timetable events.""" if not self._timetable: return STATE_OFF - now = datetime.datetime.now().time() + now = dt_util.now().time() prev = StateEvent(MIDNIGHT, self._timetable[-1].state) for event in self._timetable: if prev.time <= now < event.time: @@ -329,7 +330,7 @@ def _schedule_update(self): if not self._timetable or len(self._timetable) == 1: return - now = datetime.datetime.now() + now = dt_util.now() time = now.time() today = now.date() prev = MIDNIGHT From 25b46c1eb4aa382b6d3612094b7f46d3b163754f Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Sun, 25 Oct 2020 00:50:22 +0000 Subject: [PATCH 22/27] Update "issur_melacha_in_effect" via time tracking --- .../jewish_calendar/binary_sensor.py | 46 +++- .../jewish_calendar/test_binary_sensor.py | 196 ++++++++++++++++-- 2 files changed, 216 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 6edcc7b27c3ce7..5c80bc1a3839d4 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -1,7 +1,11 @@ """Support for Jewish Calendar binary sensors.""" +import datetime as dt + import hdate from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback +from homeassistant.helpers import event import homeassistant.util.dt as dt_util from . import DOMAIN, SENSOR_TYPES @@ -32,8 +36,8 @@ def __init__(self, data, sensor, sensor_info): self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] - self._state = False self._prefix = data["prefix"] + self._next_update = None @property def icon(self): @@ -53,11 +57,16 @@ def name(self): @property def is_on(self): """Return true if sensor is on.""" - return self._state + return self._get_zmanim().issur_melacha_in_effect - async def async_update(self): - """Update the state of the sensor.""" - zmanim = hdate.Zmanim( + @property + def should_poll(self): + """No polling needed.""" + return False + + def _get_zmanim(self): + """Return the Zmanim object for now().""" + return hdate.Zmanim( date=dt_util.now(), location=self._location, candle_lighting_offset=self._candle_lighting_offset, @@ -65,4 +74,29 @@ async def async_update(self): hebrew=self._hebrew, ) - self._state = zmanim.issur_melacha_in_effect + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._schedule_update() + + @callback + async def _update(self, now=None): + """Update the state of the sensor.""" + self._schedule_update() + self.async_write_ha_state() + + def _schedule_update(self): + """Schedule the next update of the sensor.""" + now = dt_util.now() + zmanim = self._get_zmanim() + update = zmanim.zmanim["sunrise"] + dt.timedelta(days=1) + candle_lighting = zmanim.candle_lighting + if candle_lighting is not None and now < candle_lighting < update: + update = candle_lighting + havdalah = zmanim.havdalah + if havdalah is not None and now < havdalah < update: + update = havdalah + if self._next_update == update: + return + self._next_update = update + event.async_track_point_in_time(self.hass, self._update, update) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 8986c2e6f53ecd..67a586506b31c6 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -15,22 +15,108 @@ make_nyc_test_params, ) +from tests.async_mock import patch from tests.common import async_fire_time_changed MELACHA_PARAMS = [ - make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON), - make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 7, 13, 1), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 8, 21, 25), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 9, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 9, 10, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 9, 28, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF), - make_nyc_test_params(dt(2018, 9, 30, 21, 25), STATE_ON), - make_nyc_test_params(dt(2018, 10, 1, 21, 25), STATE_ON), - make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF), - make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), STATE_ON), - make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), STATE_OFF), + make_nyc_test_params( + dt(2018, 9, 1, 16, 0), + { + "state": STATE_ON, + "update": dt(2018, 9, 1, 20, 14), + }, + ), + make_nyc_test_params( + dt(2018, 9, 1, 20, 21), + { + "state": STATE_OFF, + "update": dt(2018, 9, 2, 6, 21), + }, + ), + make_nyc_test_params( + dt(2018, 9, 7, 13, 1), + { + "state": STATE_OFF, + "update": dt(2018, 9, 7, 19, 4), + }, + ), + make_nyc_test_params( + dt(2018, 9, 8, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 9, 9, 6, 27), + }, + ), + make_nyc_test_params( + dt(2018, 9, 9, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 10, 6, 28), + }, + ), + make_nyc_test_params( + dt(2018, 9, 10, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 11, 6, 29), + }, + ), + make_nyc_test_params( + dt(2018, 9, 11, 11, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 11, 19, 57), + }, + ), + make_nyc_test_params( + dt(2018, 9, 29, 16, 25), + { + "state": STATE_ON, + "update": dt(2018, 9, 29, 19, 25), + }, + ), + make_nyc_test_params( + dt(2018, 9, 29, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 9, 30, 6, 48), + }, + ), + make_nyc_test_params( + dt(2018, 9, 30, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 10, 1, 6, 49), + }, + ), + make_nyc_test_params( + dt(2018, 10, 1, 21, 25), + { + "state": STATE_ON, + "update": dt(2018, 10, 2, 6, 50), + }, + ), + make_jerusalem_test_params( + dt(2018, 9, 29, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 9, 30, 6, 29), + }, + ), + make_jerusalem_test_params( + dt(2018, 10, 1, 11, 25), + { + "state": STATE_ON, + "update": dt(2018, 10, 1, 19, 2), + }, + ), + make_jerusalem_test_params( + dt(2018, 10, 1, 21, 25), + { + "state": STATE_OFF, + "update": dt(2018, 10, 2, 6, 31), + }, + ), ] MELACHA_TEST_IDS = [ @@ -39,7 +125,8 @@ "friday_upcoming_shabbat", "upcoming_rosh_hashana", "currently_rosh_hashana", - "second_day_rosh_hashana", + "second_day_rosh_hashana_night", + "second_day_rosh_hashana_day", "currently_shabbat_chol_hamoed", "upcoming_two_day_yomtov_in_diaspora", "currently_first_day_of_two_day_yomtov_in_diaspora", @@ -86,7 +173,9 @@ async def test_issur_melacha_sensor( registry = await hass.helpers.entity_registry.async_get_registry() - with alter_time(test_time): + with alter_time(test_time), patch( + "homeassistant.helpers.event.async_track_point_in_time" + ) as async_track_point_in_time: assert await async_setup_component( hass, jewish_calendar.DOMAIN, @@ -102,13 +191,9 @@ async def test_issur_melacha_sensor( ) await hass.async_block_till_done() - future = dt_util.utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert ( hass.states.get("binary_sensor.test_issur_melacha_in_effect").state - == result + == result["state"] ) entity = registry.async_get("binary_sensor.test_issur_melacha_in_effect") target_uid = "_".join( @@ -128,3 +213,74 @@ async def test_issur_melacha_sensor( ) ) assert entity.unique_id == target_uid + + assert async_track_point_in_time.call_args[0][2] == result["update"] + + +@pytest.mark.parametrize( + [ + "now", + "candle_lighting", + "havdalah", + "diaspora", + "tzname", + "latitude", + "longitude", + "result", + ], + [ + make_nyc_test_params(dt(2020, 10, 23, 17, 46), [STATE_OFF, STATE_ON]), + make_nyc_test_params(dt(2020, 10, 24, 18, 44), [STATE_ON, STATE_OFF]), + ], + ids=["before_candle_lighting", "before_havdalah"], +) +async def test_issur_melacha_sensor_update( + hass, + legacy_patchable_time, + now, + candle_lighting, + havdalah, + diaspora, + tzname, + latitude, + longitude, + result, +): + """Test Issur Melacha sensor output.""" + time_zone = dt_util.get_time_zone(tzname) + test_time = time_zone.localize(now) + + hass.config.time_zone = time_zone + hass.config.latitude = latitude + hass.config.longitude = longitude + + with alter_time(test_time): + assert await async_setup_component( + hass, + jewish_calendar.DOMAIN, + { + "jewish_calendar": { + "name": "test", + "language": "english", + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, + } + }, + ) + await hass.async_block_till_done() + + assert ( + hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + == result[0] + ) + + test_time += timedelta(seconds=61) + with alter_time(test_time): + async_fire_time_changed(hass, test_time) + await hass.async_block_till_done() + + assert ( + hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + == result[1] + ) From 8d7aa2b9648acaf8946042bdd9b1760286b80aec Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Sun, 25 Oct 2020 05:31:40 +0000 Subject: [PATCH 23/27] Decrease test window to 2 seconds (from 61) --- tests/components/jewish_calendar/test_binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 67a586506b31c6..e3bb138169daf9 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -229,8 +229,8 @@ async def test_issur_melacha_sensor( "result", ], [ - make_nyc_test_params(dt(2020, 10, 23, 17, 46), [STATE_OFF, STATE_ON]), - make_nyc_test_params(dt(2020, 10, 24, 18, 44), [STATE_ON, STATE_OFF]), + make_nyc_test_params(dt(2020, 10, 23, 17, 46, 59), [STATE_OFF, STATE_ON]), + make_nyc_test_params(dt(2020, 10, 24, 18, 44, 59), [STATE_ON, STATE_OFF]), ], ids=["before_candle_lighting", "before_havdalah"], ) @@ -275,7 +275,7 @@ async def test_issur_melacha_sensor_update( == result[0] ) - test_time += timedelta(seconds=61) + test_time += timedelta(seconds=2) with alter_time(test_time): async_fire_time_changed(hass, test_time) await hass.async_block_till_done() From 8843cd7c2e15931dd876d220296ad00ca5fef120 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Sun, 25 Oct 2020 08:31:52 +0000 Subject: [PATCH 24/27] Improve test_state cases --- tests/components/input_timetable/test_init.py | 122 ++++++++---------- 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py index e0a85901f91d39..7824cb4cf75206 100644 --- a/tests/components/input_timetable/test_init.py +++ b/tests/components/input_timetable/test_init.py @@ -29,7 +29,7 @@ # pylint: disable=protected-access from tests.async_mock import patch -from tests.common import mock_restore_cache +from tests.common import async_fire_time_changed, mock_restore_cache @pytest.fixture(name="storage_setup") @@ -260,85 +260,75 @@ async def test_reconfig(hass, caplog): ) -async def test_state(hass, caplog): +@patch("homeassistant.util.dt.now") +async def test_state(mock_now, hass): """Test state attribute.""" + mock_now.return_value = datetime.datetime.fromisoformat("2000-01-01 23:50:01") + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) entity_id = "input_timetable.test" + await call_reconfig( + hass, + entity_id, + [ + {ATTR_TIME: "23:50:00", ATTR_STATE: STATE_ON}, + {ATTR_TIME: "23:55:00", ATTR_STATE: STATE_OFF}, + {ATTR_TIME: "00:00:00", ATTR_STATE: STATE_ON}, + {ATTR_TIME: "00:05:00", ATTR_STATE: STATE_OFF}, + ], + ) + assert hass.states.get(entity_id).state == STATE_ON - assert hass.states.get(entity_id).state == STATE_OFF + state = STATE_OFF + for _ in range(3): + mock_now.return_value += datetime.timedelta(minutes=5) + async_fire_time_changed(hass, mock_now.return_value) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == state + state = STATE_ON if state == STATE_OFF else STATE_OFF - now = datetime.datetime.now().replace(microsecond=0) - in_5_minutes = now + datetime.timedelta(minutes=5) - in_10_minutes = now + datetime.timedelta(minutes=10) - previous_10_minutes = now + datetime.timedelta(minutes=-10) - previous_5_minutes = now + datetime.timedelta(minutes=-5) - await call_set(hass, entity_id, previous_5_minutes.time().isoformat(), STATE_ON) - assert hass.states.get(entity_id).state == STATE_ON - await call_reset(hass, entity_id) +@patch("homeassistant.util.dt.now") +@patch("homeassistant.helpers.event.async_track_point_in_time") +async def test_state_update(async_track_point_in_time, mock_now, hass): + """Test next update time.""" + mock_now.return_value = datetime.datetime.fromisoformat("2000-01-01") + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) + entity_id = f"{DOMAIN}.test" - await call_set(hass, entity_id, in_5_minutes.time().isoformat(), STATE_ON) - assert hass.states.get(entity_id).state == STATE_ON - await call_reset(hass, entity_id) + in_5_minutes = mock_now.return_value + datetime.timedelta(minutes=5) + in_10_minutes = mock_now.return_value + datetime.timedelta(minutes=10) + previous_5_minutes = mock_now.return_value + datetime.timedelta(minutes=-5) + previous_10_minutes = mock_now.return_value + datetime.timedelta(minutes=-10) - await call_set(hass, entity_id, previous_10_minutes.time().isoformat(), STATE_ON) - await call_set(hass, entity_id, previous_5_minutes.time().isoformat(), STATE_OFF) - assert hass.states.get(entity_id).state == STATE_OFF - await call_reset(hass, entity_id) + # No events => no updates. + assert async_track_point_in_time.call_count == 0 - await call_set(hass, entity_id, in_5_minutes.time().isoformat(), STATE_ON) - await call_set(hass, entity_id, in_10_minutes.time().isoformat(), STATE_OFF) - assert hass.states.get(entity_id).state == STATE_OFF + # One event => no updates. + await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) + assert async_track_point_in_time.call_count == 0 await call_reset(hass, entity_id) - await call_set(hass, entity_id, previous_5_minutes.time().isoformat(), STATE_ON) - await call_set(hass, entity_id, in_5_minutes.time().isoformat(), STATE_OFF) - assert hass.states.get(entity_id).state == STATE_ON + # Between 2 events. + await call_set(hass, entity_id, previous_5_minutes.time(), STATE_ON) + await call_set(hass, entity_id, in_5_minutes.time(), STATE_OFF) + next_update = async_track_point_in_time.call_args[0][2] + assert next_update == in_5_minutes await call_reset(hass, entity_id) + # After any event. + await call_set(hass, entity_id, previous_10_minutes.time(), STATE_ON) + await call_set(hass, entity_id, previous_5_minutes.time(), STATE_OFF) + next_update = async_track_point_in_time.call_args[0][2] + assert next_update == previous_10_minutes + datetime.timedelta(days=1) + await call_reset(hass, entity_id) -async def test_state_update(hass, caplog): - """Test next update time.""" - with patch( - "homeassistant.helpers.event.async_track_point_in_time" - ) as async_track_point_in_time: - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = f"{DOMAIN}.test" - - now = datetime.datetime.now().replace(microsecond=0) - in_5_minutes = now + datetime.timedelta(minutes=5) - in_10_minutes = now + datetime.timedelta(minutes=10) - previous_5_minutes = now + datetime.timedelta(minutes=-5) - previous_10_minutes = now + datetime.timedelta(minutes=-10) - - # No events => no updates. - assert async_track_point_in_time.call_count == 0 - - # One event => no updates. - await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) - assert async_track_point_in_time.call_count == 0 - await call_reset(hass, entity_id) - - # Between 2 events. - await call_set(hass, entity_id, previous_5_minutes.time(), STATE_ON) - await call_set(hass, entity_id, in_5_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes - await call_reset(hass, entity_id) - - # After any event. - await call_set(hass, entity_id, previous_10_minutes.time(), STATE_ON) - await call_set(hass, entity_id, previous_5_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == previous_10_minutes + datetime.timedelta(days=1) - await call_reset(hass, entity_id) - - # Before any event. - await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) - await call_set(hass, entity_id, in_10_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes - await call_reset(hass, entity_id) + # Before any event. + await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) + await call_set(hass, entity_id, in_10_minutes.time(), STATE_OFF) + next_update = async_track_point_in_time.call_args[0][2] + assert next_update == in_5_minutes + await call_reset(hass, entity_id) async def test_restore_state(hass): From 82c23113ddccb249aced8cdfee236de203cb082f Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Mon, 26 Oct 2020 10:55:31 +0000 Subject: [PATCH 25/27] Decrease test window to 1 second (from 2) --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/jewish_calendar/test_binary_sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 9f0d55433f04ed..500d98dbe9fa58 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -2,6 +2,6 @@ "domain": "jewish_calendar", "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", - "requirements": ["hdate==0.9.5"], + "requirements": ["hdate==0.9.12"], "codeowners": ["@tsvi"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9ed9c4f83f5128..df877ddd04d736 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ hass_splunk==0.1.1 hatasmota==0.0.22 # homeassistant.components.jewish_calendar -hdate==0.9.5 +hdate==0.9.12 # homeassistant.components.heatmiser heatmiserV3==1.1.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcfb3166f02af5..d76f7c4bcaa35a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hass-nabucasa==0.37.1 hatasmota==0.0.22 # homeassistant.components.jewish_calendar -hdate==0.9.5 +hdate==0.9.12 # homeassistant.components.here_travel_time herepy==2.0.0 diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index e3bb138169daf9..38c6cdbc8ddd1f 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -275,7 +275,7 @@ async def test_issur_melacha_sensor_update( == result[0] ) - test_time += timedelta(seconds=2) + test_time += timedelta(seconds=1) with alter_time(test_time): async_fire_time_changed(hass, test_time) await hass.async_block_till_done() From 77f6b376fde19ca64f1854eef6ce1a9f0d964d86 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Mon, 26 Oct 2020 11:06:42 +0000 Subject: [PATCH 26/27] Delete unrelated changes --- CODEOWNERS | 1 - .../components/default_config/manifest.json | 1 - .../components/input_timetable/__init__.py | 368 ------------ .../components/input_timetable/manifest.json | 7 - .../components/input_timetable/services.yaml | 39 -- .../components/input_timetable/strings.json | 9 - .../input_timetable/translations/en.json | 9 - tests/components/input_timetable/__init__.py | 1 - tests/components/input_timetable/test_init.py | 557 ------------------ 9 files changed, 992 deletions(-) delete mode 100644 homeassistant/components/input_timetable/__init__.py delete mode 100644 homeassistant/components/input_timetable/manifest.json delete mode 100644 homeassistant/components/input_timetable/services.yaml delete mode 100644 homeassistant/components/input_timetable/strings.json delete mode 100644 homeassistant/components/input_timetable/translations/en.json delete mode 100644 tests/components/input_timetable/__init__.py delete mode 100644 tests/components/input_timetable/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 6db5fafa53b672..18439b3bb83c10 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,7 +209,6 @@ homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core -homeassistant/components/input_timetable/* @home-assistant/core homeassistant/components/insteon/* @teharris1 homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index f7ca925d66fe63..8c6a3dde6cfa6b 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -26,7 +26,6 @@ "input_text", "input_number", "input_select", - "input_timetable", "counter", "timer" ], diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py deleted file mode 100644 index 276e1dfd4c28cb..00000000000000 --- a/homeassistant/components/input_timetable/__init__.py +++ /dev/null @@ -1,368 +0,0 @@ -"""Support to set a timetable (on and off times during the day).""" -import datetime -import enum -import logging -import typing - -import voluptuous as vol - -import homeassistant -from homeassistant.const import ( - ATTR_EDITABLE, - ATTR_STATE, - CONF_ICON, - CONF_ID, - CONF_NAME, - SERVICE_RELOAD, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import callback -from homeassistant.helpers import collection, event as event_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType -import homeassistant.util.dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "input_timetable" - -ATTR_TIME = "time" -ATTR_TIMETABLE = "timetable" - -SERVICE_SET = "set" -SERVICE_UNSET = "unset" -SERVICE_RESET = "reset" -SERVICE_RECONFIG = "reconfig" - -MIDNIGHT = datetime.time() - - -class StateType(enum.Enum): - """Possible options for states.""" - - on = STATE_ON - off = STATE_OFF - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: cv.schema_with_slug_keys( - vol.Any( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, - }, - None, - ) - ) - }, - extra=vol.ALLOW_EXTRA, -) - -SERVICE_SET_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TIME): cv.time, - vol.Required(ATTR_STATE): cv.enum(StateType), - }, - extra=vol.ALLOW_EXTRA, -) -SERVICE_UNSET_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TIME): cv.time, - }, - extra=vol.ALLOW_EXTRA, -) -SERVICE_RESET_SCHEMA = vol.Schema( - {}, - extra=vol.ALLOW_EXTRA, -) -SERVICE_RECONFIG_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TIMETABLE): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), - }, - extra=vol.ALLOW_EXTRA, -) -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - -STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 - -CREATE_FIELDS = { - vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), - vol.Optional(CONF_ICON): cv.icon, -} - -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, -} - - -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up an input timetable.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - id_manager = collection.IDManager() - - yaml_collection = collection.YamlCollection( - logging.getLogger(f"{__name__}.yaml_collection"), id_manager - ) - collection.attach_entity_component_collection( - component, yaml_collection, InputTimeTable.from_yaml - ) - - storage_collection = TimeTableStorageCollection( - Store(hass, STORAGE_VERSION, STORAGE_KEY), - logging.getLogger(f"{__name__}.storage_collection"), - id_manager, - ) - collection.attach_entity_component_collection( - component, storage_collection, InputTimeTable - ) - - await yaml_collection.async_load( - [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] - ) - await storage_collection.async_load() - - collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS - ).async_setup(hass) - - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - - component.async_register_entity_service( - SERVICE_SET, SERVICE_SET_SCHEMA, "async_set" - ) - component.async_register_entity_service( - SERVICE_UNSET, SERVICE_UNSET_SCHEMA, "async_unset" - ) - component.async_register_entity_service( - SERVICE_RESET, SERVICE_RESET_SCHEMA, "async_reset" - ) - component.async_register_entity_service( - SERVICE_RECONFIG, SERVICE_RECONFIG_SCHEMA, "async_reconfig" - ) - - async def reload_service_handler(service_call: ServiceCallType) -> None: - """Reload yaml entities.""" - conf = await component.async_prepare_reload(skip_reset=True) - if conf is None: - conf = {DOMAIN: {}} - await yaml_collection.async_load( - [ - {CONF_ID: id_, **(conf or {})} - for id_, conf in conf.get(DOMAIN, {}).items() - ] - ) - - homeassistant.helpers.service.async_register_admin_service( - hass, - DOMAIN, - SERVICE_RELOAD, - reload_service_handler, - schema=RELOAD_SERVICE_SCHEMA, - ) - - return True - - -class TimeTableStorageCollection(collection.StorageCollection): - """Input storage based collection.""" - - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: - """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) - - @callback - def _get_suggested_id(self, info: typing.Dict) -> str: - """Suggest an ID based on the config.""" - return info[CONF_NAME] - - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: - """Return a new updated data object.""" - self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} - - -class InputTimeTable(RestoreEntity): - """Representation of a timetable.""" - - def __init__(self, config: typing.Dict): - """Initialize an input timetable.""" - self._config = config - self._timetable = [] - self.editable = True - self._event_unsub = None - - @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputTimeTable": - """Return entity instance initialized from yaml storage.""" - input_timetable = cls(config) - input_timetable.entity_id = f"{DOMAIN}.{config[CONF_ID]}" - input_timetable.editable = False - return input_timetable - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the input timetable.""" - return self._config.get(CONF_NAME) - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._config.get(CONF_ICON) - - @property - def state(self): - """Return the state based on the timetable events.""" - if not self._timetable: - return STATE_OFF - now = dt_util.now().time() - prev = StateEvent(MIDNIGHT, self._timetable[-1].state) - for event in self._timetable: - if prev.time <= now < event.time: - break - prev = event - return prev.state.value - - @property - def state_attributes(self): - """Return the state attributes.""" - return { - ATTR_EDITABLE: self.editable, - ATTR_TIMETABLE: self._timetable_to_attribute(), - } - - @property - def unique_id(self) -> typing.Optional[str]: - """Return unique id of the entity.""" - return self._config[CONF_ID] - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if state and state.attributes.get(ATTR_TIMETABLE): - self._timetable_from_attribute(state.attributes[ATTR_TIMETABLE]) - self._update_state() - - def _timetable_to_attribute(self): - return [ - {ATTR_TIME: event.time.isoformat(), ATTR_STATE: event.state.value} - for event in self._timetable - ] - - def _timetable_from_attribute(self, timetable): - self._timetable = [ - StateEvent( - datetime.time.fromisoformat(event[ATTR_TIME]).replace( - microsecond=0, tzinfo=None - ), - StateType(event[ATTR_STATE]), - ) - for event in timetable - ] - self._sort_timetable() - - def _sort_timetable(self): - self._timetable.sort(key=lambda event: event.time) - - async def async_set(self, time, state): - """Add an state change event to the timetable.""" - time = time.replace(microsecond=0, tzinfo=None) - for event in self._timetable: - if event.time == time: - event.state = state - break - else: - self._timetable.append(StateEvent(time, state)) - self._sort_timetable() - self._update_state() - - async def async_unset(self, time): - """Remove a state change event.""" - time = time.replace(microsecond=0, tzinfo=None) - for event in self._timetable: - if event.time == time: - self._timetable.remove(event) - break - self._update_state() - - async def async_reset(self): - """Remove all state changes.""" - self._timetable.clear() - self._update_state() - - async def async_reconfig(self, timetable): - """Override the timetable with the new list.""" - self._timetable = [ - StateEvent( - event[ATTR_TIME].replace(microsecond=0, tzinfo=None), event[ATTR_STATE] - ) - for event in timetable - ] - self._sort_timetable() - self._update_state() - - async def async_update_config(self, config: typing.Dict) -> None: - """Handle when the config is updated.""" - self._config = config - self._update_state() - - def _schedule_update(self): - if self._event_unsub: - self._event_unsub() - self._event_unsub = None - - if not self._timetable or len(self._timetable) == 1: - return - - now = dt_util.now() - time = now.time() - today = now.date() - prev = MIDNIGHT - for event in self._timetable: - if prev <= time < event.time: - next_change = datetime.datetime.combine( - today, - event.time, - ) - break - prev = event.time - else: - next_change = datetime.datetime.combine( - today + datetime.timedelta(days=1), - self._timetable[0].time, - ) - - self._event_unsub = event_helper.async_track_point_in_time( - self.hass, self._update_state, next_change - ) - - @callback - def _update_state(self, now=None): - """Update the state to reflect the current time.""" - self._schedule_update() - self.async_write_ha_state() - - -class StateEvent: - """State event properties (time, and state value).""" - - def __init__(self, time, state): - """Initialize the object.""" - self.time = time - self.state = state diff --git a/homeassistant/components/input_timetable/manifest.json b/homeassistant/components/input_timetable/manifest.json deleted file mode 100644 index 8a5cc7b910541a..00000000000000 --- a/homeassistant/components/input_timetable/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "input_timetable", - "name": "Input Timetable", - "documentation": "https://www.home-assistant.io/integrations/input_timetable", - "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" -} diff --git a/homeassistant/components/input_timetable/services.yaml b/homeassistant/components/input_timetable/services.yaml deleted file mode 100644 index c2204e274b91d2..00000000000000 --- a/homeassistant/components/input_timetable/services.yaml +++ /dev/null @@ -1,39 +0,0 @@ -set: - description: Adds a state change at the specified time. - fields: - entity_id: - description: Entity id of the input timetable that should be set. - example: input_timetable.lights - time: - description: The time of the change. - example: '"05:04:20"' - state: - description: The new state (on or off). - example: "on" -unset: - description: Removes a state change. - fields: - entity_id: - description: Entity id of the input timetable that should be reset. - example: input_timetable.lights - time: - description: The time of the state change to remove. - example: '"05:04:20"' -reset: - description: Removes all state changes. - fields: - entity_id: - description: Entity id of the input timetable that should be reset. - example: input_timetable.lights -reconfig: - description: Overrides the timetable with the provided list. - fields: - entity_id: - description: Entity id of the input timetable that should be set. - example: input_timetable.lights - timetable: - description: List of state changes. - example: - [{ time: "01:02:03", state: "on" }, { time: "04:05:06", state: "off" }] -reload: - description: Reload the input_timetable configuration. diff --git a/homeassistant/components/input_timetable/strings.json b/homeassistant/components/input_timetable/strings.json deleted file mode 100644 index 9eca15963ace7e..00000000000000 --- a/homeassistant/components/input_timetable/strings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Input timetable", - "state": { - "_": { - "off": "[%key:common::state::off%]", - "on": "[%key:common::state::on%]" - } - } -} diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json deleted file mode 100644 index 6e37a00a85ea24..00000000000000 --- a/homeassistant/components/input_timetable/translations/en.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "state": { - "_": { - "off": "Off", - "on": "On" - } - }, - "title": "Input timetable" -} \ No newline at end of file diff --git a/tests/components/input_timetable/__init__.py b/tests/components/input_timetable/__init__.py deleted file mode 100644 index f77a4d71049c5b..00000000000000 --- a/tests/components/input_timetable/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the input_timetable component.""" diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py deleted file mode 100644 index 7824cb4cf75206..00000000000000 --- a/tests/components/input_timetable/test_init.py +++ /dev/null @@ -1,557 +0,0 @@ -"""The tests for the input timetable component.""" -import datetime - -import pytest - -from homeassistant.components.input_timetable import ( - ATTR_STATE, - ATTR_TIME, - ATTR_TIMETABLE, - DOMAIN, - SERVICE_RECONFIG, - SERVICE_RELOAD, - SERVICE_RESET, - SERVICE_SET, - SERVICE_UNSET, -) -from homeassistant.const import ( - ATTR_EDITABLE, - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - ATTR_NAME, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import Context, CoreState, State -from homeassistant.exceptions import Unauthorized -from homeassistant.helpers import entity_registry -from homeassistant.setup import async_setup_component - -# pylint: disable=protected-access -from tests.async_mock import patch -from tests.common import async_fire_time_changed, mock_restore_cache - - -@pytest.fixture(name="storage_setup") -def storage_setup_fixture(hass, hass_storage): - """Storage setup.""" - - async def _storage(items=None, config=None): - if items is None: - hass_storage[DOMAIN] = { - "key": DOMAIN, - "version": 1, - "data": { - "items": [ - { - "id": "from_storage", - "name": "from storage", - } - ] - }, - } - else: - hass_storage[DOMAIN] = { - "key": DOMAIN, - "version": 1, - "data": {"items": items}, - } - if config is None: - config = {DOMAIN: {}} - return await async_setup_component(hass, DOMAIN, config) - - return _storage - - -async def call_set(hass, entity_id, time, state): - """Add a state change event.""" - await hass.services.async_call( - DOMAIN, - SERVICE_SET, - {ATTR_ENTITY_ID: entity_id, ATTR_TIME: time, ATTR_STATE: state}, - blocking=True, - ) - - -async def call_unset(hass, entity_id, time): - """Remove a state change.""" - await hass.services.async_call( - DOMAIN, - SERVICE_UNSET, - {ATTR_ENTITY_ID: entity_id, ATTR_TIME: time}, - blocking=True, - ) - - -async def call_reset(hass, entity_id): - """Remove all state changes.""" - await hass.services.async_call( - DOMAIN, - SERVICE_RESET, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - -async def call_reconfig(hass, entity_id, config): - """Override the timetable with the new list.""" - await hass.services.async_call( - DOMAIN, - SERVICE_RECONFIG, - {ATTR_ENTITY_ID: entity_id, ATTR_TIMETABLE: config}, - blocking=True, - ) - - -async def test_load_from_storage(hass, storage_setup): - """Test set up from storage.""" - assert await storage_setup() - - state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" - - -async def test_editable_state_attribute(hass, storage_setup): - """Test editable attribute.""" - assert await storage_setup(config={DOMAIN: {"from_yaml": {ATTR_NAME: "from yaml"}}}) - - state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" - assert state.attributes[ATTR_EDITABLE] - - state = hass.states.get(f"{DOMAIN}.from_yaml") - assert state.attributes[ATTR_FRIENDLY_NAME] == "from yaml" - assert not state.attributes[ATTR_EDITABLE] - - -@pytest.mark.parametrize( - ["config", "timetable"], - [ - ( - [("01:02:03", STATE_ON)], - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_ON, - }, - ], - ), - ( - [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_OFF, - }, - { - ATTR_TIME: "04:05:06", - ATTR_STATE: STATE_ON, - }, - ], - ), - ( - [("01:02:03", STATE_ON), ("01:02:03", STATE_OFF)], - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_OFF, - }, - ], - ), - ], - ids=[ - "single", - "multiple", - "override", - ], -) -async def test_set(hass, config, timetable): - """Test set method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - for event in config: - time = datetime.time.fromisoformat(event[0]) - state = event[1] - await call_set(hass, entity_id, time, state) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_TIMETABLE] == timetable - - -@pytest.mark.parametrize( - ["config", "unset", "timetable"], - [ - ( - [("01:02:03", STATE_ON)], - "01:02:03", - [], - ), - ( - [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], - "04:05:06", - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_OFF, - }, - ], - ), - ( - [("01:02:03", STATE_ON)], - "04:05:06", - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_ON, - }, - ], - ), - ], - ids=[ - "single", - "multiple", - "none", - ], -) -async def test_unset(hass, config, unset, timetable): - """Test unset method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - for event in config: - time = datetime.time.fromisoformat(event[0]) - state = event[1] - await call_set(hass, entity_id, time, state) - time = datetime.time.fromisoformat(unset) - await call_unset(hass, entity_id, time) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_TIMETABLE] == timetable - - -async def test_reset(hass, caplog): - """Test reset method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - await call_set(hass, entity_id, "01:02:03", STATE_ON) - await call_set(hass, entity_id, "04:05:06", STATE_OFF) - await call_set(hass, entity_id, "07:08:09", STATE_ON) - await call_set(hass, entity_id, "10:11:12", STATE_OFF) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 4 - await call_reset(hass, entity_id) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 0 - - -async def test_reconfig(hass, caplog): - """Test reconfig method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - await call_set(hass, entity_id, "01:02:03", STATE_ON) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 1 - await call_reconfig( - hass, - entity_id, - [ - {ATTR_TIME: "07:08:09", ATTR_STATE: STATE_OFF}, - {ATTR_TIME: "04:05:06", ATTR_STATE: STATE_ON}, - ], - ) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 2 - assert ( - hass.states.get(entity_id).attributes[ATTR_TIMETABLE][0][ATTR_TIME] - == "04:05:06" - ) - - -@patch("homeassistant.util.dt.now") -async def test_state(mock_now, hass): - """Test state attribute.""" - mock_now.return_value = datetime.datetime.fromisoformat("2000-01-01 23:50:01") - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - await call_reconfig( - hass, - entity_id, - [ - {ATTR_TIME: "23:50:00", ATTR_STATE: STATE_ON}, - {ATTR_TIME: "23:55:00", ATTR_STATE: STATE_OFF}, - {ATTR_TIME: "00:00:00", ATTR_STATE: STATE_ON}, - {ATTR_TIME: "00:05:00", ATTR_STATE: STATE_OFF}, - ], - ) - assert hass.states.get(entity_id).state == STATE_ON - - state = STATE_OFF - for _ in range(3): - mock_now.return_value += datetime.timedelta(minutes=5) - async_fire_time_changed(hass, mock_now.return_value) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == state - state = STATE_ON if state == STATE_OFF else STATE_OFF - - -@patch("homeassistant.util.dt.now") -@patch("homeassistant.helpers.event.async_track_point_in_time") -async def test_state_update(async_track_point_in_time, mock_now, hass): - """Test next update time.""" - mock_now.return_value = datetime.datetime.fromisoformat("2000-01-01") - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = f"{DOMAIN}.test" - - in_5_minutes = mock_now.return_value + datetime.timedelta(minutes=5) - in_10_minutes = mock_now.return_value + datetime.timedelta(minutes=10) - previous_5_minutes = mock_now.return_value + datetime.timedelta(minutes=-5) - previous_10_minutes = mock_now.return_value + datetime.timedelta(minutes=-10) - - # No events => no updates. - assert async_track_point_in_time.call_count == 0 - - # One event => no updates. - await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) - assert async_track_point_in_time.call_count == 0 - await call_reset(hass, entity_id) - - # Between 2 events. - await call_set(hass, entity_id, previous_5_minutes.time(), STATE_ON) - await call_set(hass, entity_id, in_5_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes - await call_reset(hass, entity_id) - - # After any event. - await call_set(hass, entity_id, previous_10_minutes.time(), STATE_ON) - await call_set(hass, entity_id, previous_5_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == previous_10_minutes + datetime.timedelta(days=1) - await call_reset(hass, entity_id) - - # Before any event. - await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) - await call_set(hass, entity_id, in_10_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes - await call_reset(hass, entity_id) - - -async def test_restore_state(hass): - """Ensure states are restored on startup.""" - a_timetable = [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_ON, - }, - ] - b_timetable = [ - { - ATTR_TIME: "07:08:09", - ATTR_STATE: STATE_OFF, - }, - ] - mock_restore_cache( - hass, - ( - State("input_timetable.a", "", {ATTR_TIMETABLE: a_timetable}), - State("input_timetable.b", "", {ATTR_TIMETABLE: b_timetable}), - ), - ) - - hass.state = CoreState.starting - - await async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"a": {}, "b": {}}}, - ) - - state = hass.states.get("input_timetable.a") - assert state - assert state.attributes[ATTR_TIMETABLE] == a_timetable - - state = hass.states.get("input_timetable.b") - assert state - assert state.attributes[ATTR_TIMETABLE] == b_timetable - - -async def test_input_scheudle_context(hass, hass_admin_user): - """Test that input_timetable context works.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"x": {}}}) - - state = hass.states.get(f"{DOMAIN}.x") - assert state is not None - - await hass.services.async_call( - DOMAIN, - SERVICE_SET, - { - ATTR_ENTITY_ID: state.entity_id, - ATTR_TIME: datetime.time.fromisoformat("01:02:03"), - ATTR_STATE: STATE_ON, - }, - True, - Context(user_id=hass_admin_user.id), - ) - - state2 = hass.states.get(f"{DOMAIN}.x") - assert state2 is not None - assert state.attributes[ATTR_TIMETABLE] != state2.attributes[ATTR_TIMETABLE] - assert state2.context.user_id == hass_admin_user.id - - -async def test_reload(hass, hass_admin_user, hass_read_only_user): - """Test reload service.""" - count_start = len(hass.states.async_entity_ids()) - ent_reg = await entity_registry.async_get_registry(hass) - - assert await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "test_1": {ATTR_NAME: "before"}, - "test_3": {}, - } - }, - ) - - assert count_start + 2 == len(hass.states.async_entity_ids()) - - state_1 = hass.states.get(f"{DOMAIN}.test_1") - state_2 = hass.states.get(f"{DOMAIN}.test_2") - state_3 = hass.states.get(f"{DOMAIN}.test_3") - - assert state_1 is not None - assert state_1.attributes[ATTR_FRIENDLY_NAME] == "before" - assert state_2 is None - assert state_3 is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None - - with patch( - "homeassistant.config.load_yaml_config_file", - autospec=True, - return_value={ - DOMAIN: { - "test_1": {ATTR_NAME: "after"}, - "test_2": {}, - } - }, - ): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_admin_user.id), - ) - await hass.async_block_till_done() - - assert count_start + 2 == len(hass.states.async_entity_ids()) - - state_1 = hass.states.get(f"{DOMAIN}.test_1") - assert state_1.attributes[ATTR_FRIENDLY_NAME] == "after" - state_2 = hass.states.get(f"{DOMAIN}.test_2") - state_3 = hass.states.get(f"{DOMAIN}.test_3") - - assert state_1 is not None - assert state_2 is not None - assert state_3 is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None - - -async def test_setup_no_config(hass, hass_admin_user): - """Test component setup with no config.""" - count_start = len(hass.states.async_entity_ids()) - assert await async_setup_component(hass, DOMAIN, {}) - - with patch( - "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} - ): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_admin_user.id), - ) - await hass.async_block_till_done() - - assert count_start == len(hass.states.async_entity_ids()) - - -async def test_ws_list(hass, hass_ws_client, storage_setup): - """Test listing via WS.""" - assert await storage_setup(config={DOMAIN: {"from_yaml": {}}}) - - client = await hass_ws_client(hass) - - await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) - resp = await client.receive_json() - assert resp["success"] - - storage_ent = "from_storage" - yaml_ent = "from_yaml" - result = {item["id"]: item for item in resp["result"]} - - assert len(result) == 1 - assert storage_ent in result - assert yaml_ent not in result - assert result[storage_ent][ATTR_NAME] == "from storage" - - -async def test_ws_delete(hass, hass_ws_client, storage_setup): - """Test WS delete cleans up entity registry.""" - assert await storage_setup() - - input_id = "from_storage" - input_entity_id = f"{DOMAIN}.{input_id}" - ent_reg = await entity_registry.async_get_registry(hass) - - state = hass.states.get(input_entity_id) - assert state is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None - - client = await hass_ws_client(hass) - - await client.send_json( - {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} - ) - resp = await client.receive_json() - assert resp["success"] - - state = hass.states.get(input_entity_id) - assert state is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None - - -async def test_ws_create(hass, hass_ws_client, storage_setup): - """Test create WS.""" - assert await storage_setup(items=[]) - - input_id = "new_input" - input_entity_id = f"{DOMAIN}.{input_id}" - ent_reg = await entity_registry.async_get_registry(hass) - - state = hass.states.get(input_entity_id) - assert state is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None - - client = await hass_ws_client(hass) - - await client.send_json( - { - "id": 6, - "type": f"{DOMAIN}/create", - "name": "New Input", - } - ) - resp = await client.receive_json() - assert resp["success"] - - state = hass.states.get(input_entity_id) - assert state.state == STATE_OFF From 6732a6ec9d9291fdd25a2aae73346bb6c2dba8bd Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Mon, 26 Oct 2020 11:06:42 +0000 Subject: [PATCH 27/27] Rerun test --- CODEOWNERS | 1 - .../components/default_config/manifest.json | 1 - .../components/input_timetable/__init__.py | 368 ------------ .../components/input_timetable/manifest.json | 7 - .../components/input_timetable/services.yaml | 39 -- .../components/input_timetable/strings.json | 9 - .../input_timetable/translations/en.json | 9 - tests/components/input_timetable/__init__.py | 1 - tests/components/input_timetable/test_init.py | 557 ------------------ 9 files changed, 992 deletions(-) delete mode 100644 homeassistant/components/input_timetable/__init__.py delete mode 100644 homeassistant/components/input_timetable/manifest.json delete mode 100644 homeassistant/components/input_timetable/services.yaml delete mode 100644 homeassistant/components/input_timetable/strings.json delete mode 100644 homeassistant/components/input_timetable/translations/en.json delete mode 100644 tests/components/input_timetable/__init__.py delete mode 100644 tests/components/input_timetable/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 6db5fafa53b672..18439b3bb83c10 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,7 +209,6 @@ homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core -homeassistant/components/input_timetable/* @home-assistant/core homeassistant/components/insteon/* @teharris1 homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index f7ca925d66fe63..8c6a3dde6cfa6b 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -26,7 +26,6 @@ "input_text", "input_number", "input_select", - "input_timetable", "counter", "timer" ], diff --git a/homeassistant/components/input_timetable/__init__.py b/homeassistant/components/input_timetable/__init__.py deleted file mode 100644 index 276e1dfd4c28cb..00000000000000 --- a/homeassistant/components/input_timetable/__init__.py +++ /dev/null @@ -1,368 +0,0 @@ -"""Support to set a timetable (on and off times during the day).""" -import datetime -import enum -import logging -import typing - -import voluptuous as vol - -import homeassistant -from homeassistant.const import ( - ATTR_EDITABLE, - ATTR_STATE, - CONF_ICON, - CONF_ID, - CONF_NAME, - SERVICE_RELOAD, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import callback -from homeassistant.helpers import collection, event as event_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType -import homeassistant.util.dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "input_timetable" - -ATTR_TIME = "time" -ATTR_TIMETABLE = "timetable" - -SERVICE_SET = "set" -SERVICE_UNSET = "unset" -SERVICE_RESET = "reset" -SERVICE_RECONFIG = "reconfig" - -MIDNIGHT = datetime.time() - - -class StateType(enum.Enum): - """Possible options for states.""" - - on = STATE_ON - off = STATE_OFF - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: cv.schema_with_slug_keys( - vol.Any( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, - }, - None, - ) - ) - }, - extra=vol.ALLOW_EXTRA, -) - -SERVICE_SET_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TIME): cv.time, - vol.Required(ATTR_STATE): cv.enum(StateType), - }, - extra=vol.ALLOW_EXTRA, -) -SERVICE_UNSET_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TIME): cv.time, - }, - extra=vol.ALLOW_EXTRA, -) -SERVICE_RESET_SCHEMA = vol.Schema( - {}, - extra=vol.ALLOW_EXTRA, -) -SERVICE_RECONFIG_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TIMETABLE): vol.All(cv.ensure_list, [SERVICE_SET_SCHEMA]), - }, - extra=vol.ALLOW_EXTRA, -) -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - -STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 - -CREATE_FIELDS = { - vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), - vol.Optional(CONF_ICON): cv.icon, -} - -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, -} - - -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up an input timetable.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - id_manager = collection.IDManager() - - yaml_collection = collection.YamlCollection( - logging.getLogger(f"{__name__}.yaml_collection"), id_manager - ) - collection.attach_entity_component_collection( - component, yaml_collection, InputTimeTable.from_yaml - ) - - storage_collection = TimeTableStorageCollection( - Store(hass, STORAGE_VERSION, STORAGE_KEY), - logging.getLogger(f"{__name__}.storage_collection"), - id_manager, - ) - collection.attach_entity_component_collection( - component, storage_collection, InputTimeTable - ) - - await yaml_collection.async_load( - [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] - ) - await storage_collection.async_load() - - collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS - ).async_setup(hass) - - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) - collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) - - component.async_register_entity_service( - SERVICE_SET, SERVICE_SET_SCHEMA, "async_set" - ) - component.async_register_entity_service( - SERVICE_UNSET, SERVICE_UNSET_SCHEMA, "async_unset" - ) - component.async_register_entity_service( - SERVICE_RESET, SERVICE_RESET_SCHEMA, "async_reset" - ) - component.async_register_entity_service( - SERVICE_RECONFIG, SERVICE_RECONFIG_SCHEMA, "async_reconfig" - ) - - async def reload_service_handler(service_call: ServiceCallType) -> None: - """Reload yaml entities.""" - conf = await component.async_prepare_reload(skip_reset=True) - if conf is None: - conf = {DOMAIN: {}} - await yaml_collection.async_load( - [ - {CONF_ID: id_, **(conf or {})} - for id_, conf in conf.get(DOMAIN, {}).items() - ] - ) - - homeassistant.helpers.service.async_register_admin_service( - hass, - DOMAIN, - SERVICE_RELOAD, - reload_service_handler, - schema=RELOAD_SERVICE_SCHEMA, - ) - - return True - - -class TimeTableStorageCollection(collection.StorageCollection): - """Input storage based collection.""" - - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: - """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) - - @callback - def _get_suggested_id(self, info: typing.Dict) -> str: - """Suggest an ID based on the config.""" - return info[CONF_NAME] - - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: - """Return a new updated data object.""" - self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} - - -class InputTimeTable(RestoreEntity): - """Representation of a timetable.""" - - def __init__(self, config: typing.Dict): - """Initialize an input timetable.""" - self._config = config - self._timetable = [] - self.editable = True - self._event_unsub = None - - @classmethod - def from_yaml(cls, config: typing.Dict) -> "InputTimeTable": - """Return entity instance initialized from yaml storage.""" - input_timetable = cls(config) - input_timetable.entity_id = f"{DOMAIN}.{config[CONF_ID]}" - input_timetable.editable = False - return input_timetable - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the input timetable.""" - return self._config.get(CONF_NAME) - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._config.get(CONF_ICON) - - @property - def state(self): - """Return the state based on the timetable events.""" - if not self._timetable: - return STATE_OFF - now = dt_util.now().time() - prev = StateEvent(MIDNIGHT, self._timetable[-1].state) - for event in self._timetable: - if prev.time <= now < event.time: - break - prev = event - return prev.state.value - - @property - def state_attributes(self): - """Return the state attributes.""" - return { - ATTR_EDITABLE: self.editable, - ATTR_TIMETABLE: self._timetable_to_attribute(), - } - - @property - def unique_id(self) -> typing.Optional[str]: - """Return unique id of the entity.""" - return self._config[CONF_ID] - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if state and state.attributes.get(ATTR_TIMETABLE): - self._timetable_from_attribute(state.attributes[ATTR_TIMETABLE]) - self._update_state() - - def _timetable_to_attribute(self): - return [ - {ATTR_TIME: event.time.isoformat(), ATTR_STATE: event.state.value} - for event in self._timetable - ] - - def _timetable_from_attribute(self, timetable): - self._timetable = [ - StateEvent( - datetime.time.fromisoformat(event[ATTR_TIME]).replace( - microsecond=0, tzinfo=None - ), - StateType(event[ATTR_STATE]), - ) - for event in timetable - ] - self._sort_timetable() - - def _sort_timetable(self): - self._timetable.sort(key=lambda event: event.time) - - async def async_set(self, time, state): - """Add an state change event to the timetable.""" - time = time.replace(microsecond=0, tzinfo=None) - for event in self._timetable: - if event.time == time: - event.state = state - break - else: - self._timetable.append(StateEvent(time, state)) - self._sort_timetable() - self._update_state() - - async def async_unset(self, time): - """Remove a state change event.""" - time = time.replace(microsecond=0, tzinfo=None) - for event in self._timetable: - if event.time == time: - self._timetable.remove(event) - break - self._update_state() - - async def async_reset(self): - """Remove all state changes.""" - self._timetable.clear() - self._update_state() - - async def async_reconfig(self, timetable): - """Override the timetable with the new list.""" - self._timetable = [ - StateEvent( - event[ATTR_TIME].replace(microsecond=0, tzinfo=None), event[ATTR_STATE] - ) - for event in timetable - ] - self._sort_timetable() - self._update_state() - - async def async_update_config(self, config: typing.Dict) -> None: - """Handle when the config is updated.""" - self._config = config - self._update_state() - - def _schedule_update(self): - if self._event_unsub: - self._event_unsub() - self._event_unsub = None - - if not self._timetable or len(self._timetable) == 1: - return - - now = dt_util.now() - time = now.time() - today = now.date() - prev = MIDNIGHT - for event in self._timetable: - if prev <= time < event.time: - next_change = datetime.datetime.combine( - today, - event.time, - ) - break - prev = event.time - else: - next_change = datetime.datetime.combine( - today + datetime.timedelta(days=1), - self._timetable[0].time, - ) - - self._event_unsub = event_helper.async_track_point_in_time( - self.hass, self._update_state, next_change - ) - - @callback - def _update_state(self, now=None): - """Update the state to reflect the current time.""" - self._schedule_update() - self.async_write_ha_state() - - -class StateEvent: - """State event properties (time, and state value).""" - - def __init__(self, time, state): - """Initialize the object.""" - self.time = time - self.state = state diff --git a/homeassistant/components/input_timetable/manifest.json b/homeassistant/components/input_timetable/manifest.json deleted file mode 100644 index 8a5cc7b910541a..00000000000000 --- a/homeassistant/components/input_timetable/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "input_timetable", - "name": "Input Timetable", - "documentation": "https://www.home-assistant.io/integrations/input_timetable", - "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" -} diff --git a/homeassistant/components/input_timetable/services.yaml b/homeassistant/components/input_timetable/services.yaml deleted file mode 100644 index c2204e274b91d2..00000000000000 --- a/homeassistant/components/input_timetable/services.yaml +++ /dev/null @@ -1,39 +0,0 @@ -set: - description: Adds a state change at the specified time. - fields: - entity_id: - description: Entity id of the input timetable that should be set. - example: input_timetable.lights - time: - description: The time of the change. - example: '"05:04:20"' - state: - description: The new state (on or off). - example: "on" -unset: - description: Removes a state change. - fields: - entity_id: - description: Entity id of the input timetable that should be reset. - example: input_timetable.lights - time: - description: The time of the state change to remove. - example: '"05:04:20"' -reset: - description: Removes all state changes. - fields: - entity_id: - description: Entity id of the input timetable that should be reset. - example: input_timetable.lights -reconfig: - description: Overrides the timetable with the provided list. - fields: - entity_id: - description: Entity id of the input timetable that should be set. - example: input_timetable.lights - timetable: - description: List of state changes. - example: - [{ time: "01:02:03", state: "on" }, { time: "04:05:06", state: "off" }] -reload: - description: Reload the input_timetable configuration. diff --git a/homeassistant/components/input_timetable/strings.json b/homeassistant/components/input_timetable/strings.json deleted file mode 100644 index 9eca15963ace7e..00000000000000 --- a/homeassistant/components/input_timetable/strings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Input timetable", - "state": { - "_": { - "off": "[%key:common::state::off%]", - "on": "[%key:common::state::on%]" - } - } -} diff --git a/homeassistant/components/input_timetable/translations/en.json b/homeassistant/components/input_timetable/translations/en.json deleted file mode 100644 index 6e37a00a85ea24..00000000000000 --- a/homeassistant/components/input_timetable/translations/en.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "state": { - "_": { - "off": "Off", - "on": "On" - } - }, - "title": "Input timetable" -} \ No newline at end of file diff --git a/tests/components/input_timetable/__init__.py b/tests/components/input_timetable/__init__.py deleted file mode 100644 index f77a4d71049c5b..00000000000000 --- a/tests/components/input_timetable/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the input_timetable component.""" diff --git a/tests/components/input_timetable/test_init.py b/tests/components/input_timetable/test_init.py deleted file mode 100644 index 7824cb4cf75206..00000000000000 --- a/tests/components/input_timetable/test_init.py +++ /dev/null @@ -1,557 +0,0 @@ -"""The tests for the input timetable component.""" -import datetime - -import pytest - -from homeassistant.components.input_timetable import ( - ATTR_STATE, - ATTR_TIME, - ATTR_TIMETABLE, - DOMAIN, - SERVICE_RECONFIG, - SERVICE_RELOAD, - SERVICE_RESET, - SERVICE_SET, - SERVICE_UNSET, -) -from homeassistant.const import ( - ATTR_EDITABLE, - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - ATTR_NAME, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import Context, CoreState, State -from homeassistant.exceptions import Unauthorized -from homeassistant.helpers import entity_registry -from homeassistant.setup import async_setup_component - -# pylint: disable=protected-access -from tests.async_mock import patch -from tests.common import async_fire_time_changed, mock_restore_cache - - -@pytest.fixture(name="storage_setup") -def storage_setup_fixture(hass, hass_storage): - """Storage setup.""" - - async def _storage(items=None, config=None): - if items is None: - hass_storage[DOMAIN] = { - "key": DOMAIN, - "version": 1, - "data": { - "items": [ - { - "id": "from_storage", - "name": "from storage", - } - ] - }, - } - else: - hass_storage[DOMAIN] = { - "key": DOMAIN, - "version": 1, - "data": {"items": items}, - } - if config is None: - config = {DOMAIN: {}} - return await async_setup_component(hass, DOMAIN, config) - - return _storage - - -async def call_set(hass, entity_id, time, state): - """Add a state change event.""" - await hass.services.async_call( - DOMAIN, - SERVICE_SET, - {ATTR_ENTITY_ID: entity_id, ATTR_TIME: time, ATTR_STATE: state}, - blocking=True, - ) - - -async def call_unset(hass, entity_id, time): - """Remove a state change.""" - await hass.services.async_call( - DOMAIN, - SERVICE_UNSET, - {ATTR_ENTITY_ID: entity_id, ATTR_TIME: time}, - blocking=True, - ) - - -async def call_reset(hass, entity_id): - """Remove all state changes.""" - await hass.services.async_call( - DOMAIN, - SERVICE_RESET, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - -async def call_reconfig(hass, entity_id, config): - """Override the timetable with the new list.""" - await hass.services.async_call( - DOMAIN, - SERVICE_RECONFIG, - {ATTR_ENTITY_ID: entity_id, ATTR_TIMETABLE: config}, - blocking=True, - ) - - -async def test_load_from_storage(hass, storage_setup): - """Test set up from storage.""" - assert await storage_setup() - - state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" - - -async def test_editable_state_attribute(hass, storage_setup): - """Test editable attribute.""" - assert await storage_setup(config={DOMAIN: {"from_yaml": {ATTR_NAME: "from yaml"}}}) - - state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" - assert state.attributes[ATTR_EDITABLE] - - state = hass.states.get(f"{DOMAIN}.from_yaml") - assert state.attributes[ATTR_FRIENDLY_NAME] == "from yaml" - assert not state.attributes[ATTR_EDITABLE] - - -@pytest.mark.parametrize( - ["config", "timetable"], - [ - ( - [("01:02:03", STATE_ON)], - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_ON, - }, - ], - ), - ( - [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_OFF, - }, - { - ATTR_TIME: "04:05:06", - ATTR_STATE: STATE_ON, - }, - ], - ), - ( - [("01:02:03", STATE_ON), ("01:02:03", STATE_OFF)], - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_OFF, - }, - ], - ), - ], - ids=[ - "single", - "multiple", - "override", - ], -) -async def test_set(hass, config, timetable): - """Test set method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - for event in config: - time = datetime.time.fromisoformat(event[0]) - state = event[1] - await call_set(hass, entity_id, time, state) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_TIMETABLE] == timetable - - -@pytest.mark.parametrize( - ["config", "unset", "timetable"], - [ - ( - [("01:02:03", STATE_ON)], - "01:02:03", - [], - ), - ( - [("04:05:06", STATE_ON), ("01:02:03", STATE_OFF)], - "04:05:06", - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_OFF, - }, - ], - ), - ( - [("01:02:03", STATE_ON)], - "04:05:06", - [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_ON, - }, - ], - ), - ], - ids=[ - "single", - "multiple", - "none", - ], -) -async def test_unset(hass, config, unset, timetable): - """Test unset method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - for event in config: - time = datetime.time.fromisoformat(event[0]) - state = event[1] - await call_set(hass, entity_id, time, state) - time = datetime.time.fromisoformat(unset) - await call_unset(hass, entity_id, time) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_TIMETABLE] == timetable - - -async def test_reset(hass, caplog): - """Test reset method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - await call_set(hass, entity_id, "01:02:03", STATE_ON) - await call_set(hass, entity_id, "04:05:06", STATE_OFF) - await call_set(hass, entity_id, "07:08:09", STATE_ON) - await call_set(hass, entity_id, "10:11:12", STATE_OFF) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 4 - await call_reset(hass, entity_id) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 0 - - -async def test_reconfig(hass, caplog): - """Test reconfig method.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - await call_set(hass, entity_id, "01:02:03", STATE_ON) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 1 - await call_reconfig( - hass, - entity_id, - [ - {ATTR_TIME: "07:08:09", ATTR_STATE: STATE_OFF}, - {ATTR_TIME: "04:05:06", ATTR_STATE: STATE_ON}, - ], - ) - assert len(hass.states.get(entity_id).attributes[ATTR_TIMETABLE]) == 2 - assert ( - hass.states.get(entity_id).attributes[ATTR_TIMETABLE][0][ATTR_TIME] - == "04:05:06" - ) - - -@patch("homeassistant.util.dt.now") -async def test_state(mock_now, hass): - """Test state attribute.""" - mock_now.return_value = datetime.datetime.fromisoformat("2000-01-01 23:50:01") - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = "input_timetable.test" - await call_reconfig( - hass, - entity_id, - [ - {ATTR_TIME: "23:50:00", ATTR_STATE: STATE_ON}, - {ATTR_TIME: "23:55:00", ATTR_STATE: STATE_OFF}, - {ATTR_TIME: "00:00:00", ATTR_STATE: STATE_ON}, - {ATTR_TIME: "00:05:00", ATTR_STATE: STATE_OFF}, - ], - ) - assert hass.states.get(entity_id).state == STATE_ON - - state = STATE_OFF - for _ in range(3): - mock_now.return_value += datetime.timedelta(minutes=5) - async_fire_time_changed(hass, mock_now.return_value) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == state - state = STATE_ON if state == STATE_OFF else STATE_OFF - - -@patch("homeassistant.util.dt.now") -@patch("homeassistant.helpers.event.async_track_point_in_time") -async def test_state_update(async_track_point_in_time, mock_now, hass): - """Test next update time.""" - mock_now.return_value = datetime.datetime.fromisoformat("2000-01-01") - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}}) - entity_id = f"{DOMAIN}.test" - - in_5_minutes = mock_now.return_value + datetime.timedelta(minutes=5) - in_10_minutes = mock_now.return_value + datetime.timedelta(minutes=10) - previous_5_minutes = mock_now.return_value + datetime.timedelta(minutes=-5) - previous_10_minutes = mock_now.return_value + datetime.timedelta(minutes=-10) - - # No events => no updates. - assert async_track_point_in_time.call_count == 0 - - # One event => no updates. - await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) - assert async_track_point_in_time.call_count == 0 - await call_reset(hass, entity_id) - - # Between 2 events. - await call_set(hass, entity_id, previous_5_minutes.time(), STATE_ON) - await call_set(hass, entity_id, in_5_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes - await call_reset(hass, entity_id) - - # After any event. - await call_set(hass, entity_id, previous_10_minutes.time(), STATE_ON) - await call_set(hass, entity_id, previous_5_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == previous_10_minutes + datetime.timedelta(days=1) - await call_reset(hass, entity_id) - - # Before any event. - await call_set(hass, entity_id, in_5_minutes.time(), STATE_ON) - await call_set(hass, entity_id, in_10_minutes.time(), STATE_OFF) - next_update = async_track_point_in_time.call_args[0][2] - assert next_update == in_5_minutes - await call_reset(hass, entity_id) - - -async def test_restore_state(hass): - """Ensure states are restored on startup.""" - a_timetable = [ - { - ATTR_TIME: "01:02:03", - ATTR_STATE: STATE_ON, - }, - ] - b_timetable = [ - { - ATTR_TIME: "07:08:09", - ATTR_STATE: STATE_OFF, - }, - ] - mock_restore_cache( - hass, - ( - State("input_timetable.a", "", {ATTR_TIMETABLE: a_timetable}), - State("input_timetable.b", "", {ATTR_TIMETABLE: b_timetable}), - ), - ) - - hass.state = CoreState.starting - - await async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"a": {}, "b": {}}}, - ) - - state = hass.states.get("input_timetable.a") - assert state - assert state.attributes[ATTR_TIMETABLE] == a_timetable - - state = hass.states.get("input_timetable.b") - assert state - assert state.attributes[ATTR_TIMETABLE] == b_timetable - - -async def test_input_scheudle_context(hass, hass_admin_user): - """Test that input_timetable context works.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"x": {}}}) - - state = hass.states.get(f"{DOMAIN}.x") - assert state is not None - - await hass.services.async_call( - DOMAIN, - SERVICE_SET, - { - ATTR_ENTITY_ID: state.entity_id, - ATTR_TIME: datetime.time.fromisoformat("01:02:03"), - ATTR_STATE: STATE_ON, - }, - True, - Context(user_id=hass_admin_user.id), - ) - - state2 = hass.states.get(f"{DOMAIN}.x") - assert state2 is not None - assert state.attributes[ATTR_TIMETABLE] != state2.attributes[ATTR_TIMETABLE] - assert state2.context.user_id == hass_admin_user.id - - -async def test_reload(hass, hass_admin_user, hass_read_only_user): - """Test reload service.""" - count_start = len(hass.states.async_entity_ids()) - ent_reg = await entity_registry.async_get_registry(hass) - - assert await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "test_1": {ATTR_NAME: "before"}, - "test_3": {}, - } - }, - ) - - assert count_start + 2 == len(hass.states.async_entity_ids()) - - state_1 = hass.states.get(f"{DOMAIN}.test_1") - state_2 = hass.states.get(f"{DOMAIN}.test_2") - state_3 = hass.states.get(f"{DOMAIN}.test_3") - - assert state_1 is not None - assert state_1.attributes[ATTR_FRIENDLY_NAME] == "before" - assert state_2 is None - assert state_3 is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None - - with patch( - "homeassistant.config.load_yaml_config_file", - autospec=True, - return_value={ - DOMAIN: { - "test_1": {ATTR_NAME: "after"}, - "test_2": {}, - } - }, - ): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_admin_user.id), - ) - await hass.async_block_till_done() - - assert count_start + 2 == len(hass.states.async_entity_ids()) - - state_1 = hass.states.get(f"{DOMAIN}.test_1") - assert state_1.attributes[ATTR_FRIENDLY_NAME] == "after" - state_2 = hass.states.get(f"{DOMAIN}.test_2") - state_3 = hass.states.get(f"{DOMAIN}.test_3") - - assert state_1 is not None - assert state_2 is not None - assert state_3 is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None - - -async def test_setup_no_config(hass, hass_admin_user): - """Test component setup with no config.""" - count_start = len(hass.states.async_entity_ids()) - assert await async_setup_component(hass, DOMAIN, {}) - - with patch( - "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} - ): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_admin_user.id), - ) - await hass.async_block_till_done() - - assert count_start == len(hass.states.async_entity_ids()) - - -async def test_ws_list(hass, hass_ws_client, storage_setup): - """Test listing via WS.""" - assert await storage_setup(config={DOMAIN: {"from_yaml": {}}}) - - client = await hass_ws_client(hass) - - await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) - resp = await client.receive_json() - assert resp["success"] - - storage_ent = "from_storage" - yaml_ent = "from_yaml" - result = {item["id"]: item for item in resp["result"]} - - assert len(result) == 1 - assert storage_ent in result - assert yaml_ent not in result - assert result[storage_ent][ATTR_NAME] == "from storage" - - -async def test_ws_delete(hass, hass_ws_client, storage_setup): - """Test WS delete cleans up entity registry.""" - assert await storage_setup() - - input_id = "from_storage" - input_entity_id = f"{DOMAIN}.{input_id}" - ent_reg = await entity_registry.async_get_registry(hass) - - state = hass.states.get(input_entity_id) - assert state is not None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None - - client = await hass_ws_client(hass) - - await client.send_json( - {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} - ) - resp = await client.receive_json() - assert resp["success"] - - state = hass.states.get(input_entity_id) - assert state is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None - - -async def test_ws_create(hass, hass_ws_client, storage_setup): - """Test create WS.""" - assert await storage_setup(items=[]) - - input_id = "new_input" - input_entity_id = f"{DOMAIN}.{input_id}" - ent_reg = await entity_registry.async_get_registry(hass) - - state = hass.states.get(input_entity_id) - assert state is None - assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None - - client = await hass_ws_client(hass) - - await client.send_json( - { - "id": 6, - "type": f"{DOMAIN}/create", - "name": "New Input", - } - ) - resp = await client.receive_json() - assert resp["success"] - - state = hass.states.get(input_entity_id) - assert state.state == STATE_OFF