mirror of
https://github.com/home-assistant/core.git
synced 2025-11-12 12:30:31 +00:00
Compare commits
5 Commits
copilot/ad
...
input-week
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39d970347e | ||
|
|
9cccc96f63 | ||
|
|
a32ada3155 | ||
|
|
77f078e57d | ||
|
|
8657bfd0bf |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -753,6 +753,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/input_select/ @home-assistant/core
|
/tests/components/input_select/ @home-assistant/core
|
||||||
/homeassistant/components/input_text/ @home-assistant/core
|
/homeassistant/components/input_text/ @home-assistant/core
|
||||||
/tests/components/input_text/ @home-assistant/core
|
/tests/components/input_text/ @home-assistant/core
|
||||||
|
/homeassistant/components/input_weekday/ @home-assistant/core
|
||||||
|
/tests/components/input_weekday/ @home-assistant/core
|
||||||
/homeassistant/components/insteon/ @teharris1
|
/homeassistant/components/insteon/ @teharris1
|
||||||
/tests/components/insteon/ @teharris1
|
/tests/components/insteon/ @teharris1
|
||||||
/homeassistant/components/integration/ @dgomes
|
/homeassistant/components/integration/ @dgomes
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ DEFAULT_INTEGRATIONS = {
|
|||||||
"input_datetime",
|
"input_datetime",
|
||||||
"input_number",
|
"input_number",
|
||||||
"input_select",
|
"input_select",
|
||||||
|
"input_weekday",
|
||||||
"input_text",
|
"input_text",
|
||||||
"schedule",
|
"schedule",
|
||||||
"timer",
|
"timer",
|
||||||
|
|||||||
@@ -72,15 +72,21 @@ _TIME_TRIGGER_SCHEMA = vol.Any(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_WEEKDAY_SCHEMA = vol.Any(
|
||||||
|
vol.In(WEEKDAYS),
|
||||||
|
vol.All(cv.ensure_list, [vol.In(WEEKDAYS)]),
|
||||||
|
cv.entity_domain(["input_weekday"]),
|
||||||
|
msg=(
|
||||||
|
"Expected a weekday (mon, tue, wed, thu, fri, sat, sun), "
|
||||||
|
"a list of weekdays, or an Entity ID with domain 'input_weekday'"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PLATFORM): "time",
|
vol.Required(CONF_PLATFORM): "time",
|
||||||
vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
|
vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
|
||||||
vol.Optional(CONF_WEEKDAY): vol.Any(
|
vol.Optional(CONF_WEEKDAY): _WEEKDAY_SCHEMA,
|
||||||
vol.In(WEEKDAYS),
|
|
||||||
vol.All(cv.ensure_list, [vol.In(WEEKDAYS)]),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,7 +123,14 @@ async def async_attach_trigger( # noqa: C901
|
|||||||
|
|
||||||
# Check if current weekday matches the configuration
|
# Check if current weekday matches the configuration
|
||||||
if isinstance(weekday_config, str):
|
if isinstance(weekday_config, str):
|
||||||
if current_weekday != weekday_config:
|
# Could be a single weekday string or an entity_id
|
||||||
|
if weekday_config.startswith("input_weekday."):
|
||||||
|
if (weekday_state := hass.states.get(weekday_config)) is None:
|
||||||
|
return
|
||||||
|
entity_weekdays = weekday_state.attributes.get("weekdays", [])
|
||||||
|
if current_weekday not in entity_weekdays:
|
||||||
|
return
|
||||||
|
elif current_weekday != weekday_config:
|
||||||
return
|
return
|
||||||
elif current_weekday not in weekday_config:
|
elif current_weekday not in weekday_config:
|
||||||
return
|
return
|
||||||
|
|||||||
285
homeassistant/components/input_weekday/__init__.py
Normal file
285
homeassistant/components/input_weekday/__init__.py
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
"""Support to select weekdays for use in automation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Self
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_EDITABLE,
|
||||||
|
CONF_ICON,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_NAME,
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
WEEKDAYS,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
|
from homeassistant.helpers import collection, config_validation as cv
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
import homeassistant.helpers.service
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = "input_weekday"
|
||||||
|
|
||||||
|
CONF_WEEKDAYS = "weekdays"
|
||||||
|
|
||||||
|
ATTR_WEEKDAYS = "weekdays"
|
||||||
|
ATTR_WEEKDAY = "weekday"
|
||||||
|
|
||||||
|
SERVICE_SET_WEEKDAYS = "set_weekdays"
|
||||||
|
SERVICE_ADD_WEEKDAY = "add_weekday"
|
||||||
|
SERVICE_REMOVE_WEEKDAY = "remove_weekday"
|
||||||
|
SERVICE_TOGGLE_WEEKDAY = "toggle_weekday"
|
||||||
|
SERVICE_CLEAR = "clear"
|
||||||
|
|
||||||
|
STORAGE_KEY = DOMAIN
|
||||||
|
STORAGE_VERSION = 1
|
||||||
|
|
||||||
|
STORAGE_FIELDS: VolDictType = {
|
||||||
|
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||||
|
vol.Optional(CONF_WEEKDAYS, default=list): vol.All(
|
||||||
|
cv.ensure_list, [vol.In(WEEKDAYS)]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _cv_input_weekday(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Configure validation helper for input weekday (voluptuous)."""
|
||||||
|
if CONF_WEEKDAYS in cfg:
|
||||||
|
weekdays = cfg[CONF_WEEKDAYS]
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
cfg[CONF_WEEKDAYS] = list(dict.fromkeys(weekdays))
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: cv.schema_with_slug_keys(
|
||||||
|
vol.All(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_WEEKDAYS): vol.All(
|
||||||
|
cv.ensure_list, [vol.In(WEEKDAYS)]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
|
},
|
||||||
|
_cv_input_weekday,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up an input weekday."""
|
||||||
|
component = EntityComponent[InputWeekday](_LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
|
id_manager = collection.IDManager()
|
||||||
|
|
||||||
|
yaml_collection = collection.YamlCollection(
|
||||||
|
logging.getLogger(f"{__name__}.yaml_collection"), id_manager
|
||||||
|
)
|
||||||
|
collection.sync_entity_lifecycle(
|
||||||
|
hass, DOMAIN, DOMAIN, component, yaml_collection, InputWeekday
|
||||||
|
)
|
||||||
|
|
||||||
|
storage_collection = InputWeekdayStorageCollection(
|
||||||
|
Store(hass, STORAGE_VERSION, STORAGE_KEY),
|
||||||
|
id_manager,
|
||||||
|
)
|
||||||
|
collection.sync_entity_lifecycle(
|
||||||
|
hass, DOMAIN, DOMAIN, component, storage_collection, InputWeekday
|
||||||
|
)
|
||||||
|
|
||||||
|
await yaml_collection.async_load(
|
||||||
|
[{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()]
|
||||||
|
)
|
||||||
|
await storage_collection.async_load()
|
||||||
|
|
||||||
|
collection.DictStorageCollectionWebsocket(
|
||||||
|
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||||
|
).async_setup(hass)
|
||||||
|
|
||||||
|
async def reload_service_handler(service_call: ServiceCall) -> 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_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()]
|
||||||
|
)
|
||||||
|
|
||||||
|
homeassistant.helpers.service.async_register_admin_service(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
reload_service_handler,
|
||||||
|
schema=RELOAD_SERVICE_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_SET_WEEKDAYS,
|
||||||
|
{vol.Required(ATTR_WEEKDAYS): vol.All(cv.ensure_list, [vol.In(WEEKDAYS)])},
|
||||||
|
"async_set_weekdays",
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_ADD_WEEKDAY,
|
||||||
|
{vol.Required(ATTR_WEEKDAY): vol.In(WEEKDAYS)},
|
||||||
|
"async_add_weekday",
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_REMOVE_WEEKDAY,
|
||||||
|
{vol.Required(ATTR_WEEKDAY): vol.In(WEEKDAYS)},
|
||||||
|
"async_remove_weekday",
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_TOGGLE_WEEKDAY,
|
||||||
|
{vol.Required(ATTR_WEEKDAY): vol.In(WEEKDAYS)},
|
||||||
|
"async_toggle_weekday",
|
||||||
|
)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_CLEAR,
|
||||||
|
None,
|
||||||
|
"async_clear",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class InputWeekdayStorageCollection(collection.DictStorageCollection):
|
||||||
|
"""Input weekday storage based collection."""
|
||||||
|
|
||||||
|
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_weekday))
|
||||||
|
|
||||||
|
async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Validate the config is valid."""
|
||||||
|
return self.CREATE_UPDATE_SCHEMA(data)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _get_suggested_id(self, info: dict[str, Any]) -> str:
|
||||||
|
"""Suggest an ID based on the config."""
|
||||||
|
return info[CONF_NAME]
|
||||||
|
|
||||||
|
async def _update_data(
|
||||||
|
self, item: dict[str, Any], update_data: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return a new updated data object."""
|
||||||
|
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||||
|
return item | update_data
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable-next=hass-enforce-class-module
|
||||||
|
class InputWeekday(collection.CollectionEntity, RestoreEntity):
|
||||||
|
"""Representation of a weekday input."""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset({ATTR_EDITABLE})
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
editable: bool
|
||||||
|
|
||||||
|
def __init__(self, config: ConfigType) -> None:
|
||||||
|
"""Initialize a weekday input."""
|
||||||
|
self._config = config
|
||||||
|
self._attr_weekdays = config.get(CONF_WEEKDAYS, [])
|
||||||
|
self._attr_unique_id = config[CONF_ID]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_storage(cls, config: ConfigType) -> Self:
|
||||||
|
"""Return entity instance initialized from storage."""
|
||||||
|
input_weekday = cls(config)
|
||||||
|
input_weekday.editable = True
|
||||||
|
return input_weekday
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_yaml(cls, config: ConfigType) -> Self:
|
||||||
|
"""Return entity instance initialized from yaml."""
|
||||||
|
input_weekday = cls(config)
|
||||||
|
input_weekday.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
|
||||||
|
input_weekday.editable = False
|
||||||
|
return input_weekday
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return name of the weekday input."""
|
||||||
|
return self._config.get(CONF_NAME) or self._config[CONF_ID]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str | None:
|
||||||
|
"""Return the icon to be used for this entity."""
|
||||||
|
return self._config.get(CONF_ICON)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
# Return a comma-separated string of selected weekdays
|
||||||
|
return ",".join(self._attr_weekdays) if self._attr_weekdays else ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
|
"""Return the state attributes of the entity."""
|
||||||
|
return {
|
||||||
|
ATTR_WEEKDAYS: self._attr_weekdays,
|
||||||
|
ATTR_EDITABLE: self.editable,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Call when entity about to be added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
# Restore previous state if no initial weekdays were provided
|
||||||
|
if self._config.get(CONF_WEEKDAYS) is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
state = await self.async_get_last_state()
|
||||||
|
if state is not None and ATTR_WEEKDAYS in state.attributes:
|
||||||
|
self._attr_weekdays = state.attributes[ATTR_WEEKDAYS]
|
||||||
|
|
||||||
|
async def async_set_weekdays(self, weekdays: list[str]) -> None:
|
||||||
|
"""Set the selected weekdays."""
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
self._attr_weekdays = list(dict.fromkeys(weekdays))
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_add_weekday(self, weekday: str) -> None:
|
||||||
|
"""Add a weekday to the selection."""
|
||||||
|
if weekday not in self._attr_weekdays:
|
||||||
|
self._attr_weekdays.append(weekday)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_remove_weekday(self, weekday: str) -> None:
|
||||||
|
"""Remove a weekday from the selection."""
|
||||||
|
if weekday in self._attr_weekdays:
|
||||||
|
self._attr_weekdays.remove(weekday)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_toggle_weekday(self, weekday: str) -> None:
|
||||||
|
"""Toggle a weekday in the selection."""
|
||||||
|
if weekday in self._attr_weekdays:
|
||||||
|
self._attr_weekdays.remove(weekday)
|
||||||
|
else:
|
||||||
|
self._attr_weekdays.append(weekday)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_clear(self) -> None:
|
||||||
|
"""Clear all selected weekdays."""
|
||||||
|
self._attr_weekdays = []
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_update_config(self, config: ConfigType) -> None:
|
||||||
|
"""Handle when the config is updated."""
|
||||||
|
self._config = config
|
||||||
|
self._attr_weekdays = config.get(CONF_WEEKDAYS, [])
|
||||||
|
self.async_write_ha_state()
|
||||||
29
homeassistant/components/input_weekday/icons.json
Normal file
29
homeassistant/components/input_weekday/icons.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"input_weekday": {
|
||||||
|
"default": {
|
||||||
|
"default": "mdi:calendar-week"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"set_weekdays": {
|
||||||
|
"service": "mdi:calendar-edit"
|
||||||
|
},
|
||||||
|
"add_weekday": {
|
||||||
|
"service": "mdi:calendar-plus"
|
||||||
|
},
|
||||||
|
"remove_weekday": {
|
||||||
|
"service": "mdi:calendar-minus"
|
||||||
|
},
|
||||||
|
"toggle_weekday": {
|
||||||
|
"service": "mdi:calendar-check"
|
||||||
|
},
|
||||||
|
"clear": {
|
||||||
|
"service": "mdi:calendar-remove"
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"service": "mdi:reload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
homeassistant/components/input_weekday/manifest.json
Normal file
8
homeassistant/components/input_weekday/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"domain": "input_weekday",
|
||||||
|
"name": "Input Weekday",
|
||||||
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/input_weekday",
|
||||||
|
"integration_type": "helper",
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
||||||
42
homeassistant/components/input_weekday/reproduce_state.py
Normal file
42
homeassistant/components/input_weekday/reproduce_state.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Reproduce an Input Weekday state."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
from homeassistant.core import Context, HomeAssistant, State
|
||||||
|
|
||||||
|
from . import ATTR_WEEKDAYS, DOMAIN, SERVICE_SET_WEEKDAYS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_reproduce_states(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
states: list[State],
|
||||||
|
*,
|
||||||
|
context: Context | None = None,
|
||||||
|
reproduce_options: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Reproduce Input Weekday states."""
|
||||||
|
for state in states:
|
||||||
|
if ATTR_WEEKDAYS not in state.attributes:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to reproduce state for %s: %s attribute is missing",
|
||||||
|
state.entity_id,
|
||||||
|
ATTR_WEEKDAYS,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
weekdays = state.attributes[ATTR_WEEKDAYS]
|
||||||
|
|
||||||
|
service_data = {
|
||||||
|
ATTR_ENTITY_ID: state.entity_id,
|
||||||
|
ATTR_WEEKDAYS: weekdays,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_SET_WEEKDAYS, service_data, context=context, blocking=True
|
||||||
|
)
|
||||||
115
homeassistant/components/input_weekday/services.yaml
Normal file
115
homeassistant/components/input_weekday/services.yaml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
set_weekdays:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: input_weekday
|
||||||
|
fields:
|
||||||
|
weekdays:
|
||||||
|
required: true
|
||||||
|
example: '["mon", "wed", "fri"]'
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
multiple: true
|
||||||
|
mode: list
|
||||||
|
options:
|
||||||
|
- value: mon
|
||||||
|
label: Monday
|
||||||
|
- value: tue
|
||||||
|
label: Tuesday
|
||||||
|
- value: wed
|
||||||
|
label: Wednesday
|
||||||
|
- value: thu
|
||||||
|
label: Thursday
|
||||||
|
- value: fri
|
||||||
|
label: Friday
|
||||||
|
- value: sat
|
||||||
|
label: Saturday
|
||||||
|
- value: sun
|
||||||
|
label: Sunday
|
||||||
|
|
||||||
|
add_weekday:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: input_weekday
|
||||||
|
fields:
|
||||||
|
weekday:
|
||||||
|
required: true
|
||||||
|
example: mon
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- value: mon
|
||||||
|
label: Monday
|
||||||
|
- value: tue
|
||||||
|
label: Tuesday
|
||||||
|
- value: wed
|
||||||
|
label: Wednesday
|
||||||
|
- value: thu
|
||||||
|
label: Thursday
|
||||||
|
- value: fri
|
||||||
|
label: Friday
|
||||||
|
- value: sat
|
||||||
|
label: Saturday
|
||||||
|
- value: sun
|
||||||
|
label: Sunday
|
||||||
|
|
||||||
|
remove_weekday:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: input_weekday
|
||||||
|
fields:
|
||||||
|
weekday:
|
||||||
|
required: true
|
||||||
|
example: mon
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- value: mon
|
||||||
|
label: Monday
|
||||||
|
- value: tue
|
||||||
|
label: Tuesday
|
||||||
|
- value: wed
|
||||||
|
label: Wednesday
|
||||||
|
- value: thu
|
||||||
|
label: Thursday
|
||||||
|
- value: fri
|
||||||
|
label: Friday
|
||||||
|
- value: sat
|
||||||
|
label: Saturday
|
||||||
|
- value: sun
|
||||||
|
label: Sunday
|
||||||
|
|
||||||
|
toggle_weekday:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: input_weekday
|
||||||
|
fields:
|
||||||
|
weekday:
|
||||||
|
required: true
|
||||||
|
example: mon
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- value: mon
|
||||||
|
label: Monday
|
||||||
|
- value: tue
|
||||||
|
label: Tuesday
|
||||||
|
- value: wed
|
||||||
|
label: Wednesday
|
||||||
|
- value: thu
|
||||||
|
label: Thursday
|
||||||
|
- value: fri
|
||||||
|
label: Friday
|
||||||
|
- value: sat
|
||||||
|
label: Saturday
|
||||||
|
- value: sun
|
||||||
|
label: Sunday
|
||||||
|
|
||||||
|
clear:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: input_weekday
|
||||||
|
|
||||||
|
reload:
|
||||||
70
homeassistant/components/input_weekday/strings.json
Normal file
70
homeassistant/components/input_weekday/strings.json
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"title": "Input Weekday",
|
||||||
|
"entity_component": {
|
||||||
|
"_": {
|
||||||
|
"name": "[%key:component::input_weekday::title%]",
|
||||||
|
"state_attributes": {
|
||||||
|
"weekdays": {
|
||||||
|
"name": "Weekdays"
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"name": "[%key:common::generic::ui_managed%]",
|
||||||
|
"state": {
|
||||||
|
"true": "[%key:common::state::yes%]",
|
||||||
|
"false": "[%key:common::state::no%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"set_weekdays": {
|
||||||
|
"name": "Set weekdays",
|
||||||
|
"description": "Sets the selected weekdays.",
|
||||||
|
"fields": {
|
||||||
|
"weekdays": {
|
||||||
|
"name": "Weekdays",
|
||||||
|
"description": "List of weekdays to select."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"add_weekday": {
|
||||||
|
"name": "Add weekday",
|
||||||
|
"description": "Adds a weekday to the selection.",
|
||||||
|
"fields": {
|
||||||
|
"weekday": {
|
||||||
|
"name": "Weekday",
|
||||||
|
"description": "Weekday to add."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remove_weekday": {
|
||||||
|
"name": "Remove weekday",
|
||||||
|
"description": "Removes a weekday from the selection.",
|
||||||
|
"fields": {
|
||||||
|
"weekday": {
|
||||||
|
"name": "Weekday",
|
||||||
|
"description": "Weekday to remove."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toggle_weekday": {
|
||||||
|
"name": "Toggle weekday",
|
||||||
|
"description": "Toggles a weekday in the selection.",
|
||||||
|
"fields": {
|
||||||
|
"weekday": {
|
||||||
|
"name": "Weekday",
|
||||||
|
"description": "Weekday to toggle."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clear": {
|
||||||
|
"name": "Clear",
|
||||||
|
"description": "Clears all selected weekdays."
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "[%key:common::action::reload%]",
|
||||||
|
"description": "Reloads helpers from the YAML-configuration."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7922,6 +7922,10 @@
|
|||||||
"integration_type": "helper",
|
"integration_type": "helper",
|
||||||
"config_flow": false
|
"config_flow": false
|
||||||
},
|
},
|
||||||
|
"input_weekday": {
|
||||||
|
"integration_type": "helper",
|
||||||
|
"config_flow": false
|
||||||
|
},
|
||||||
"integration": {
|
"integration": {
|
||||||
"integration_type": "helper",
|
"integration_type": "helper",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
@@ -8021,6 +8025,7 @@
|
|||||||
"input_number",
|
"input_number",
|
||||||
"input_select",
|
"input_select",
|
||||||
"input_text",
|
"input_text",
|
||||||
|
"input_weekday",
|
||||||
"integration",
|
"integration",
|
||||||
"irm_kmi",
|
"irm_kmi",
|
||||||
"islamic_prayer_times",
|
"islamic_prayer_times",
|
||||||
|
|||||||
@@ -954,6 +954,20 @@ def time(
|
|||||||
if weekday is not None:
|
if weekday is not None:
|
||||||
now_weekday = WEEKDAYS[now.weekday()]
|
now_weekday = WEEKDAYS[now.weekday()]
|
||||||
|
|
||||||
|
# Check if weekday is an entity_id
|
||||||
|
if isinstance(weekday, str) and weekday.startswith("input_weekday."):
|
||||||
|
if (weekday_state := hass.states.get(weekday)) is None:
|
||||||
|
condition_trace_update_result(weekday=weekday, now_weekday=now_weekday)
|
||||||
|
return False
|
||||||
|
entity_weekdays = weekday_state.attributes.get("weekdays", [])
|
||||||
|
condition_trace_update_result(
|
||||||
|
weekday=weekday,
|
||||||
|
now_weekday=now_weekday,
|
||||||
|
entity_weekdays=entity_weekdays,
|
||||||
|
)
|
||||||
|
if now_weekday not in entity_weekdays:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
condition_trace_update_result(weekday=weekday, now_weekday=now_weekday)
|
condition_trace_update_result(weekday=weekday, now_weekday=now_weekday)
|
||||||
if (
|
if (
|
||||||
isinstance(weekday, str) and weekday != now_weekday
|
isinstance(weekday, str) and weekday != now_weekday
|
||||||
|
|||||||
@@ -843,7 +843,10 @@ def time_zone(value: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)])
|
weekdays = vol.Any(
|
||||||
|
vol.All(ensure_list, [vol.In(WEEKDAYS)]),
|
||||||
|
entity_domain(["input_weekday"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def socket_timeout(value: Any | None) -> object:
|
def socket_timeout(value: Any | None) -> object:
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ NO_IOT_CLASS = [
|
|||||||
"input_number",
|
"input_number",
|
||||||
"input_select",
|
"input_select",
|
||||||
"input_text",
|
"input_text",
|
||||||
|
"input_weekday",
|
||||||
"intent_script",
|
"intent_script",
|
||||||
"intent",
|
"intent",
|
||||||
"logbook",
|
"logbook",
|
||||||
|
|||||||
@@ -2214,6 +2214,7 @@ NO_QUALITY_SCALE = [
|
|||||||
"input_number",
|
"input_number",
|
||||||
"input_select",
|
"input_select",
|
||||||
"input_text",
|
"input_text",
|
||||||
|
"input_weekday",
|
||||||
"intent_script",
|
"intent_script",
|
||||||
"intent",
|
"intent",
|
||||||
"logbook",
|
"logbook",
|
||||||
|
|||||||
@@ -1061,6 +1061,14 @@ def test_weekday_validation() -> None:
|
|||||||
}
|
}
|
||||||
time.TRIGGER_SCHEMA(valid_config)
|
time.TRIGGER_SCHEMA(valid_config)
|
||||||
|
|
||||||
|
# Valid input_weekday entity
|
||||||
|
valid_config = {
|
||||||
|
"platform": "time",
|
||||||
|
"at": "5:00:00",
|
||||||
|
"weekday": "input_weekday.workdays",
|
||||||
|
}
|
||||||
|
time.TRIGGER_SCHEMA(valid_config)
|
||||||
|
|
||||||
# Invalid weekday
|
# Invalid weekday
|
||||||
invalid_config = {"platform": "time", "at": "5:00:00", "weekday": "invalid"}
|
invalid_config = {"platform": "time", "at": "5:00:00", "weekday": "invalid"}
|
||||||
with pytest.raises(vol.Invalid):
|
with pytest.raises(vol.Invalid):
|
||||||
@@ -1074,3 +1082,176 @@ def test_weekday_validation() -> None:
|
|||||||
}
|
}
|
||||||
with pytest.raises(vol.Invalid):
|
with pytest.raises(vol.Invalid):
|
||||||
time.TRIGGER_SCHEMA(invalid_config)
|
time.TRIGGER_SCHEMA(invalid_config)
|
||||||
|
|
||||||
|
# Invalid entity domain
|
||||||
|
invalid_config = {
|
||||||
|
"platform": "time",
|
||||||
|
"at": "5:00:00",
|
||||||
|
"weekday": "input_boolean.my_bool",
|
||||||
|
}
|
||||||
|
with pytest.raises(vol.Invalid):
|
||||||
|
time.TRIGGER_SCHEMA(invalid_config)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_fires_using_weekday_input_weekday_entity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
) -> None:
|
||||||
|
"""Test for firing on weekday using input_weekday entity."""
|
||||||
|
# Setup input_weekday helper with Mon, Tue, Wed
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"input_weekday",
|
||||||
|
{
|
||||||
|
"input_weekday": {
|
||||||
|
"workdays": {
|
||||||
|
"name": "Work Days",
|
||||||
|
"weekdays": ["mon", "tue", "wed"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Freeze time to Monday, January 2, 2023 at 5:00:00
|
||||||
|
monday_trigger = dt_util.as_utc(datetime(2023, 1, 2, 5, 0, 0, 0))
|
||||||
|
freezer.move_to(monday_trigger)
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
"platform": "time",
|
||||||
|
"at": "5:00:00",
|
||||||
|
"weekday": "input_weekday.workdays",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {
|
||||||
|
"some": "{{ trigger.platform }} - {{ trigger.now.strftime('%A') }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Fire on Monday - should trigger (Monday is in workdays)
|
||||||
|
async_fire_time_changed(hass, monday_trigger + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
automation_calls = [call for call in service_calls if call.domain == "test"]
|
||||||
|
assert len(automation_calls) == 1
|
||||||
|
assert "Monday" in automation_calls[0].data["some"]
|
||||||
|
|
||||||
|
# Fire on Tuesday - should trigger (Tuesday is in workdays)
|
||||||
|
tuesday_trigger = dt_util.as_utc(datetime(2023, 1, 3, 5, 0, 0, 0))
|
||||||
|
async_fire_time_changed(hass, tuesday_trigger)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
automation_calls = [call for call in service_calls if call.domain == "test"]
|
||||||
|
assert len(automation_calls) == 2
|
||||||
|
assert "Tuesday" in automation_calls[1].data["some"]
|
||||||
|
|
||||||
|
# Fire on Thursday - should not trigger (Thursday is not in workdays)
|
||||||
|
thursday_trigger = dt_util.as_utc(datetime(2023, 1, 5, 5, 0, 0, 0))
|
||||||
|
async_fire_time_changed(hass, thursday_trigger)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
automation_calls = [call for call in service_calls if call.domain == "test"]
|
||||||
|
assert len(automation_calls) == 2
|
||||||
|
|
||||||
|
# Fire on Saturday - should not trigger (Saturday is not in workdays)
|
||||||
|
saturday_trigger = dt_util.as_utc(datetime(2023, 1, 7, 5, 0, 0, 0))
|
||||||
|
async_fire_time_changed(hass, saturday_trigger)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
automation_calls = [call for call in service_calls if call.domain == "test"]
|
||||||
|
assert len(automation_calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_action_weekday_input_weekday_entity(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> None:
|
||||||
|
"""Test time condition with input_weekday entity."""
|
||||||
|
# Setup input_weekday helper with Sat, Sun
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"input_weekday",
|
||||||
|
{
|
||||||
|
"input_weekday": {
|
||||||
|
"weekend": {"name": "Weekend Days", "weekdays": ["sat", "sun"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||||
|
"condition": {"condition": "time", "weekday": "input_weekday.weekend"},
|
||||||
|
"action": {"service": "test.automation"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
days_past_monday = dt_util.now().weekday()
|
||||||
|
monday = dt_util.now() - timedelta(days=days_past_monday)
|
||||||
|
saturday = monday + timedelta(days=5)
|
||||||
|
sunday = saturday + timedelta(days=1)
|
||||||
|
|
||||||
|
# Test on Monday - should not trigger (not in weekend)
|
||||||
|
with patch("homeassistant.helpers.condition.dt_util.now", return_value=monday):
|
||||||
|
hass.bus.async_fire("test_event")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 0
|
||||||
|
|
||||||
|
# Test on Saturday - should trigger
|
||||||
|
with patch("homeassistant.helpers.condition.dt_util.now", return_value=saturday):
|
||||||
|
hass.bus.async_fire("test_event")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
|
||||||
|
# Test on Sunday - should trigger
|
||||||
|
with patch("homeassistant.helpers.condition.dt_util.now", return_value=sunday):
|
||||||
|
hass.bus.async_fire("test_event")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_fires_weekday_entity_unavailable(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
) -> None:
|
||||||
|
"""Test that trigger does not fire when input_weekday entity is unavailable."""
|
||||||
|
# Freeze time to Monday, January 2, 2023 at 5:00:00
|
||||||
|
monday_trigger = dt_util.as_utc(datetime(2023, 1, 2, 5, 0, 0, 0))
|
||||||
|
freezer.move_to(monday_trigger)
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
"platform": "time",
|
||||||
|
"at": "5:00:00",
|
||||||
|
"weekday": "input_weekday.nonexistent",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Fire on Monday - should not trigger (entity doesn't exist)
|
||||||
|
async_fire_time_changed(hass, monday_trigger + timedelta(seconds=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
automation_calls = [call for call in service_calls if call.domain == "test"]
|
||||||
|
assert len(automation_calls) == 0
|
||||||
|
|||||||
1
tests/components/input_weekday/__init__.py
Normal file
1
tests/components/input_weekday/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Input Weekday component."""
|
||||||
518
tests/components/input_weekday/test_init.py
Normal file
518
tests/components/input_weekday/test_init.py
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
"""Tests for the Input Weekday component."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.input_weekday import (
|
||||||
|
ATTR_WEEKDAY,
|
||||||
|
ATTR_WEEKDAYS,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADD_WEEKDAY,
|
||||||
|
SERVICE_CLEAR,
|
||||||
|
SERVICE_REMOVE_WEEKDAY,
|
||||||
|
SERVICE_SET_WEEKDAYS,
|
||||||
|
SERVICE_TOGGLE_WEEKDAY,
|
||||||
|
STORAGE_VERSION,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_EDITABLE,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import mock_restore_cache
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def storage_setup(hass: HomeAssistant, hass_storage: dict[str, Any]):
|
||||||
|
"""Storage setup."""
|
||||||
|
|
||||||
|
async def _storage(items=None, config=None):
|
||||||
|
if items is None:
|
||||||
|
hass_storage[DOMAIN] = {
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": STORAGE_VERSION,
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "from_storage",
|
||||||
|
"name": "from storage",
|
||||||
|
"weekdays": ["mon", "wed", "fri"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
hass_storage[DOMAIN] = {
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": STORAGE_VERSION,
|
||||||
|
"data": {"items": items},
|
||||||
|
}
|
||||||
|
if config is None:
|
||||||
|
config = {DOMAIN: {}}
|
||||||
|
return await async_setup_component(hass, DOMAIN, config)
|
||||||
|
|
||||||
|
return _storage
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"invalid_config",
|
||||||
|
[
|
||||||
|
None,
|
||||||
|
{"name with space": None},
|
||||||
|
{"bad_weekdays": {"weekdays": ["invalid"]}},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_config(hass: HomeAssistant, invalid_config) -> None:
|
||||||
|
"""Test config."""
|
||||||
|
assert not await async_setup_component(hass, DOMAIN, {DOMAIN: invalid_config})
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_weekdays(hass: HomeAssistant) -> None:
|
||||||
|
"""Test set_weekdays service."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon", "tue"]}}},
|
||||||
|
)
|
||||||
|
entity_id = "input_weekday.test_1"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "mon,tue"
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "tue"]
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_WEEKDAYS,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAYS: ["wed", "thu", "fri"]},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "wed,thu,fri"
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["wed", "thu", "fri"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_weekdays_removes_duplicates(hass: HomeAssistant) -> None:
|
||||||
|
"""Test set_weekdays removes duplicate weekdays."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": []}}},
|
||||||
|
)
|
||||||
|
entity_id = "input_weekday.test_1"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_WEEKDAYS,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAYS: ["mon", "tue", "mon", "wed"]},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "tue", "wed"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_weekday(hass: HomeAssistant) -> None:
|
||||||
|
"""Test add_weekday service."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon"]}}},
|
||||||
|
)
|
||||||
|
entity_id = "input_weekday.test_1"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon"]
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADD_WEEKDAY,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAY: "wed"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed"]
|
||||||
|
|
||||||
|
# Adding duplicate should not add it again
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADD_WEEKDAY,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAY: "mon"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_weekday(hass: HomeAssistant) -> None:
|
||||||
|
"""Test remove_weekday service."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon", "wed", "fri"]}}},
|
||||||
|
)
|
||||||
|
entity_id = "input_weekday.test_1"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed", "fri"]
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_REMOVE_WEEKDAY,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAY: "wed"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "fri"]
|
||||||
|
|
||||||
|
# Removing non-existent weekday should not error
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_REMOVE_WEEKDAY,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAY: "wed"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "fri"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_toggle_weekday(hass: HomeAssistant) -> None:
|
||||||
|
"""Test toggle_weekday service."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon"]}}},
|
||||||
|
)
|
||||||
|
entity_id = "input_weekday.test_1"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon"]
|
||||||
|
|
||||||
|
# Toggle off (remove)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TOGGLE_WEEKDAY,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAY: "mon"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == []
|
||||||
|
|
||||||
|
# Toggle on (add)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_TOGGLE_WEEKDAY,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_WEEKDAY: "tue"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["tue"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_clear(hass: HomeAssistant) -> None:
|
||||||
|
"""Test clear service."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon", "wed", "fri"]}}},
|
||||||
|
)
|
||||||
|
entity_id = "input_weekday.test_1"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed", "fri"]
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CLEAR,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == ""
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_with_name(hass: HomeAssistant) -> None:
|
||||||
|
"""Test configuration with name."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"name": "Test Weekday", "weekdays": ["sat", "sun"]}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes[ATTR_FRIENDLY_NAME] == "Test Weekday"
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["sat", "sun"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_empty_weekdays(hass: HomeAssistant) -> None:
|
||||||
|
"""Test empty weekdays configuration."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": []}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == ""
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_default_weekdays(hass: HomeAssistant) -> None:
|
||||||
|
"""Test default weekdays (empty list)."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == ""
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_removes_duplicates(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that configuration removes duplicate weekdays."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon", "tue", "mon", "wed"]}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "tue", "wed"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reload(hass: HomeAssistant) -> None:
|
||||||
|
"""Test reload service."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon"]}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state_1 = hass.states.get("input_weekday.test_1")
|
||||||
|
state_2 = hass.states.get("input_weekday.test_2")
|
||||||
|
|
||||||
|
assert state_1 is not None
|
||||||
|
assert state_2 is None
|
||||||
|
assert state_1.attributes[ATTR_WEEKDAYS] == ["mon"]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config.load_yaml_config_file",
|
||||||
|
return_value={
|
||||||
|
DOMAIN: {
|
||||||
|
"test_2": {"weekdays": ["tue", "thu"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state_1 = hass.states.get("input_weekday.test_1")
|
||||||
|
state_2 = hass.states.get("input_weekday.test_2")
|
||||||
|
|
||||||
|
assert state_1 is None
|
||||||
|
assert state_2 is not None
|
||||||
|
assert state_2.attributes[ATTR_WEEKDAYS] == ["tue", "thu"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_state_restoration(hass: HomeAssistant) -> None:
|
||||||
|
"""Test state restoration."""
|
||||||
|
mock_restore_cache(
|
||||||
|
hass,
|
||||||
|
(
|
||||||
|
State(
|
||||||
|
"input_weekday.test_1",
|
||||||
|
"mon,wed,fri",
|
||||||
|
{ATTR_WEEKDAYS: ["mon", "wed", "fri"]},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.state = "starting"
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed", "fri"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_state_restoration_with_initial(hass: HomeAssistant) -> None:
|
||||||
|
"""Test state restoration with initial value - should prefer initial."""
|
||||||
|
mock_restore_cache(
|
||||||
|
hass,
|
||||||
|
(
|
||||||
|
State(
|
||||||
|
"input_weekday.test_1",
|
||||||
|
"mon,wed,fri",
|
||||||
|
{ATTR_WEEKDAYS: ["mon", "wed", "fri"]},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.state = "starting"
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["sat", "sun"]}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["sat", "sun"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_storage(hass: HomeAssistant, storage_setup) -> None:
|
||||||
|
"""Test storage."""
|
||||||
|
assert await storage_setup()
|
||||||
|
state = hass.states.get("input_weekday.from_storage")
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed", "fri"]
|
||||||
|
assert state.attributes[ATTR_EDITABLE]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_editable_state_attribute(hass: HomeAssistant) -> None:
|
||||||
|
"""Test editable attribute."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{DOMAIN: {"test_1": {"weekdays": ["mon"]}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test_1")
|
||||||
|
assert state.attributes[ATTR_EDITABLE] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_websocket_create(
|
||||||
|
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test create via websocket."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": f"{DOMAIN}/create",
|
||||||
|
"name": "My Weekday",
|
||||||
|
"weekdays": ["mon", "fri"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = await client.receive_json()
|
||||||
|
assert resp["success"]
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.my_weekday")
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "fri"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_websocket_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test update via websocket."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": f"{DOMAIN}/create",
|
||||||
|
"name": "My Weekday",
|
||||||
|
"weekdays": ["mon"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = await client.receive_json()
|
||||||
|
assert resp["success"]
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.my_weekday")
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon"]
|
||||||
|
|
||||||
|
entity_entry = entity_registry.async_get("input_weekday.my_weekday")
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": f"{DOMAIN}/update",
|
||||||
|
f"{DOMAIN}_id": entity_entry.unique_id,
|
||||||
|
"weekdays": ["tue", "wed"],
|
||||||
|
"name": "Updated Weekday",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = await client.receive_json()
|
||||||
|
assert resp["success"]
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.my_weekday")
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["tue", "wed"]
|
||||||
|
assert state.attributes[ATTR_FRIENDLY_NAME] == "Updated Weekday"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_websocket_delete(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test delete via websocket."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": f"{DOMAIN}/create",
|
||||||
|
"name": "My Weekday",
|
||||||
|
"weekdays": ["mon"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = await client.receive_json()
|
||||||
|
assert resp["success"]
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.my_weekday")
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
entity_entry = entity_registry.async_get("input_weekday.my_weekday")
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": f"{DOMAIN}/delete",
|
||||||
|
f"{DOMAIN}_id": entity_entry.unique_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = await client.receive_json()
|
||||||
|
assert resp["success"]
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.my_weekday")
|
||||||
|
assert state is None
|
||||||
37
tests/components/input_weekday/test_recorder.py
Normal file
37
tests/components/input_weekday/test_recorder.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""Tests for the Input Weekday recorder."""
|
||||||
|
|
||||||
|
from homeassistant.components.input_weekday import ATTR_EDITABLE, ATTR_WEEKDAYS
|
||||||
|
from homeassistant.components.recorder import Recorder
|
||||||
|
from homeassistant.components.recorder.history import get_significant_states
|
||||||
|
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from tests.components.recorder.common import async_wait_recording_done
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None:
|
||||||
|
"""Test that certain attributes are excluded."""
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"input_weekday",
|
||||||
|
{"input_weekday": {"test": {"weekdays": ["mon", "wed"]}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("input_weekday.test")
|
||||||
|
assert state.attributes[ATTR_WEEKDAYS] == ["mon", "wed"]
|
||||||
|
assert state.attributes[ATTR_EDITABLE] is False
|
||||||
|
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, ["input_weekday.test"]
|
||||||
|
)
|
||||||
|
assert len(states) == 1
|
||||||
|
for entity_states in states.values():
|
||||||
|
for state in entity_states:
|
||||||
|
assert ATTR_WEEKDAYS in state.attributes
|
||||||
|
assert ATTR_EDITABLE not in state.attributes
|
||||||
|
assert ATTR_FRIENDLY_NAME in state.attributes
|
||||||
59
tests/components/input_weekday/test_reproduce_state.py
Normal file
59
tests/components/input_weekday/test_reproduce_state.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Test reproduce state for Input Weekday."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.input_weekday import ATTR_WEEKDAYS, DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers.state import async_reproduce_state
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def setup_component(hass: HomeAssistant):
|
||||||
|
"""Set up component."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass, DOMAIN, {DOMAIN: {"test_weekday": {"weekdays": []}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reproduce_weekday(hass: HomeAssistant) -> None:
|
||||||
|
"""Test reproduce weekday."""
|
||||||
|
calls = async_mock_service(hass, DOMAIN, "set_weekdays")
|
||||||
|
|
||||||
|
await async_reproduce_state(
|
||||||
|
hass,
|
||||||
|
[
|
||||||
|
State(
|
||||||
|
"input_weekday.test_weekday",
|
||||||
|
"mon,wed,fri",
|
||||||
|
{ATTR_WEEKDAYS: ["mon", "wed", "fri"]},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == {
|
||||||
|
"entity_id": "input_weekday.test_weekday",
|
||||||
|
ATTR_WEEKDAYS: ["mon", "wed", "fri"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reproduce_weekday_missing_attribute(
|
||||||
|
hass: HomeAssistant, setup_component, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test reproduce weekday with missing weekdays attribute."""
|
||||||
|
calls = async_mock_service(hass, DOMAIN, "set_weekdays")
|
||||||
|
|
||||||
|
await async_reproduce_state(
|
||||||
|
hass,
|
||||||
|
[State("input_weekday.test_weekday", "mon,wed")],
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert "weekdays attribute is missing" in caplog.text
|
||||||
Reference in New Issue
Block a user