mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 14:58:50 +00:00
Compare commits
14 Commits
add_light_
...
knx-text-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c383081f1 | ||
|
|
c5418b4849 | ||
|
|
1f53b9183c | ||
|
|
3050a5c896 | ||
|
|
9f886e66c7 | ||
|
|
3c752d4516 | ||
|
|
e4bfdf5b30 | ||
|
|
3e43424a73 | ||
|
|
0db9dcfd1c | ||
|
|
5b5850224a | ||
|
|
065b0eb5b2 | ||
|
|
6a1d86d5db | ||
|
|
f99a73ef28 | ||
|
|
0436d30062 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -794,6 +794,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/intellifire/ @jeeftor
|
||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/homeassistant/components/intent_script/ @arturpragacz
|
||||
/tests/components/intent_script/ @arturpragacz
|
||||
/homeassistant/components/intesishome/ @jnimmo
|
||||
/homeassistant/components/iometer/ @jukrebs
|
||||
/tests/components/iometer/ @jukrebs
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
|
||||
}
|
||||
|
||||
@@ -134,7 +134,9 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"humidifier",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"siren",
|
||||
"switch",
|
||||
"text",
|
||||
"update",
|
||||
|
||||
@@ -110,7 +110,7 @@ async def async_register_dynalite_frontend(hass: HomeAssistant):
|
||||
frontend_url_path=DOMAIN,
|
||||
config_panel_domain=DOMAIN,
|
||||
webcomponent_name="dynalite-panel",
|
||||
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
||||
module_url=f"{URL_BASE}/entrypoint.{build_id}.js",
|
||||
embed_iframe=True,
|
||||
require_admin=True,
|
||||
)
|
||||
|
||||
@@ -70,6 +70,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
device_type=device_type,
|
||||
)
|
||||
|
||||
# For NVRs or devices with no detected events, try to fetch events from ISAPI
|
||||
if device_type == "NVR" or not camera.current_event_states:
|
||||
|
||||
def fetch_and_inject_nvr_events() -> None:
|
||||
"""Fetch and inject NVR events in a single executor job."""
|
||||
if nvr_events := camera.get_event_triggers(None):
|
||||
camera.inject_events(nvr_events)
|
||||
|
||||
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
|
||||
|
||||
# Start the event stream
|
||||
await hass.async_add_executor_job(camera.start_stream)
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ async def async_register_insteon_frontend(hass: HomeAssistant):
|
||||
frontend_url_path=DOMAIN,
|
||||
webcomponent_name="insteon-frontend",
|
||||
config_panel_domain=DOMAIN,
|
||||
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
||||
module_url=f"{URL_BASE}/entrypoint.{build_id}.js",
|
||||
embed_iframe=True,
|
||||
require_admin=True,
|
||||
)
|
||||
|
||||
@@ -282,6 +282,8 @@ async def websocket_reset_properties(
|
||||
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
|
||||
return
|
||||
|
||||
for prop in device.configuration.values():
|
||||
prop.new_value = None
|
||||
for prop in device.operating_flags:
|
||||
device.operating_flags[prop].new_value = None
|
||||
for prop in device.properties:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"loggers": ["pyinsteon", "pypubsub"],
|
||||
"requirements": [
|
||||
"pyinsteon==1.6.4",
|
||||
"insteon-frontend-home-assistant==0.5.0"
|
||||
"insteon-frontend-home-assistant==0.6.0"
|
||||
],
|
||||
"single_config_entry": true,
|
||||
"usb": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "intent_script",
|
||||
"name": "Intent Script",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@arturpragacz"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/intent_script",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ SUPPORTED_PLATFORMS_UI: Final = {
|
||||
Platform.LIGHT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TEXT,
|
||||
Platform.TIME,
|
||||
}
|
||||
|
||||
|
||||
@@ -74,3 +74,6 @@ CONF_GA_SATURATION: Final = "ga_saturation"
|
||||
|
||||
# Sensor
|
||||
CONF_ALWAYS_CALLBACK: Final = "always_callback"
|
||||
|
||||
# Text
|
||||
CONF_GA_TEXT: Final = "ga_text"
|
||||
|
||||
@@ -13,10 +13,12 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.text import TextMode
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@@ -90,6 +92,7 @@ from .const import (
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_TEMPERATURE_CURRENT,
|
||||
CONF_GA_TEMPERATURE_TARGET,
|
||||
CONF_GA_TEXT,
|
||||
CONF_GA_TIME,
|
||||
CONF_GA_UP_DOWN,
|
||||
CONF_GA_VALVE,
|
||||
@@ -428,6 +431,20 @@ SWITCH_KNX_SCHEMA = vol.Schema(
|
||||
},
|
||||
)
|
||||
|
||||
TEXT_KNX_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_GA_TEXT): GASelector(write_required=True, dpt=["string"]),
|
||||
vol.Required(CONF_MODE, default=TextMode.TEXT): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=list(TextMode),
|
||||
translation_key="component.knx.config_panel.entities.create.text.knx.mode",
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
TIME_KNX_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_GA_TIME): GASelector(write_required=True, valid_dpt="10.001"),
|
||||
@@ -696,6 +713,7 @@ KNX_SCHEMA_FOR_PLATFORM = {
|
||||
Platform.LIGHT: LIGHT_KNX_SCHEMA,
|
||||
Platform.SENSOR: SENSOR_KNX_SCHEMA,
|
||||
Platform.SWITCH: SWITCH_KNX_SCHEMA,
|
||||
Platform.TEXT: TEXT_KNX_SCHEMA,
|
||||
Platform.TIME: TIME_KNX_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
"8_005": "[%key:component::knx::config_panel::dpt::options::8_002%]",
|
||||
"8_006": "[%key:component::knx::config_panel::dpt::options::8_002%]",
|
||||
"8_007": "[%key:component::knx::config_panel::dpt::options::8_002%]",
|
||||
"8_010": "Percent (-327,68 … 327,67)",
|
||||
"8_010": "Percent (-327.68 … 327.67)",
|
||||
"8_011": "Rotation angle",
|
||||
"8_012": "Length (Altitude)",
|
||||
"9": "Generic 2-byte floating point",
|
||||
@@ -816,6 +816,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"description": "The KNX text platform is used as an interface to text objects.",
|
||||
"knx": {
|
||||
"ga_text": {
|
||||
"description": "The group address of the text object.",
|
||||
"label": "Text"
|
||||
},
|
||||
"mode": {
|
||||
"description": "Select how the entity is displayed in Home Assistant.",
|
||||
"label": "[%common::config_flow::data::mode%]",
|
||||
"options": {
|
||||
"password": "[%common::config_flow::data::password%]",
|
||||
"text": "[%key:component::text::entity_component::_::state_attributes::mode::state::text%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"description": "The KNX time platform is used as an interface to time objects.",
|
||||
"knx": {
|
||||
@@ -1061,7 +1078,7 @@
|
||||
"name": "[%key:component::knx::services::send::fields::address::name%]"
|
||||
},
|
||||
"attribute": {
|
||||
"description": "Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”.",
|
||||
"description": "Attribute of the entity that shall be sent to the KNX bus. If not set, the state will be sent. Eg. for a light the state is either “on” or “off” - with attribute you can expose its “brightness”.",
|
||||
"name": "Entity attribute"
|
||||
},
|
||||
"default": {
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from xknx import XKNX
|
||||
from propcache.api import cached_property
|
||||
from xknx.devices import Notification as XknxNotification
|
||||
from xknx.dpt import DPTLatin1
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.text import TextEntity
|
||||
from homeassistant.components.text import TextEntity, TextMode
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_MODE,
|
||||
@@ -18,13 +18,25 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, KNX_ADDRESS, KNX_MODULE_KEY
|
||||
from .entity import KnxYamlEntity
|
||||
from .const import (
|
||||
CONF_RESPOND_TO_READ,
|
||||
CONF_STATE_ADDRESS,
|
||||
CONF_SYNC_STATE,
|
||||
DOMAIN,
|
||||
KNX_ADDRESS,
|
||||
KNX_MODULE_KEY,
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .knx_module import KNXModule
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_TEXT
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -32,46 +44,39 @@ async def async_setup_entry(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensor(s) for KNX platform."""
|
||||
"""Set up text(s) for KNX platform."""
|
||||
knx_module = hass.data[KNX_MODULE_KEY]
|
||||
config: list[ConfigType] = knx_module.config_yaml[Platform.TEXT]
|
||||
|
||||
async_add_entities(KNXText(knx_module, entity_config) for entity_config in config)
|
||||
|
||||
|
||||
def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification:
|
||||
"""Return a KNX Notification to be used within XKNX."""
|
||||
return XknxNotification(
|
||||
xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
platform = async_get_current_platform()
|
||||
knx_module.config_store.add_platform(
|
||||
platform=Platform.TEXT,
|
||||
controller=KnxUiEntityPlatformController(
|
||||
knx_module=knx_module,
|
||||
entity_platform=platform,
|
||||
entity_class=KnxUiText,
|
||||
),
|
||||
)
|
||||
|
||||
entities: list[KnxYamlEntity | KnxUiEntity] = []
|
||||
if yaml_platform_config := knx_module.config_yaml.get(Platform.TEXT):
|
||||
entities.extend(
|
||||
KnxYamlText(knx_module, entity_config)
|
||||
for entity_config in yaml_platform_config
|
||||
)
|
||||
if ui_config := knx_module.config_store.data["entities"].get(Platform.TEXT):
|
||||
entities.extend(
|
||||
KnxUiText(knx_module, unique_id, config)
|
||||
for unique_id, config in ui_config.items()
|
||||
)
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
|
||||
|
||||
class _KnxText(TextEntity, RestoreEntity):
|
||||
"""Representation of a KNX text."""
|
||||
|
||||
_device: XknxNotification
|
||||
_attr_native_max = 14
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX text."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_notification(knx_module.xknx, config),
|
||||
)
|
||||
self._attr_mode = config[CONF_MODE]
|
||||
self._attr_pattern = (
|
||||
r"[\u0000-\u00ff]*" # Latin-1
|
||||
if issubclass(self._device.remote_value.dpt_class, DPTLatin1)
|
||||
else r"[\u0000-\u007f]*" # ASCII
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -81,6 +86,15 @@ class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
|
||||
if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
||||
self._device.remote_value.value = last_state.state
|
||||
|
||||
@cached_property
|
||||
def pattern(self) -> str | None:
|
||||
"""Return the regex pattern that the value must match."""
|
||||
return (
|
||||
r"[\u0000-\u00ff]*" # Latin-1
|
||||
if issubclass(self._device.remote_value.dpt_class, DPTLatin1)
|
||||
else r"[\u0000-\u007f]*" # ASCII
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the value reported by the text."""
|
||||
@@ -89,3 +103,56 @@ class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
|
||||
async def async_set_value(self, value: str) -> None:
|
||||
"""Change the value."""
|
||||
await self._device.set(value)
|
||||
|
||||
|
||||
class KnxYamlText(_KnxText, KnxYamlEntity):
|
||||
"""Representation of a KNX text configured from YAML."""
|
||||
|
||||
_device: XknxNotification
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX text."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxNotification(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
),
|
||||
)
|
||||
self._attr_mode = config[CONF_MODE]
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
|
||||
class KnxUiText(_KnxText, KnxUiEntity):
|
||||
"""Representation of a KNX text configured from UI."""
|
||||
|
||||
_device: XknxNotification
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
knx_module: KNXModule,
|
||||
unique_id: str,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize a KNX text."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxNotification(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address=knx_conf.get_write(CONF_GA_TEXT),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_TEXT),
|
||||
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
value_type=knx_conf.get_dpt(CONF_GA_TEXT),
|
||||
)
|
||||
self._attr_mode = TextMode(knx_conf.get(CONF_MODE))
|
||||
|
||||
@@ -22,5 +22,19 @@
|
||||
"unlock": {
|
||||
"service": "mdi:lock-open-variant"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"jammed": {
|
||||
"trigger": "mdi:lock-alert"
|
||||
},
|
||||
"locked": {
|
||||
"trigger": "mdi:lock"
|
||||
},
|
||||
"opened": {
|
||||
"trigger": "mdi:lock-open-variant"
|
||||
},
|
||||
"unlocked": {
|
||||
"trigger": "mdi:lock-open-variant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted locks to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"lock": "Lock {entity_name}",
|
||||
@@ -50,6 +54,15 @@
|
||||
"message": "The code for {entity_id} doesn't match pattern {code_format}."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"lock": {
|
||||
"description": "Locks a lock.",
|
||||
@@ -82,5 +95,47 @@
|
||||
"name": "Unlock"
|
||||
}
|
||||
},
|
||||
"title": "Lock"
|
||||
"title": "Lock",
|
||||
"triggers": {
|
||||
"jammed": {
|
||||
"description": "Triggers after one or more locks jam.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::lock::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::lock::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock jammed"
|
||||
},
|
||||
"locked": {
|
||||
"description": "Triggers after one or more locks lock.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::lock::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::lock::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock locked"
|
||||
},
|
||||
"opened": {
|
||||
"description": "Triggers after one or more locks open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::lock::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::lock::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock opened"
|
||||
},
|
||||
"unlocked": {
|
||||
"description": "Triggers after one or more locks unlock.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::lock::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::lock::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock unlocked"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
homeassistant/components/lock/trigger.py
Normal file
18
homeassistant/components/lock/trigger.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Provides triggers for locks."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN, LockState
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"jammed": make_entity_target_state_trigger(DOMAIN, LockState.JAMMED),
|
||||
"locked": make_entity_target_state_trigger(DOMAIN, LockState.LOCKED),
|
||||
"opened": make_entity_target_state_trigger(DOMAIN, LockState.OPEN),
|
||||
"unlocked": make_entity_target_state_trigger(DOMAIN, LockState.UNLOCKED),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for locks."""
|
||||
return TRIGGERS
|
||||
20
homeassistant/components/lock/triggers.yaml
Normal file
20
homeassistant/components/lock/triggers.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: lock
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
jammed: *trigger_common
|
||||
locked: *trigger_common
|
||||
opened: *trigger_common
|
||||
unlocked: *trigger_common
|
||||
@@ -14,5 +14,13 @@
|
||||
"turn_on": {
|
||||
"service": "mdi:bullhorn"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:bullhorn-outline"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:bullhorn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted sirens to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::siren::title%]",
|
||||
@@ -13,6 +17,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"toggle": {
|
||||
"description": "Toggles the siren on/off.",
|
||||
@@ -41,5 +54,27 @@
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Siren"
|
||||
"title": "Siren",
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"description": "Triggers after one or more sirens turn off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::siren::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::siren::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Siren turned off"
|
||||
},
|
||||
"turned_on": {
|
||||
"description": "Triggers after one or more sirens turn on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::siren::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::siren::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Siren turned on"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
homeassistant/components/siren/trigger.py
Normal file
17
homeassistant/components/siren/trigger.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Provides triggers for sirens."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for sirens."""
|
||||
return TRIGGERS
|
||||
18
homeassistant/components/siren/triggers.yaml
Normal file
18
homeassistant/components/siren/triggers.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: siren
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
@@ -31,6 +31,7 @@ from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import section
|
||||
@@ -132,6 +133,15 @@ from .vacuum import (
|
||||
SERVICE_STOP,
|
||||
async_create_preview_vacuum,
|
||||
)
|
||||
from .weather import (
|
||||
CONF_CONDITION,
|
||||
CONF_FORECAST_DAILY,
|
||||
CONF_FORECAST_HOURLY,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE as CONF_WEATHER_TEMPERATURE,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
async_create_preview_weather,
|
||||
)
|
||||
|
||||
_SCHEMA_STATE: dict[vol.Marker, Any] = {
|
||||
vol.Required(CONF_STATE): selector.TemplateSelector(),
|
||||
@@ -394,6 +404,22 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
vol.Optional(SERVICE_LOCATE): selector.ActionSelector(),
|
||||
}
|
||||
|
||||
if domain == Platform.WEATHER:
|
||||
schema |= {
|
||||
vol.Required(CONF_CONDITION): selector.TemplateSelector(),
|
||||
vol.Required(CONF_HUMIDITY): selector.TemplateSelector(),
|
||||
vol.Required(CONF_WEATHER_TEMPERATURE): selector.TemplateSelector(),
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in UnitOfTemperature],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
sort=True,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_FORECAST_DAILY): selector.TemplateSelector(),
|
||||
vol.Optional(CONF_FORECAST_HOURLY): selector.TemplateSelector(),
|
||||
}
|
||||
|
||||
schema |= {
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
vol.Optional(CONF_ADVANCED_OPTIONS): section(
|
||||
@@ -414,6 +440,15 @@ options_schema = partial(generate_schema, flow_type="options")
|
||||
config_schema = partial(generate_schema, flow_type="config")
|
||||
|
||||
|
||||
async def _get_forecast_description_place_holders(
|
||||
handler: SchemaCommonFlowHandler,
|
||||
) -> dict[str, str]:
|
||||
return {
|
||||
"daily_link": "https://www.home-assistant.io/integrations/template/#daily-weather-forecast",
|
||||
"hourly_link": "https://www.home-assistant.io/integrations/template/#hourly-weather-forecast",
|
||||
}
|
||||
|
||||
|
||||
async def choose_options_step(options: dict[str, Any]) -> str:
|
||||
"""Return next step_id for options flow according to template_type."""
|
||||
return cast(str, options["template_type"])
|
||||
@@ -511,6 +546,7 @@ TEMPLATE_TYPES = [
|
||||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
Platform.VACUUM,
|
||||
Platform.WEATHER,
|
||||
]
|
||||
|
||||
CONFIG_FLOW = {
|
||||
@@ -589,6 +625,12 @@ CONFIG_FLOW = {
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.VACUUM),
|
||||
),
|
||||
Platform.WEATHER: SchemaFlowFormStep(
|
||||
config_schema(Platform.WEATHER),
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.WEATHER),
|
||||
description_placeholders=_get_forecast_description_place_holders,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -668,6 +710,12 @@ OPTIONS_FLOW = {
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.VACUUM),
|
||||
),
|
||||
Platform.WEATHER: SchemaFlowFormStep(
|
||||
options_schema(Platform.WEATHER),
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.WEATHER),
|
||||
description_placeholders=_get_forecast_description_place_holders,
|
||||
),
|
||||
}
|
||||
|
||||
CREATE_PREVIEW_ENTITY: dict[
|
||||
@@ -687,6 +735,7 @@ CREATE_PREVIEW_ENTITY: dict[
|
||||
Platform.SWITCH: async_create_preview_switch,
|
||||
Platform.UPDATE: async_create_preview_update,
|
||||
Platform.VACUUM: async_create_preview_vacuum,
|
||||
Platform.WEATHER: async_create_preview_weather,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -463,7 +463,8 @@
|
||||
"sensor": "[%key:component::sensor::title%]",
|
||||
"switch": "[%key:component::switch::title%]",
|
||||
"update": "[%key:component::update::title%]",
|
||||
"vacuum": "[%key:component::vacuum::title%]"
|
||||
"vacuum": "[%key:component::vacuum::title%]",
|
||||
"weather": "[%key:component::weather::title%]"
|
||||
},
|
||||
"title": "Template helper"
|
||||
},
|
||||
@@ -507,6 +508,36 @@
|
||||
}
|
||||
},
|
||||
"title": "Template vacuum"
|
||||
},
|
||||
"weather": {
|
||||
"data": {
|
||||
"condition": "Condition",
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"forecast_daily": "Forecast daily",
|
||||
"forecast_hourly": "Forecast hourly",
|
||||
"humidity": "Humidity",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"temperature": "Temperature",
|
||||
"temperature_unit": "Temperature unit"
|
||||
},
|
||||
"data_description": {
|
||||
"condition": "Defines a template to get the current weather condition",
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"forecast_daily": "Defines a template to get the [daily forecast data]({daily_link})",
|
||||
"forecast_hourly": "Defines a template to get the [hourly forecast data]({hourly_link})",
|
||||
"humidity": "Defines a template to get the current humidity",
|
||||
"temperature": "Defines a template to get the current temperature",
|
||||
"temperature_unit": "The temperature unit"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"name": "[%key:component::template::common::advanced_options%]"
|
||||
}
|
||||
},
|
||||
"title": "Template weather"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -995,6 +1026,36 @@
|
||||
}
|
||||
},
|
||||
"title": "[%key:component::template::config::step::vacuum::title%]"
|
||||
},
|
||||
"weather": {
|
||||
"data": {
|
||||
"condition": "[%key:component::template::config::step::weather::data::condition%]",
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"forecast_daily": "[%key:component::template::config::step::weather::data::forecast_daily%]",
|
||||
"forecast_hourly": "[%key:component::template::config::step::weather::data::forecast_hourly%]",
|
||||
"humidity": "[%key:component::template::config::step::weather::data::humidity%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"temperature": "[%key:component::template::config::step::weather::data::temperature%]",
|
||||
"temperature_unit": "[%key:component::template::config::step::weather::data::temperature_unit%]"
|
||||
},
|
||||
"data_description": {
|
||||
"condition": "[%key:component::template::config::step::weather::data_description::condition%]",
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"forecast_daily": "[%key:component::template::config::step::weather::data_description::forecast_daily%]",
|
||||
"forecast_hourly": "[%key:component::template::config::step::weather::data_description::forecast_hourly%]",
|
||||
"humidity": "[%key:component::template::config::step::weather::data_description::humidity%]",
|
||||
"temperature": "[%key:component::template::config::step::weather::data_description::temperature%]",
|
||||
"temperature_unit": "[%key:component::template::config::step::weather::data_description::temperature_unit%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"name": "[%key:component::template::common::advanced_options%]"
|
||||
}
|
||||
},
|
||||
"title": "[%key:component::template::config::step::weather::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
68
homeassistant/components/unifiprotect/quality_scale.yaml
Normal file
68
homeassistant/components/unifiprotect/quality_scale.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: Integration is push-based using WebSockets (iot_class local_push).
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: done
|
||||
comment: Diagnostics tests could use snapshot testing.
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: done
|
||||
comment: "Planned improvement: remove doorbell occupancy binary sensor and keep the event sensor after a solution for https://github.com/home-assistant/core/issues/145941 is available."
|
||||
entity-disabled-by-default: done
|
||||
entity-translations:
|
||||
status: done
|
||||
comment: "Planned improvement: camera insecure is not translated but will be dropped soon with public api migration"
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -48,37 +48,37 @@ _KEY_LIGHT_MOTION = "light_motion"
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
HDR_MODES = [
|
||||
{"id": "always", "name": "Always On"},
|
||||
{"id": "off", "name": "Always Off"},
|
||||
{"id": "auto", "name": "Auto"},
|
||||
{"id": "always", "name": "always"},
|
||||
{"id": "off", "name": "off"},
|
||||
{"id": "auto", "name": "auto"},
|
||||
]
|
||||
|
||||
INFRARED_MODES = [
|
||||
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
|
||||
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
|
||||
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
|
||||
{"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"},
|
||||
{"id": IRLEDMode.OFF.value, "name": "Always Disable"},
|
||||
{"id": IRLEDMode.AUTO.value, "name": "auto"},
|
||||
{"id": IRLEDMode.ON.value, "name": "on"},
|
||||
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "auto_filter_only"},
|
||||
{"id": IRLEDMode.CUSTOM.value, "name": "custom"},
|
||||
{"id": IRLEDMode.OFF.value, "name": "off"},
|
||||
]
|
||||
|
||||
CHIME_TYPES = [
|
||||
{"id": ChimeType.NONE.value, "name": "None"},
|
||||
{"id": ChimeType.MECHANICAL.value, "name": "Mechanical"},
|
||||
{"id": ChimeType.DIGITAL.value, "name": "Digital"},
|
||||
{"id": ChimeType.NONE.value, "name": "none"},
|
||||
{"id": ChimeType.MECHANICAL.value, "name": "mechanical"},
|
||||
{"id": ChimeType.DIGITAL.value, "name": "digital"},
|
||||
]
|
||||
|
||||
MOUNT_TYPES = [
|
||||
{"id": MountType.NONE.value, "name": "None"},
|
||||
{"id": MountType.DOOR.value, "name": "Door"},
|
||||
{"id": MountType.WINDOW.value, "name": "Window"},
|
||||
{"id": MountType.GARAGE.value, "name": "Garage"},
|
||||
{"id": MountType.LEAK.value, "name": "Leak"},
|
||||
{"id": MountType.NONE.value, "name": MountType.NONE.value},
|
||||
{"id": MountType.DOOR.value, "name": MountType.DOOR.value},
|
||||
{"id": MountType.WINDOW.value, "name": MountType.WINDOW.value},
|
||||
{"id": MountType.GARAGE.value, "name": MountType.GARAGE.value},
|
||||
{"id": MountType.LEAK.value, "name": MountType.LEAK.value},
|
||||
]
|
||||
|
||||
LIGHT_MODE_MOTION = "On Motion - Always"
|
||||
LIGHT_MODE_MOTION_DARK = "On Motion - When Dark"
|
||||
LIGHT_MODE_DARK = "When Dark"
|
||||
LIGHT_MODE_OFF = "Manual"
|
||||
LIGHT_MODE_MOTION = "motion"
|
||||
LIGHT_MODE_MOTION_DARK = "motion_dark"
|
||||
LIGHT_MODE_DARK = "when_dark"
|
||||
LIGHT_MODE_OFF = "manual"
|
||||
LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
|
||||
|
||||
LIGHT_MODE_TO_SETTINGS = {
|
||||
@@ -93,13 +93,13 @@ LIGHT_MODE_TO_SETTINGS = {
|
||||
|
||||
MOTION_MODE_TO_LIGHT_MODE = [
|
||||
{"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
|
||||
{"id": f"{LightModeType.MOTION.value}Dark", "name": LIGHT_MODE_MOTION_DARK},
|
||||
{"id": f"{LightModeType.MOTION.value}_dark", "name": LIGHT_MODE_MOTION_DARK},
|
||||
{"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
|
||||
{"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
|
||||
]
|
||||
|
||||
DEVICE_RECORDING_MODES = [
|
||||
{"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode)
|
||||
{"id": mode.value, "name": mode.value} for mode in list(RecordingMode)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -350,31 +350,68 @@
|
||||
},
|
||||
"select": {
|
||||
"chime_type": {
|
||||
"name": "Chime type"
|
||||
"name": "Chime type",
|
||||
"state": {
|
||||
"digital": "Digital",
|
||||
"mechanical": "Mechanical",
|
||||
"none": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"doorbell_text": {
|
||||
"name": "Doorbell text"
|
||||
},
|
||||
"hdr_mode": {
|
||||
"name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]"
|
||||
"name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]",
|
||||
"state": {
|
||||
"always": "Always on",
|
||||
"auto": "Auto",
|
||||
"off": "Always off"
|
||||
}
|
||||
},
|
||||
"infrared_mode": {
|
||||
"name": "Infrared mode"
|
||||
"name": "Infrared mode",
|
||||
"state": {
|
||||
"auto": "Auto",
|
||||
"auto_filter_only": "Auto (filter only, no LEDs)",
|
||||
"custom": "Auto (custom lux)",
|
||||
"off": "Always disable",
|
||||
"on": "Always enable"
|
||||
}
|
||||
},
|
||||
"light_mode": {
|
||||
"name": "Light mode"
|
||||
"name": "Light mode",
|
||||
"state": {
|
||||
"manual": "Manual",
|
||||
"motion": "On motion - always",
|
||||
"motion_dark": "On motion - when dark",
|
||||
"when_dark": "When dark"
|
||||
}
|
||||
},
|
||||
"liveview": {
|
||||
"name": "Liveview"
|
||||
},
|
||||
"mount_type": {
|
||||
"name": "Mount type"
|
||||
"name": "Mount type",
|
||||
"state": {
|
||||
"door": "Door",
|
||||
"garage": "Garage",
|
||||
"leak": "Leak",
|
||||
"none": "[%key:common::state::off%]",
|
||||
"window": "Window"
|
||||
}
|
||||
},
|
||||
"paired_camera": {
|
||||
"name": "Paired camera"
|
||||
},
|
||||
"recording_mode": {
|
||||
"name": "Recording mode"
|
||||
"name": "Recording mode",
|
||||
"state": {
|
||||
"adaptive": "Adaptive",
|
||||
"always": "Always",
|
||||
"detections": "Detections",
|
||||
"never": "Never",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -104,7 +104,7 @@ def async_get_light_motion_current(obj: Light) -> str:
|
||||
obj.light_mode_settings.mode is LightModeType.MOTION
|
||||
and obj.light_mode_settings.enable_at is LightModeEnableType.DARK
|
||||
):
|
||||
return f"{LightModeType.MOTION.value}Dark"
|
||||
return f"{LightModeType.MOTION.value}_dark"
|
||||
return obj.light_mode_settings.mode.value
|
||||
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yale",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["socketio", "engineio", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["yalexs-ble==3.2.2"]
|
||||
"requirements": ["yalexs-ble==3.2.4"]
|
||||
}
|
||||
|
||||
4
requirements_all.txt
generated
4
requirements_all.txt
generated
@@ -1291,7 +1291,7 @@ influxdb==5.3.1
|
||||
inkbird-ble==1.1.1
|
||||
|
||||
# homeassistant.components.insteon
|
||||
insteon-frontend-home-assistant==0.5.0
|
||||
insteon-frontend-home-assistant==0.6.0
|
||||
|
||||
# homeassistant.components.intellifire
|
||||
intellifire4py==4.2.1
|
||||
@@ -3224,7 +3224,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.2
|
||||
yalexs-ble==3.2.4
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
|
||||
4
requirements_test_all.txt
generated
4
requirements_test_all.txt
generated
@@ -1137,7 +1137,7 @@ influxdb==5.3.1
|
||||
inkbird-ble==1.1.1
|
||||
|
||||
# homeassistant.components.insteon
|
||||
insteon-frontend-home-assistant==0.5.0
|
||||
insteon-frontend-home-assistant==0.6.0
|
||||
|
||||
# homeassistant.components.intellifire
|
||||
intellifire4py==4.2.1
|
||||
@@ -2691,7 +2691,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.2
|
||||
yalexs-ble==3.2.4
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
|
||||
@@ -1008,7 +1008,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"unifi",
|
||||
"unifi_direct",
|
||||
"unifiled",
|
||||
"unifiprotect",
|
||||
"universal",
|
||||
"upb",
|
||||
"upc_connect",
|
||||
@@ -2032,7 +2031,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"unifi",
|
||||
"unifi_direct",
|
||||
"unifiled",
|
||||
"unifiprotect",
|
||||
"universal",
|
||||
"upb",
|
||||
"upc_connect",
|
||||
|
||||
@@ -82,9 +82,20 @@ def mock_hikcamera() -> Generator[MagicMock]:
|
||||
None,
|
||||
"2024-01-01T00:00:00Z",
|
||||
)
|
||||
camera.get_event_triggers.return_value = {}
|
||||
yield hikcamera_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hik_nvr(mock_hikcamera: MagicMock) -> MagicMock:
|
||||
"""Return a mocked HikCamera configured as an NVR."""
|
||||
camera = mock_hikcamera.return_value
|
||||
camera.get_type = "NVR"
|
||||
camera.current_event_states = {}
|
||||
camera.get_event_triggers.return_value = {"Motion": [1, 2]}
|
||||
return mock_hikcamera
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_hikcamera: MagicMock
|
||||
|
||||
@@ -89,3 +89,16 @@ async def test_setup_entry_no_device_id(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_entry_nvr_fetches_events(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_hik_nvr: MagicMock,
|
||||
) -> None:
|
||||
"""Test setup fetches NVR events for NVR devices."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_hik_nvr.return_value.get_event_triggers.assert_called_once()
|
||||
mock_hik_nvr.return_value.inject_events.assert_called_once()
|
||||
|
||||
29
tests/components/knx/fixtures/config_store_text.json
Normal file
29
tests/components/knx/fixtures/config_store_text.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 2,
|
||||
"minor_version": 2,
|
||||
"key": "knx/config_store.json",
|
||||
"data": {
|
||||
"entities": {
|
||||
"text": {
|
||||
"knx_es_01KCXYT0WEJEQQ13SGCY1NHCYV": {
|
||||
"entity": {
|
||||
"name": "test",
|
||||
"device_info": null,
|
||||
"entity_category": null
|
||||
},
|
||||
"knx": {
|
||||
"ga_text": {
|
||||
"write": "1/1/1",
|
||||
"dpt": "16.001",
|
||||
"state": "2/2/2",
|
||||
"passive": []
|
||||
},
|
||||
"respond_to_read": false,
|
||||
"sync_state": true,
|
||||
"mode": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1967,6 +1967,69 @@
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_knx_get_schema[text]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': list([
|
||||
dict({
|
||||
'name': 'ga_text',
|
||||
'options': dict({
|
||||
'dptClasses': list([
|
||||
'string',
|
||||
]),
|
||||
'passive': True,
|
||||
'state': dict({
|
||||
'required': False,
|
||||
}),
|
||||
'write': dict({
|
||||
'required': True,
|
||||
}),
|
||||
}),
|
||||
'required': True,
|
||||
'type': 'knx_group_address',
|
||||
}),
|
||||
dict({
|
||||
'default': 'text',
|
||||
'name': 'mode',
|
||||
'required': True,
|
||||
'selector': dict({
|
||||
'select': dict({
|
||||
'custom_value': False,
|
||||
'multiple': False,
|
||||
'options': list([
|
||||
'password',
|
||||
'text',
|
||||
]),
|
||||
'sort': False,
|
||||
'translation_key': 'component.knx.config_panel.entities.create.text.knx.mode',
|
||||
}),
|
||||
}),
|
||||
'type': 'ha_selector',
|
||||
}),
|
||||
dict({
|
||||
'default': False,
|
||||
'name': 'respond_to_read',
|
||||
'optional': True,
|
||||
'required': False,
|
||||
'selector': dict({
|
||||
'boolean': dict({
|
||||
}),
|
||||
}),
|
||||
'type': 'ha_selector',
|
||||
}),
|
||||
dict({
|
||||
'allow_false': False,
|
||||
'default': True,
|
||||
'name': 'sync_state',
|
||||
'optional': True,
|
||||
'required': False,
|
||||
'type': 'knx_sync_state',
|
||||
}),
|
||||
]),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_knx_get_schema[time]
|
||||
dict({
|
||||
'id': 1,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS
|
||||
from homeassistant.components.knx.schema import TextSchema
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.text import TextMode
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
|
||||
from . import KnxEntityGenerator
|
||||
from .conftest import KNXTestKit
|
||||
|
||||
from tests.common import mock_restore_cache
|
||||
@@ -99,3 +101,44 @@ async def test_text_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) ->
|
||||
)
|
||||
state = hass.states.get("text.test")
|
||||
assert state.state == "hallo"
|
||||
|
||||
|
||||
async def test_text_ui_create(
|
||||
hass: HomeAssistant,
|
||||
knx: KNXTestKit,
|
||||
create_ui_entity: KnxEntityGenerator,
|
||||
) -> None:
|
||||
"""Test creating a text."""
|
||||
await knx.setup_integration()
|
||||
await create_ui_entity(
|
||||
platform=Platform.TEXT,
|
||||
entity_data={"name": "test"},
|
||||
knx_data={
|
||||
"ga_text": {"write": "1/1/1", "dpt": "16.000"},
|
||||
"mode": TextMode.PASSWORD,
|
||||
"sync_state": True,
|
||||
},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"text",
|
||||
"set_value",
|
||||
{"entity_id": "text.test", "value": "hallo"},
|
||||
blocking=True,
|
||||
)
|
||||
await knx.assert_write(
|
||||
"1/1/1",
|
||||
(0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0),
|
||||
)
|
||||
knx.assert_state("text.test", "hallo", mode=TextMode.PASSWORD)
|
||||
|
||||
|
||||
async def test_text_ui_load(knx: KNXTestKit) -> None:
|
||||
"""Test loading a text from storage."""
|
||||
await knx.setup_integration(config_store_fixture="config_store_text.json")
|
||||
|
||||
await knx.assert_read("2/2/2")
|
||||
await knx.receive_response(
|
||||
"2/2/2",
|
||||
(0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0),
|
||||
)
|
||||
knx.assert_state("text.test", "hallo", mode=TextMode.TEXT)
|
||||
|
||||
261
tests/components/lock/test_trigger.py
Normal file
261
tests/components/lock/test_trigger.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""Test lock triggers."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.lock import DOMAIN, LockState
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.components import (
|
||||
StateDescription,
|
||||
arm_trigger,
|
||||
other_states,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
||||
def enable_experimental_triggers_conditions() -> Generator[None]:
|
||||
"""Enable experimental triggers and conditions."""
|
||||
with patch(
|
||||
"homeassistant.components.labs.async_is_preview_feature_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_locks(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple lock entities associated with different targets."""
|
||||
return (await target_entities(hass, DOMAIN))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"lock.jammed",
|
||||
"lock.locked",
|
||||
"lock.opened",
|
||||
"lock.unlocked",
|
||||
],
|
||||
)
|
||||
async def test_lock_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the lock triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.jammed",
|
||||
target_states=[LockState.JAMMED],
|
||||
other_states=other_states(LockState.JAMMED),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.locked",
|
||||
target_states=[LockState.LOCKED],
|
||||
other_states=other_states(LockState.LOCKED),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.opened",
|
||||
target_states=[LockState.OPEN],
|
||||
other_states=other_states(LockState.OPEN),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.unlocked",
|
||||
target_states=[LockState.UNLOCKED],
|
||||
other_states=other_states(LockState.UNLOCKED),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_lock_state_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_locks: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the lock state trigger fires when any lock state changes to a specific state."""
|
||||
other_entity_ids = set(target_locks) - {entity_id}
|
||||
|
||||
# Set all locks, including the tested one, to the initial state
|
||||
for eid in target_locks:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Check if changing other locks also triggers
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.jammed",
|
||||
target_states=[LockState.JAMMED],
|
||||
other_states=other_states(LockState.JAMMED),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.locked",
|
||||
target_states=[LockState.LOCKED],
|
||||
other_states=other_states(LockState.LOCKED),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.opened",
|
||||
target_states=[LockState.OPEN],
|
||||
other_states=other_states(LockState.OPEN),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.unlocked",
|
||||
target_states=[LockState.UNLOCKED],
|
||||
other_states=other_states(LockState.UNLOCKED),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_lock_state_trigger_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_locks: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the lock state trigger fires when the first lock changes to a specific state."""
|
||||
other_entity_ids = set(target_locks) - {entity_id}
|
||||
|
||||
# Set all locks, including the tested one, to the initial state
|
||||
for eid in target_locks:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Triggering other locks should not cause the trigger to fire again
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.jammed",
|
||||
target_states=[LockState.JAMMED],
|
||||
other_states=other_states(LockState.JAMMED),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.locked",
|
||||
target_states=[LockState.LOCKED],
|
||||
other_states=other_states(LockState.LOCKED),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.opened",
|
||||
target_states=[LockState.OPEN],
|
||||
other_states=other_states(LockState.OPEN),
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="lock.unlocked",
|
||||
target_states=[LockState.UNLOCKED],
|
||||
other_states=other_states(LockState.UNLOCKED),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_lock_state_trigger_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_locks: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the lock state trigger fires when the last lock changes to a specific state."""
|
||||
other_entity_ids = set(target_locks) - {entity_id}
|
||||
|
||||
# Set all locks, including the tested one, to the initial state
|
||||
for eid in target_locks:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
228
tests/components/siren/test_trigger.py
Normal file
228
tests/components/siren/test_trigger.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""Test siren triggers."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.siren import DOMAIN
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.components import (
|
||||
StateDescription,
|
||||
arm_trigger,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
||||
def enable_experimental_triggers_conditions() -> Generator[None]:
|
||||
"""Enable experimental triggers and conditions."""
|
||||
with patch(
|
||||
"homeassistant.components.labs.async_is_preview_feature_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_sirens(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple siren entities associated with different targets."""
|
||||
return (await target_entities(hass, DOMAIN))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"siren.turned_off",
|
||||
"siren.turned_on",
|
||||
],
|
||||
)
|
||||
async def test_siren_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the siren triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="siren.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="siren.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_siren_state_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_sirens: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the siren state trigger fires when any siren state changes to a specific state."""
|
||||
other_entity_ids = set(target_sirens) - {entity_id}
|
||||
|
||||
# Set all sirens, including the tested one, to the initial state
|
||||
for eid in target_sirens:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Check if changing other sirens also triggers
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="siren.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="siren.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_siren_state_trigger_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_sirens: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the siren state trigger fires when the first siren changes to a specific state."""
|
||||
other_entity_ids = set(target_sirens) - {entity_id}
|
||||
|
||||
# Set all sirens, including the tested one, to the initial state
|
||||
for eid in target_sirens:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Triggering other sirens should not cause the trigger to fire again
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="siren.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="siren.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_siren_state_trigger_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_sirens: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the siren state trigger fires when the last siren changes to a specific state."""
|
||||
other_entity_ids = set(target_sirens) - {entity_id}
|
||||
|
||||
# Set all sirens, including the tested one, to the initial state
|
||||
for eid in target_sirens:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
@@ -53,6 +53,29 @@
|
||||
'last_wind_speed': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_config_entry
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'assumed_state': True,
|
||||
'attribution': 'Powered by Home Assistant',
|
||||
'friendly_name': 'My template',
|
||||
'humidity': 50,
|
||||
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
'pressure_unit': <UnitOfPressure.HPA: 'hPa'>,
|
||||
'supported_features': 0,
|
||||
'temperature': 20.0,
|
||||
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
'visibility_unit': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
'wind_speed_unit': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'weather.my_template',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_trigger_weather_services[config0-1-template-get_forecasts]
|
||||
dict({
|
||||
'weather.test': dict({
|
||||
|
||||
@@ -270,6 +270,16 @@ BINARY_SENSOR_OPTIONS = {
|
||||
{"start": []},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"weather",
|
||||
{"condition": "{{ states('weather.one') }}"},
|
||||
"sunny",
|
||||
{"one": "sunny", "two": "cloudy"},
|
||||
{},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
{},
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
|
||||
@@ -463,6 +473,12 @@ async def test_config_flow(
|
||||
{"start": []},
|
||||
{"start": []},
|
||||
),
|
||||
(
|
||||
"weather",
|
||||
{"condition": "{{ states('weather.one') }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_config_flow_device(
|
||||
@@ -752,6 +768,16 @@ async def test_config_flow_device(
|
||||
{"start": []},
|
||||
"state",
|
||||
),
|
||||
(
|
||||
"weather",
|
||||
{"condition": "{{ states('weather.one') }}"},
|
||||
{"condition": "{{ states('weather.two') }}"},
|
||||
["sunny", "cloudy"],
|
||||
{"one": "sunny", "two": "cloudy"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
"condition",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
|
||||
@@ -1601,6 +1627,12 @@ async def test_option_flow_sensor_preview_config_entry_removed(
|
||||
{"start": []},
|
||||
{"start": []},
|
||||
),
|
||||
(
|
||||
"weather",
|
||||
{"condition": "{{ states('weather.one') }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_options_flow_change_device(
|
||||
|
||||
@@ -37,13 +37,15 @@ from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import ConfigurationStyle
|
||||
from .conftest import ConfigurationStyle, async_get_flow_preview_state
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
assert_setup_component,
|
||||
async_mock_restore_state_shutdown_restart,
|
||||
mock_restore_cache_with_extra_data,
|
||||
)
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
ATTR_FORECAST = "forecast"
|
||||
|
||||
@@ -122,6 +124,27 @@ async def setup_weather(
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_weather_single_attribute(
|
||||
hass: HomeAssistant,
|
||||
count: int,
|
||||
style: ConfigurationStyle,
|
||||
attribute: str,
|
||||
attribute_template: str,
|
||||
weather_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Do setup of weather integration."""
|
||||
extra = {attribute: attribute_template}
|
||||
if style == ConfigurationStyle.MODERN:
|
||||
await async_setup_modern_format(
|
||||
hass, count, {"name": TEST_OBJECT_ID, **weather_config, **extra}
|
||||
)
|
||||
if style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass, count, {"name": TEST_OBJECT_ID, **weather_config, **extra}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
@@ -1129,3 +1152,58 @@ async def test_templated_optional_config(
|
||||
state = hass.states.get(TEST_WEATHER)
|
||||
|
||||
assert state.attributes[attribute] == expected
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Tests creating a weather from a config entry."""
|
||||
|
||||
hass.states.async_set(
|
||||
"weather.test_state",
|
||||
"sunny",
|
||||
{},
|
||||
)
|
||||
|
||||
template_config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=template.DOMAIN,
|
||||
options={
|
||||
"name": "My template",
|
||||
"condition": "{{ states('sensor.test_sensor') }}",
|
||||
"humidity": "{{ 50 }}",
|
||||
"temperature": "{{ 20 }}",
|
||||
"template_type": WEATHER_DOMAIN,
|
||||
},
|
||||
title="My template",
|
||||
)
|
||||
template_config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(template_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.my_template")
|
||||
assert state is not None
|
||||
assert state == snapshot
|
||||
|
||||
|
||||
async def test_flow_preview(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test the config flow preview."""
|
||||
|
||||
state = await async_get_flow_preview_state(
|
||||
hass,
|
||||
hass_ws_client,
|
||||
WEATHER_DOMAIN,
|
||||
{
|
||||
"name": "My template",
|
||||
"condition": "{{ 'sunny' }}",
|
||||
"humidity": "{{ 50 }}",
|
||||
"temperature": "{{ 20 }}",
|
||||
},
|
||||
)
|
||||
|
||||
assert state["state"] == "sunny"
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_select_setup_light(
|
||||
await init_entry(hass, ufp, [light])
|
||||
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
||||
|
||||
expected_values = ("On Motion - When Dark", "Not Paired")
|
||||
expected_values = ("motion_dark", "Not Paired")
|
||||
|
||||
for index, description in enumerate(LIGHT_SELECTS):
|
||||
unique_id, entity_id = await ids_from_device_description(
|
||||
@@ -153,11 +153,11 @@ async def test_select_setup_camera_all(
|
||||
assert_entity_counts(hass, Platform.SELECT, 5, 5)
|
||||
|
||||
expected_values = (
|
||||
"Always",
|
||||
"Auto",
|
||||
"always",
|
||||
"auto",
|
||||
"Default Message (Welcome)",
|
||||
"None",
|
||||
"Always Off",
|
||||
"none",
|
||||
"off",
|
||||
)
|
||||
|
||||
for index, description in enumerate(CAMERA_SELECTS):
|
||||
@@ -186,7 +186,7 @@ async def test_select_setup_camera_none(
|
||||
await init_entry(hass, ufp, [camera])
|
||||
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
||||
|
||||
expected_values = ("Always", "Auto", "Default Message (Welcome)")
|
||||
expected_values = ("always", "auto", "Default Message (Welcome)")
|
||||
|
||||
for index, description in enumerate(CAMERA_SELECTS):
|
||||
if index == 2:
|
||||
@@ -403,7 +403,7 @@ async def test_select_set_option_camera_recording(
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Never"},
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "never"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -428,7 +428,7 @@ async def test_select_set_option_camera_ir(
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always Enable"},
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "on"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user