mirror of
https://github.com/home-assistant/core.git
synced 2025-10-17 15:49:58 +00:00
Compare commits
11 Commits
trigger_ac
...
input-week
Author | SHA1 | Date | |
---|---|---|---|
![]() |
39d970347e | ||
![]() |
9cccc96f63 | ||
![]() |
a32ada3155 | ||
![]() |
77f078e57d | ||
![]() |
8657bfd0bf | ||
![]() |
fe4eb8766d | ||
![]() |
2d9f14c401 | ||
![]() |
7b6ccb07fd | ||
![]() |
2ba5728060 | ||
![]() |
b5f163cc85 | ||
![]() |
65540a3e0b |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -741,7 +741,7 @@ jobs:
|
||||
- name: Generate partial mypy restore key
|
||||
id: generate-mypy-key
|
||||
run: |
|
||||
mypy_version=$(cat requirements_test.txt | grep mypy | cut -d '=' -f 3)
|
||||
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
|
||||
echo "version=$mypy_version" >> $GITHUB_OUTPUT
|
||||
echo "key=mypy-${{ env.MYPY_CACHE_VERSION }}-$mypy_version-${{
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -753,6 +753,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/input_select/ @home-assistant/core
|
||||
/homeassistant/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
|
||||
/tests/components/insteon/ @teharris1
|
||||
/homeassistant/components/integration/ @dgomes
|
||||
@@ -1413,8 +1415,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sfr_box/ @epenet
|
||||
/homeassistant/components/sftp_storage/ @maretodoric
|
||||
/tests/components/sftp_storage/ @maretodoric
|
||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
||||
/tests/components/sharkiq/ @JeffResc @funkybunch
|
||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
|
||||
/tests/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
|
||||
/homeassistant/components/shell_command/ @home-assistant/core
|
||||
/tests/components/shell_command/ @home-assistant/core
|
||||
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
||||
|
@@ -231,6 +231,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"input_datetime",
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_weekday",
|
||||
"input_text",
|
||||
"schedule",
|
||||
"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(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "time",
|
||||
vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]),
|
||||
vol.Optional(CONF_WEEKDAY): vol.Any(
|
||||
vol.In(WEEKDAYS),
|
||||
vol.All(cv.ensure_list, [vol.In(WEEKDAYS)]),
|
||||
),
|
||||
vol.Optional(CONF_WEEKDAY): _WEEKDAY_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -117,7 +123,14 @@ async def async_attach_trigger( # noqa: C901
|
||||
|
||||
# Check if current weekday matches the configuration
|
||||
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
|
||||
elif current_weekday not in weekday_config:
|
||||
return
|
||||
|
@@ -456,7 +456,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
||||
return self._available
|
||||
|
||||
@ha_callback
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event."""
|
||||
if state := self.hass.states.get(self.entity_id):
|
||||
@@ -725,7 +725,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
self._entry_title = entry_title
|
||||
self.iid_storage = iid_storage
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def pair(
|
||||
self, client_username_bytes: bytes, client_public: str, client_permissions: int
|
||||
) -> bool:
|
||||
@@ -735,7 +735,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
async_dismiss_setup_message(self.hass, self.entry_id)
|
||||
return cast(bool, success)
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def unpair(self, client_uuid: UUID) -> None:
|
||||
"""Override super function to show setup message if unpaired."""
|
||||
super().unpair(client_uuid)
|
||||
|
@@ -71,7 +71,7 @@ class HomeDoorbellAccessory(HomeAccessory):
|
||||
self.async_update_doorbell_state(None, state)
|
||||
|
||||
@ha_callback
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def run(self) -> None:
|
||||
"""Handle doorbell event."""
|
||||
if self._char_doorbell_detected:
|
||||
|
@@ -219,7 +219,7 @@ class AirPurifier(Fan):
|
||||
return preset_mode.lower() != "auto"
|
||||
|
||||
@callback
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
||||
|
@@ -229,7 +229,7 @@ class Camera(HomeDoorbellAccessory, PyhapCamera): # type: ignore[misc]
|
||||
)
|
||||
self._async_update_motion_state(None, state)
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@callback
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
@@ -127,7 +127,7 @@ class GarageDoorOpener(HomeAccessory):
|
||||
self.async_update_state(state)
|
||||
|
||||
@callback
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
||||
|
@@ -178,7 +178,7 @@ class HumidifierDehumidifier(HomeAccessory):
|
||||
self._async_update_current_humidity(humidity_state)
|
||||
|
||||
@callback
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
def run(self) -> None:
|
||||
"""Handle accessory driver started event.
|
||||
|
||||
|
@@ -108,7 +108,7 @@ class DeviceTriggerAccessory(HomeAccessory):
|
||||
_LOGGER.log,
|
||||
)
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@pyhap_callback # type: ignore[untyped-decorator]
|
||||
@callback
|
||||
def run(self) -> None:
|
||||
"""Run the accessory."""
|
||||
|
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."
|
||||
}
|
||||
}
|
||||
}
|
@@ -59,7 +59,7 @@ async def create_server(
|
||||
# Backwards compatibility with old MCP Server config
|
||||
return await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
|
||||
@server.list_prompts() # type: ignore[no-untyped-call, misc]
|
||||
@server.list_prompts() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
async def handle_list_prompts() -> list[types.Prompt]:
|
||||
llm_api = await get_api_instance()
|
||||
return [
|
||||
@@ -69,7 +69,7 @@ async def create_server(
|
||||
)
|
||||
]
|
||||
|
||||
@server.get_prompt() # type: ignore[no-untyped-call, misc]
|
||||
@server.get_prompt() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
async def handle_get_prompt(
|
||||
name: str, arguments: dict[str, str] | None
|
||||
) -> types.GetPromptResult:
|
||||
@@ -90,13 +90,13 @@ async def create_server(
|
||||
],
|
||||
)
|
||||
|
||||
@server.list_tools() # type: ignore[no-untyped-call, misc]
|
||||
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
"""List available time tools."""
|
||||
llm_api = await get_api_instance()
|
||||
return [_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools]
|
||||
|
||||
@server.call_tool() # type: ignore[misc]
|
||||
@server.call_tool() # type: ignore[untyped-decorator]
|
||||
async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
|
||||
"""Handle calling tools."""
|
||||
llm_api = await get_api_instance()
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "sharkiq",
|
||||
"name": "Shark IQ",
|
||||
"codeowners": ["@JeffResc", "@funkybunch"],
|
||||
"codeowners": ["@JeffResc", "@funkybunch", "@TheOneOgre"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||
"iot_class": "cloud_polling",
|
||||
|
@@ -157,21 +157,18 @@ SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
|
||||
key="input|input",
|
||||
name="Input",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_block_momentary_input,
|
||||
),
|
||||
("relay", "input"): BlockBinarySensorDescription(
|
||||
key="relay|input",
|
||||
name="Input",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_block_momentary_input,
|
||||
),
|
||||
("device", "input"): BlockBinarySensorDescription(
|
||||
key="device|input",
|
||||
name="Input",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_block_momentary_input,
|
||||
),
|
||||
("sensor", "extInput"): BlockBinarySensorDescription(
|
||||
@@ -201,7 +198,6 @@ RPC_SENSORS: Final = {
|
||||
key="input",
|
||||
sub_key="state",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
removal_condition=is_rpc_momentary_input,
|
||||
),
|
||||
"cloud": RpcBinarySensorDescription(
|
||||
|
@@ -11,7 +11,13 @@ from typing import Any
|
||||
from propcache.api import cached_property
|
||||
from zha.mixins import LogMixin
|
||||
|
||||
from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, EntityCategory
|
||||
from homeassistant.const import (
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_NAME,
|
||||
ATTR_VIA_DEVICE,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import State, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -85,14 +91,19 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
|
||||
ieee = zha_device_info["ieee"]
|
||||
zha_gateway = self.entity_data.device_proxy.gateway_proxy.gateway
|
||||
|
||||
return DeviceInfo(
|
||||
device_info = DeviceInfo(
|
||||
connections={(CONNECTION_ZIGBEE, ieee)},
|
||||
identifiers={(DOMAIN, ieee)},
|
||||
manufacturer=zha_device_info[ATTR_MANUFACTURER],
|
||||
model=zha_device_info[ATTR_MODEL],
|
||||
name=zha_device_info[ATTR_NAME],
|
||||
via_device=(DOMAIN, str(zha_gateway.state.node_info.ieee)),
|
||||
)
|
||||
if ieee != str(zha_gateway.state.node_info.ieee):
|
||||
device_info[ATTR_VIA_DEVICE] = (
|
||||
DOMAIN,
|
||||
str(zha_gateway.state.node_info.ieee),
|
||||
)
|
||||
return device_info
|
||||
|
||||
@callback
|
||||
def _handle_entity_events(self, event: Any) -> None:
|
||||
|
@@ -7922,6 +7922,10 @@
|
||||
"integration_type": "helper",
|
||||
"config_flow": false
|
||||
},
|
||||
"input_weekday": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": false
|
||||
},
|
||||
"integration": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
@@ -8021,6 +8025,7 @@
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"input_weekday",
|
||||
"integration",
|
||||
"irm_kmi",
|
||||
"islamic_prayer_times",
|
||||
|
@@ -954,11 +954,25 @@ def time(
|
||||
if weekday is not None:
|
||||
now_weekday = WEEKDAYS[now.weekday()]
|
||||
|
||||
condition_trace_update_result(weekday=weekday, now_weekday=now_weekday)
|
||||
if (
|
||||
isinstance(weekday, str) and weekday != now_weekday
|
||||
) or now_weekday not in weekday:
|
||||
return False
|
||||
# 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)
|
||||
if (
|
||||
isinstance(weekday, str) and weekday != now_weekday
|
||||
) or now_weekday not in weekday:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -260,11 +260,11 @@ class TriggerConfig:
|
||||
class TriggerActionType(Protocol):
|
||||
"""Protocol type for trigger action callback."""
|
||||
|
||||
def __call__(
|
||||
async def __call__(
|
||||
self,
|
||||
run_variables: dict[str, Any],
|
||||
context: Context | None = None,
|
||||
) -> Coroutine[Any, Any, Any] | Any:
|
||||
) -> Any:
|
||||
"""Define action callback type."""
|
||||
|
||||
|
||||
@@ -444,8 +444,8 @@ async def async_validate_trigger_config(
|
||||
|
||||
|
||||
def _trigger_action_wrapper(
|
||||
hass: HomeAssistant, action: TriggerActionType, conf: ConfigType
|
||||
) -> TriggerActionType:
|
||||
hass: HomeAssistant, action: Callable, conf: ConfigType
|
||||
) -> Callable:
|
||||
"""Wrap trigger action with extra vars if configured.
|
||||
|
||||
If action is a coroutine function, a coroutine function will be returned.
|
||||
@@ -477,7 +477,7 @@ def _trigger_action_wrapper(
|
||||
else:
|
||||
|
||||
@functools.wraps(action)
|
||||
def with_vars(
|
||||
async def with_vars(
|
||||
run_variables: dict[str, Any], context: Context | None = None
|
||||
) -> Any:
|
||||
"""Wrap action with extra vars."""
|
||||
|
@@ -11,9 +11,11 @@ astroid==3.3.11
|
||||
coverage==7.10.6
|
||||
freezegun==1.5.2
|
||||
go2rtc-client==0.2.1
|
||||
# librt is an internal mypy dependency
|
||||
librt==0.2.1
|
||||
license-expression==30.4.3
|
||||
mock-open==1.4.0
|
||||
mypy-dev==1.19.0a2
|
||||
mypy-dev==1.19.0a4
|
||||
pre-commit==4.2.0
|
||||
pydantic==2.12.0
|
||||
pylint==3.3.8
|
||||
|
@@ -91,6 +91,7 @@ NO_IOT_CLASS = [
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"input_weekday",
|
||||
"intent_script",
|
||||
"intent",
|
||||
"logbook",
|
||||
|
@@ -2214,6 +2214,7 @@ NO_QUALITY_SCALE = [
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"input_weekday",
|
||||
"intent_script",
|
||||
"intent",
|
||||
"logbook",
|
||||
|
@@ -1061,6 +1061,14 @@ def test_weekday_validation() -> None:
|
||||
}
|
||||
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_config = {"platform": "time", "at": "5:00:00", "weekday": "invalid"}
|
||||
with pytest.raises(vol.Invalid):
|
||||
@@ -1074,3 +1082,176 @@ def test_weekday_validation() -> None:
|
||||
}
|
||||
with pytest.raises(vol.Invalid):
|
||||
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
|
@@ -230,7 +230,7 @@ DEVICE_MOCKS = [
|
||||
"wg2_tmwhss6ntjfc7prs", # https://github.com/home-assistant/core/issues/150662
|
||||
"wg2_v7owd9tzcaninc36", # https://github.com/orgs/home-assistant/discussions/539
|
||||
"wk_6kijc7nd", # https://github.com/home-assistant/core/issues/136513
|
||||
"wk_IAYz2WK1th0cMLmL", # https://github.com/orgs/home-assistant/discussions/842
|
||||
"wk_IAYz2WK1th0cMLmL", # https://github.com/home-assistant/core/issues/150077
|
||||
"wk_aqoouq7x", # https://github.com/home-assistant/core/issues/146263
|
||||
"wk_ccpwojhalfxryigz", # https://github.com/home-assistant/core/issues/145551
|
||||
"wk_cpmgn2cf", # https://github.com/orgs/home-assistant/discussions/684
|
||||
|
@@ -10,9 +10,9 @@
|
||||
"online": true,
|
||||
"sub": false,
|
||||
"time_zone": "+01:00",
|
||||
"active_time": "2018-12-04T17:50:07+00:00",
|
||||
"create_time": "2018-12-04T17:50:07+00:00",
|
||||
"update_time": "2025-09-03T07:44:16+00:00",
|
||||
"active_time": "2022-11-15T08:35:43+00:00",
|
||||
"create_time": "2022-11-15T08:35:43+00:00",
|
||||
"update_time": "2022-11-15T08:35:43+00:00",
|
||||
"function": {
|
||||
"switch": {
|
||||
"type": "Boolean",
|
||||
@@ -22,6 +22,16 @@
|
||||
"type": "Boolean",
|
||||
"value": {}
|
||||
},
|
||||
"temp_set": {
|
||||
"type": "Integer",
|
||||
"value": {
|
||||
"unit": "\u2103",
|
||||
"min": 10,
|
||||
"max": 70,
|
||||
"scale": 1,
|
||||
"step": 5
|
||||
}
|
||||
},
|
||||
"eco": {
|
||||
"type": "Boolean",
|
||||
"value": {}
|
||||
@@ -35,26 +45,14 @@
|
||||
"scale": 0,
|
||||
"step": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"status_range": {
|
||||
"eco": {
|
||||
"type": "Boolean",
|
||||
"value": {}
|
||||
},
|
||||
"Mode": {
|
||||
"type": "Enum",
|
||||
"value": {
|
||||
"range": ["0", "1"]
|
||||
}
|
||||
},
|
||||
"program": {
|
||||
"type": "Raw",
|
||||
"value": {
|
||||
"maxlen": 128
|
||||
}
|
||||
},
|
||||
"tempSwitch": {
|
||||
"type": "Enum",
|
||||
"value": {
|
||||
"range": ["0", "1"]
|
||||
}
|
||||
},
|
||||
"TempSet": {
|
||||
"temp_set": {
|
||||
"type": "Integer",
|
||||
"value": {
|
||||
"unit": "\u2103",
|
||||
@@ -63,12 +61,6 @@
|
||||
"scale": 1,
|
||||
"step": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"status_range": {
|
||||
"eco": {
|
||||
"type": "Boolean",
|
||||
"value": {}
|
||||
},
|
||||
"switch": {
|
||||
"type": "Boolean",
|
||||
@@ -87,43 +79,14 @@
|
||||
"scale": 0,
|
||||
"step": 5
|
||||
}
|
||||
},
|
||||
"floorTemp": {
|
||||
"type": "Integer",
|
||||
"value": {
|
||||
"max": 198,
|
||||
"min": 0,
|
||||
"scale": 0,
|
||||
"step": 5,
|
||||
"unit": "\u2103"
|
||||
}
|
||||
},
|
||||
"floortempFunction": {
|
||||
"type": "Boolean",
|
||||
"value": {}
|
||||
},
|
||||
"TempSet": {
|
||||
"type": "Integer",
|
||||
"value": {
|
||||
"unit": "\u2103",
|
||||
"min": 10,
|
||||
"max": 70,
|
||||
"scale": 1,
|
||||
"step": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"switch": false,
|
||||
"upper_temp": 55,
|
||||
"eco": true,
|
||||
"child_lock": false,
|
||||
"Mode": 1,
|
||||
"program": "DwYoDwceHhQoORceOhceOxceAAkoAAoeHhQoORceOhceOxceAAkoAAoeHhQoORceOhceOxce",
|
||||
"floorTemp": 0,
|
||||
"tempSwitch": 0,
|
||||
"floortempFunction": true,
|
||||
"TempSet": 41
|
||||
"switch": true,
|
||||
"temp_set": 46,
|
||||
"upper_temp": 45,
|
||||
"eco": false,
|
||||
"child_lock": true
|
||||
},
|
||||
"set_up": true,
|
||||
"support_local": true
|
||||
|
@@ -383,9 +383,9 @@
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 35,
|
||||
'min_temp': 7,
|
||||
'target_temp_step': 1.0,
|
||||
'max_temp': 7.0,
|
||||
'min_temp': 1.0,
|
||||
'target_temp_step': 0.5,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -410,7 +410,7 @@
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 384>,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'tuya.LmLMc0ht1KW2zYAIkw',
|
||||
'unit_of_measurement': None,
|
||||
@@ -419,23 +419,24 @@
|
||||
# name: test_platform_setup_and_discovery[climate.el_termostato_de_la_cocina-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 5.5,
|
||||
'current_temperature': 4.5,
|
||||
'friendly_name': 'El termostato de la cocina',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
||||
]),
|
||||
'max_temp': 35,
|
||||
'min_temp': 7,
|
||||
'supported_features': <ClimateEntityFeature: 384>,
|
||||
'target_temp_step': 1.0,
|
||||
'max_temp': 7.0,
|
||||
'min_temp': 1.0,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'target_temp_step': 0.5,
|
||||
'temperature': 4.6,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.el_termostato_de_la_cocina',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
'state': 'heat_cool',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[climate.empore-entry]
|
||||
|
@@ -3238,7 +3238,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[switch.elivco_kitchen_socket_child_lock-entry]
|
||||
|
Reference in New Issue
Block a user