Compare commits

..

11 Commits

Author SHA1 Message Date
Wendelin
39d970347e Refactor input_weekday configuration validation and update test cases to use unique_id 2025-10-09 12:00:35 +02:00
Wendelin
9cccc96f63 Fix test 2025-10-09 10:35:03 +02:00
Wendelin
a32ada3155 Use input_weekday in automations 2025-10-09 08:48:41 +02:00
Wendelin
77f078e57d Add weekdays to build-in helpers 2025-10-08 17:23:19 +02:00
Wendelin
8657bfd0bf Add input helper weekdays 2025-10-08 17:08:48 +02:00
Joost Lekkerkerker
fe4eb8766d Don't mark ZHA coordinator as via_device with itself (#154004) 2025-10-08 16:05:54 +02:00
Mark Adkins
2d9f14c401 Add 3rd maintainer to sharkiq (#153961) 2025-10-08 15:17:52 +02:00
dependabot[bot]
7b6ccb07fd Bump github/codeql-action from 3.30.6 to 4.30.7 (#153979)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 13:42:25 +02:00
Shay Levy
2ba5728060 Enable Shelly binary input sensors by default (#154001) 2025-10-08 14:41:53 +03:00
epenet
b5f163cc85 Update Tuya fixture for product ID IAYz2WK1th0cMLmL (#154000) 2025-10-08 13:28:11 +02:00
Marc Mueller
65540a3e0b Update mypy dev to 1.19.0a4 (#153995) 2025-10-08 13:24:54 +02:00
38 changed files with 1475 additions and 117 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -231,6 +231,7 @@ DEFAULT_INTEGRATIONS = {
"input_datetime",
"input_number",
"input_select",
"input_weekday",
"input_text",
"schedule",
"timer",

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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."""

View 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()

View 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"
}
}
}

View 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"
}

View 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
)

View 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:

View 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."
}
}
}

View File

@@ -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()

View File

@@ -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",

View File

@@ -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(

View File

@@ -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:

View File

@@ -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",

View File

@@ -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

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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

View File

@@ -91,6 +91,7 @@ NO_IOT_CLASS = [
"input_number",
"input_select",
"input_text",
"input_weekday",
"intent_script",
"intent",
"logbook",

View File

@@ -2214,6 +2214,7 @@ NO_QUALITY_SCALE = [
"input_number",
"input_select",
"input_text",
"input_weekday",
"intent_script",
"intent",
"logbook",

View File

@@ -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

View File

@@ -0,0 +1 @@
"""Tests for the Input Weekday component."""

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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]