feat: Align automations with Home Assistant triggers and conditions (#2527)
Split automations into HA-style `triggers`, ongoing `conditions`, and
`actions`, with compatibility migrations for existing Advanced Camera
Card configs.
## Summary
At a glance (details below):
- **Added** `triggers:` -- a required, HA-shaped block: stock `state` /
`numeric_state` / `template` plus card-specific triggers (`camera`,
`view`, `fullscreen`, ...).
- **Added** the HA-native `if` / `then` / `else` action.
- **Removed** `actions_not` (replaced by `if` / `then` / `else`).
- **Removed** the ambient `advanced_camera_card` template namespace (use
`acc` instead).
- **Changed** the trigger template surface to a top-level `trigger.*`
variable (as in HA); the nested `acc.trigger.*` paths are removed.
- **Changed** `conditions:` to ongoing gates only -- they no longer wake
an automation, and change-only forms (`config`, valueless `camera` /
`view` / `state`) become triggers, not conditions.
- **Changed** action templates to render per step, so a later action
sees state an earlier one changed.
- **Compatibility:** HA-shaped YAML is accepted (singular keys,
single-or-list, `and` / `or` / `not` shorthand, `entity` / `entity_id`).
- **Migration:** existing configs upgrade automatically; anything that
cannot be converted faithfully is recorded under `__UPGRADE_FAILURE__`
for manual fixup.
## Breaking Changes
### 1. Automations now require triggers
Before this PR, `automations[].conditions` served two roles:
- They decided whether the automation should run.
- They also acted as the thing that woke the automation up.
After this PR:
- `triggers` wake the automation.
- `conditions` only gate it at the instant a trigger fires.
Most existing automations are migrated automatically from `conditions:`
to `triggers:`.
### 2. `actions_not` is retired
Legacy `actions_not` is replaced by an HA-style `if` action with `then`
/ `else`.
Faithful conversions are automatic. Cases that cannot be faithfully
converted are recorded under `__UPGRADE_FAILURE__.automations` and must
be migrated manually.
### 3. Template surface aligned with Home Assistant
Two related template changes, both auto-migrated:
- **Top-level `trigger.*`.** Automation actions now receive a top-level
`trigger` template variable, like Home Assistant. Legacy nested paths
such as `acc.trigger.state.to` and
`advanced_camera_card.trigger.camera.to` are migrated automatically when
they appear inside template strings.
- **The ambient `advanced_camera_card` template namespace is removed.**
The long-form ambient namespace (`advanced_camera_card.camera`,
`advanced_camera_card.view`, `advanced_camera_card.config`) is retired
in favour of its shorter `acc` alias -- supported since v7.1.0, and the
only spelling the new trigger surface uses. Existing templates are
migrated automatically by rewriting the `advanced_camera_card.` prefix
to `acc.`.
### 4. Trigger-only condition forms are no longer valid conditions
Some legacy "conditions" were really change detectors. These are now
triggers only:
- `condition: config`
- valueless `camera`
- valueless `view`
- valueless `state` / picture-elements state condition with neither
`state` nor `state_not`
These are automatically promoted in automations and stripped from
overrides/elements where they would no longer be meaningful as ongoing
conditions.
### 5. Template truthiness now follows Home Assistant behavior
Template conditions and template triggers intentionally use different
truthiness rules, matching HA:
- A template condition passes only when the rendered value is `true`
(case-insensitive), matching HA's `condition.py`.
- A template trigger uses HA's broader `result_as_boolean` coercion: a
non-zero number, or `1` / `true` / `yes` / `on` / `enable`
(case-insensitive), counts as true.
### 6. Action templates render when each action executes
Action templates are now rendered per action step, not once for the
whole sequence. This means a later action can see card-local state
changed by an earlier action in the same sequence.
The `trigger` context is fixed for the automation run. HA entity state
updates still depend on the frontend receiving updated HASS state over
the websocket.
## Automatic Migrations
### Automation `conditions:` to `triggers:`
Simple legacy automation:
```yaml
# Before
automations:
- conditions:
- condition: fullscreen
fullscreen: true
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: substream_on
```
```yaml
# After, automatic
automations:
- triggers:
- trigger: fullscreen
fullscreen: true
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: substream_on
```
State conditions become HA-style state triggers:
```yaml
# Before
automations:
- conditions:
- condition: state
entity_id: binary_sensor.front_door
state: 'on'
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: live
```
```yaml
# After, automatic
automations:
- triggers:
- trigger: state
entity_id: binary_sensor.front_door
to: 'on'
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: live
```
Multiple conditions become both triggers and ongoing conditions:
```yaml
# Before
automations:
- conditions:
- condition: camera
cameras: [front_door]
- condition: fullscreen
fullscreen: true
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: substream_on
```
```yaml
# After, automatic
automations:
- triggers:
- trigger: camera
cameras: [front_door]
- trigger: fullscreen
fullscreen: true
conditions:
- condition: camera
cameras: [front_door]
- condition: fullscreen
fullscreen: true
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: substream_on
```
The flattened trigger list is an implicit OR. The retained `conditions:`
list is an implicit AND checked when any trigger fires.
### Trigger-only legacy conditions
Legacy `config` conditions become `config` triggers:
```yaml
# Before
automations:
- conditions:
- condition: config
paths: [menu.style]
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: status_bar
```
```yaml
# After, automatic
automations:
- triggers:
- trigger: config
paths: [menu.style]
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: status_bar
```
Trigger-only leaves are removed from retained `conditions:` blocks
because they no longer describe an ongoing state.
### `actions_not` to `if` / `then` / `else`
```yaml
# Before
automations:
- conditions:
- condition: state
entity_id: input_boolean.camera_alerts
state: 'on'
actions:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: live
actions_not:
- action: none
```
```yaml
# After, automatic
automations:
- triggers:
- trigger: state
entity_id: input_boolean.camera_alerts
actions:
- if:
- condition: state
entity_id: input_boolean.camera_alerts
state: 'on'
then:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: live
else:
- action: none
```
If the legacy automation had no conditions, or only trigger-only
conditions, `actions_not` is dropped because the old `else` branch could
not be reproduced as an ongoing predicate.
### Trigger template paths
```yaml
# Before
message: 'Door is {{ acc.trigger.state.to }} from {{ acc.trigger.state.from }}'
```
```yaml
# After, automatic
message: 'Door is {{ trigger.to_state.state }} from {{ trigger.from_state.state }}'
```
Path rewrites performed automatically:
| Old path | New path |
| -------------------------- | -------------------------- |
| `acc.trigger.state.entity` | `trigger.entity_id` |
| `acc.trigger.state.from` | `trigger.from_state.state` |
| `acc.trigger.state.to` | `trigger.to_state.state` |
| `acc.trigger.camera.from` | `trigger.from_acc.camera` |
| `acc.trigger.camera.to` | `trigger.to_acc.camera` |
| `acc.trigger.view.from` | `trigger.from_acc.view` |
| `acc.trigger.view.to` | `trigger.to_acc.view` |
| `acc.trigger.config.from` | `trigger.from_acc.config` |
| `acc.trigger.config.to` | `trigger.to_acc.config` |
The same rewrites are applied for the older
`advanced_camera_card.trigger.*` namespace.
### Ambient template namespace
Any remaining long-form ambient `advanced_camera_card.*` references
(outside the trigger surface) are rewritten to the `acc.*` alias:
```yaml
# Before
title: 'Now viewing {{ advanced_camera_card.camera }}'
```
```yaml
# After, automatic
title: 'Now viewing {{ acc.camera }}'
```
## Manual Migration Cases
### `__UPGRADE_FAILURE__.automations`
If a legacy automation cannot be converted faithfully, the original
automation is recorded under:
```yaml
__UPGRADE_FAILURE__:
automations:
- ...
```
These entries require manual migration.
The main known case is legacy `actions_not` with a condition whose
trigger can only fire on a rising edge, such as:
- `condition: template`
- `condition: screen`
- `condition: numeric_state` without an entity-backed state to watch
Those conditions can start the `then` branch, but cannot reliably start
the `else` branch when they stop matching.
### Unsupported HA conditions and triggers
This PR aligns the card with HA where supported, but it is not a full HA
automation engine.
Unsupported HA condition families include:
- `time`
- `zone`
- `sun`
- `location`
- `device`
- `condition: trigger`
Unsupported HA trigger platforms include:
- `event`
- `time`
- `time_pattern`
- `sun`
- `zone`
- `calendar`
- `webhook`
- `tag`
- `device`
- `mqtt`
The card-specific camera `triggers:` feature (which auto-selects and
wakes the card on camera events such as motion) is a separate feature
from automation `triggers:`, despite the shared word.
### Trigger IDs and variables
HA keys such as `id`, `alias`, and `variables` are accepted so pasted HA
YAML validates, but they are ignored by the card. There is no
`trigger.id` support in this PR.
## New Compatibility Features
This PR also makes card config more forgiving for HA-style YAML:
- `trigger`, `condition`, and `action` singular keys are accepted and
normalized to `triggers`, `conditions`, and `actions`.
- Single trigger, condition, and action objects are accepted where lists
are expected.
- `if`, `then`, and `else` accept a single item or a list.
- Composite condition shorthand is accepted:
- `{ and: [...] }`
- `{ or: [...] }`
- `{ not: [...] }`
- `{ condition: [...] }` as an implicit AND
- State conditions resolve expected state values that name another
entity, matching HA/Lovelace behavior.
- Both `entity` and `entity_id` are accepted on state and numeric
conditions and triggers (a superset of HA's two dialects), so there is
no forced rename.
- `state_not` remains supported as a card/Lovelace-friendly extension.
## Trigger Payloads
Automation action templates receive a top-level `trigger` object.
For stock `state` and `numeric_state` triggers:
```yaml
trigger.platform
trigger.entity_id
trigger.entity
trigger.from_state
trigger.to_state
```
For template triggers:
```yaml
trigger.platform
```
For card-specific triggers:
```yaml
trigger.platform # "acc"
trigger.type
trigger.from_acc
trigger.to_acc
```
The card does not currently expose HA's `id`, `idx`, `for`, `attribute`,
`above`, `below`, or `alias` trigger fields.
BREAKING CHANGE: Automations now follow Home Assistant's `triggers:` /
`conditions:` / `actions:` model. Automations require a `triggers:`
block and `conditions:` no longer wake an automation; `actions_not` is
removed in favour of an `if` / `then` / `else` action; the nested
`acc.trigger.*` template paths and the ambient `advanced_camera_card`
template namespace are removed (use the top-level `trigger.*` surface
and the `acc` alias); trigger-only condition forms (`config`, valueless
`camera` / `view` / `state`) are no longer valid conditions; and
template-condition vs template-trigger truthiness now follow HA.
Existing configs are upgraded automatically where a faithful conversion
exists; anything that cannot be converted is recorded under
`__UPGRADE_FAILURE__` for manual migration.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
dermotduffy
co-authored by
Claude Opus 4.8
parent
209c873c58
commit
b701366762
@@ -53,7 +53,7 @@ render and can operate in `image` or `folders` views depending on configuration.
|
||||
|
||||
#### Common configuration blocks
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------------ | --------------------- |
|
||||
| [`actions`](actions/README.md) | Configure actions. |
|
||||
| [`conditions`](conditions.md) | Configure conditions. |
|
||||
| Option | Description |
|
||||
| -------------------------------------- | --------------------- |
|
||||
| [`actions`](actions/README.md) | Configure actions. |
|
||||
| [`conditions`](conditions-triggers.md) | Configure conditions. |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- [`actions`](actions/README.md)
|
||||
- [`automations`](automations.md)
|
||||
- [`cameras`](cameras/README.md)
|
||||
- [`conditions`](conditions.md)
|
||||
- [`conditions` / `triggers`](conditions-triggers.md)
|
||||
- [`dimensions`](dimensions.md)
|
||||
- [`elements`](elements/README.md)
|
||||
- [`folders`](folders.md)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [Stock Actions](./stock/README.md)
|
||||
- [`automations`](../automations.md)
|
||||
- [`cameras`](../cameras/README.md)
|
||||
- [`conditions`](../conditions.md)
|
||||
- [`conditions` / `triggers`](../conditions-triggers.md)
|
||||
- [`dimensions`](../dimensions.md)
|
||||
- [`elements`](../elements/README.md)
|
||||
- [`folders`](../folders.md)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [Stock Actions](../stock/README.md)
|
||||
- [`automations`](../../automations.md)
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`conditions` / `triggers`](../../conditions-triggers.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [`folders`](../../folders.md)
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# Stock Actions
|
||||
|
||||
## `if` / `then` / `else`
|
||||
|
||||
Run one sequence of actions or another depending on a set of
|
||||
[conditions](../../conditions-triggers.md). This action has no `action:` key: it
|
||||
is identified by the presence of an `if` key, exactly as in [Home Assistant
|
||||
script syntax](https://www.home-assistant.io/docs/scripts/#if-then). The `then`
|
||||
sequence runs when all `if` conditions hold; the optional `else` sequence runs
|
||||
otherwise.
|
||||
|
||||
```yaml
|
||||
if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.notify_enabled
|
||||
state: 'on'
|
||||
then:
|
||||
- action: fire-dom-event
|
||||
advanced_camera_card_action: live_substream_on
|
||||
else:
|
||||
- action: fire-dom-event
|
||||
advanced_camera_card_action: live_substream_off
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ----------------------------------------------------------------- |
|
||||
| `if` | A list of [conditions](../../conditions-triggers.md) to evaluate. |
|
||||
| `then` | A list of actions to run when all `if` conditions hold. |
|
||||
| `else` | An optional list of actions to run when the `if` conditions fail. |
|
||||
|
||||
## `more-info`
|
||||
|
||||
Open the "more-info" dialog for an entity. See [Home Assistant actions documentation](https://www.home-assistant.io/dashboards/actions/).
|
||||
@@ -117,4 +145,21 @@ elements:
|
||||
tap_action:
|
||||
action: fire-dom-event
|
||||
key: value
|
||||
- type: icon
|
||||
icon: mdi:numeric-8-box
|
||||
title: If / then / else action
|
||||
style:
|
||||
left: 200px
|
||||
top: 400px
|
||||
tap_action:
|
||||
if:
|
||||
- condition: state
|
||||
entity_id: light.office_main_lights
|
||||
state: 'on'
|
||||
then:
|
||||
- action: fire-dom-event
|
||||
advanced_camera_card_action: live_substream_on
|
||||
else:
|
||||
- action: fire-dom-event
|
||||
advanced_camera_card_action: live_substream_off
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [Stock Actions](README.md)
|
||||
- [`automations`](../../automations.md)
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`conditions` / `triggers`](../../conditions-triggers.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [`folders`](../../folders.md)
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
# `automations`
|
||||
|
||||
Automatically take [actions](actions/README.md) based on [conditions](conditions.md) being met.
|
||||
Automatically run [actions](actions/README.md) in response to
|
||||
[triggers](conditions-triggers.md), optionally gated by [conditions](conditions-triggers.md).
|
||||
|
||||
> [!TIP]
|
||||
> To change configuration conditionally use [overrides](overrides.md).
|
||||
> To change configuration conditionally, use [overrides](overrides.md) instead.
|
||||
|
||||
An automation has three parts, mirroring a Home Assistant automation:
|
||||
|
||||
- **`triggers:`** are the momentary occurrences that start the automation
|
||||
(required). Multiple triggers are independent: any one firing runs the
|
||||
automation (an implicit "or").
|
||||
- **`conditions:`** are ongoing predicates checked the instant a trigger fires;
|
||||
they must _all_ hold for `actions` to run (optional).
|
||||
- **`actions:`** are what runs when a trigger fires and the conditions hold
|
||||
(required).
|
||||
|
||||
```yaml
|
||||
automations:
|
||||
- conditions:
|
||||
- triggers:
|
||||
- [trigger]
|
||||
conditions:
|
||||
- [condition]
|
||||
actions:
|
||||
- [action]
|
||||
actions_not:
|
||||
- [action]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `conditions` | | A list of [conditions](conditions.md) that must evaluate to `true` in order to trigger the automation. |
|
||||
| `actions` | | An optional list of [actions](actions/README.md) that will be run when the [conditions](conditions.md) evaluate `true`. |
|
||||
| `actions_not` | | An optional list of [actions](actions/README.md) that will be run when the [conditions](conditions.md) evaluate `false`. |
|
||||
| Option | Default | Description |
|
||||
| ------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `triggers` | | A list of [triggers](conditions-triggers.md) that initiate the automation. At least one is required. |
|
||||
| `conditions` | | An optional list of [conditions](conditions-triggers.md) that must _all_ evaluate `true` at the instant a trigger fires for `actions` to run. |
|
||||
| `actions` | | A list of [actions](actions/README.md) run when a trigger fires and the conditions hold (or no conditions are configured). |
|
||||
|
||||
# Fully expanded reference
|
||||
|
||||
@@ -27,13 +38,14 @@ automations:
|
||||
|
||||
```yaml
|
||||
automations:
|
||||
- conditions:
|
||||
- triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.front_door
|
||||
to: 'on'
|
||||
conditions:
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: substream_on
|
||||
actions_not:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: substream_off
|
||||
```
|
||||
|
||||
@@ -16,24 +16,24 @@ cameras_global:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `always_error_if_entity_unavailable` | `false` | When `true` and when `camera_entity` is specified, attempting to live stream this camera will always error out if the entity state is `unavailable`, even if the `live_provider` does not actually need the `camera_entity`. |
|
||||
| `camera_entity` | | The Home Assistant camera entity. Used by most live providers for live stream data, and to auto-detect other camera metadata (e.g. Frigate camera name, camera title/icon). |
|
||||
| `capabilities` | | Allows selective disabling of camera capabilities. See [`capabilities`](#capabilities). |
|
||||
| `cast` | | Configuration that controls how this camera is "casted" / sent to media players. See [`cast`](#cast). |
|
||||
| `dependencies` | | Other cameras that this camera should depend upon. See [`dependencies`](#dependencies). |
|
||||
| `dimensions` | | Controls the dimensions and layout for media from this camera. See [`dimensions`](#dimensions). |
|
||||
| `engine` | `auto` | The camera engine to use. If `auto` the card will attempt to choose the correct engine from the specified options. See [Engine](engine.md). |
|
||||
| `frigate` | | Options for Frigate cameras. See [Frigate camera engine configuration](engine.md?id=frigate). |
|
||||
| `icon` | Autodetected from `camera_entity` if that is specified. | The icon to use for this camera in the camera menu and in the next & previous controls when using the `icon` style. |
|
||||
| `id` | `camera_entity`, `webrtc_card.entity` or `frigate.camera_name` if set (in that preference order). If none of these are set, the camera has no `id` and cannot be referenced by name in conditions or actions. | An optional identifier to use throughout the card configuration to refer unambiguously to this camera. This `id` may be used in [conditions](../conditions.md), dependencies or custom [actions](../actions/README.md) to refer to a given camera unambiguously. |
|
||||
| `live_provider` | `auto` | The choice of live stream provider. See [Live Provider](live-provider.md). |
|
||||
| `media` | | Controls the default media configuration (e.g. thumbnails) for this camera. See [`media`](#media). |
|
||||
| `proxy` | | Controls whether/how content is proxied via [hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) (must be installed separately). See [`proxy`](#proxy). |
|
||||
| `title` | Autodetected from `camera_entity` if that is specified. | A friendly name for this camera to use in the card. |
|
||||
| `triggers` | | Define what should cause this camera to update/trigger. See [`triggers`](#triggers). |
|
||||
| `webrtc_card` | | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See [Live Provider](live-provider.md?id=webrtc_card). |
|
||||
| Option | Default | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `always_error_if_entity_unavailable` | `false` | When `true` and when `camera_entity` is specified, attempting to live stream this camera will always error out if the entity state is `unavailable`, even if the `live_provider` does not actually need the `camera_entity`. |
|
||||
| `camera_entity` | | The Home Assistant camera entity. Used by most live providers for live stream data, and to auto-detect other camera metadata (e.g. Frigate camera name, camera title/icon). |
|
||||
| `capabilities` | | Allows selective disabling of camera capabilities. See [`capabilities`](#capabilities). |
|
||||
| `cast` | | Configuration that controls how this camera is "casted" / sent to media players. See [`cast`](#cast). |
|
||||
| `dependencies` | | Other cameras that this camera should depend upon. See [`dependencies`](#dependencies). |
|
||||
| `dimensions` | | Controls the dimensions and layout for media from this camera. See [`dimensions`](#dimensions). |
|
||||
| `engine` | `auto` | The camera engine to use. If `auto` the card will attempt to choose the correct engine from the specified options. See [Engine](engine.md). |
|
||||
| `frigate` | | Options for Frigate cameras. See [Frigate camera engine configuration](engine.md?id=frigate). |
|
||||
| `icon` | Autodetected from `camera_entity` if that is specified. | The icon to use for this camera in the camera menu and in the next & previous controls when using the `icon` style. |
|
||||
| `id` | `camera_entity`, `webrtc_card.entity` or `frigate.camera_name` if set (in that preference order). If none of these are set, the camera has no `id` and cannot be referenced by name in conditions or actions. | An optional identifier to use throughout the card configuration to refer unambiguously to this camera. This `id` may be used in [conditions](../conditions-triggers.md), dependencies or custom [actions](../actions/README.md) to refer to a given camera unambiguously. |
|
||||
| `live_provider` | `auto` | The choice of live stream provider. See [Live Provider](live-provider.md). |
|
||||
| `media` | | Controls the default media configuration (e.g. thumbnails) for this camera. See [`media`](#media). |
|
||||
| `proxy` | | Controls whether/how content is proxied via [hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) (must be installed separately). See [`proxy`](#proxy). |
|
||||
| `title` | Autodetected from `camera_entity` if that is specified. | A friendly name for this camera to use in the card. |
|
||||
| `triggers` | | Define what should cause this camera to update/trigger. See [`triggers`](#triggers). |
|
||||
| `webrtc_card` | | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See [Live Provider](live-provider.md?id=webrtc_card). |
|
||||
|
||||
## `capabilities`
|
||||
|
||||
@@ -374,6 +374,13 @@ to activate an action (e.g. view a camera in live, reset the card to the default
|
||||
view). See [`view.triggers`](../view.md?id=triggers) to control what happens when a
|
||||
camera is triggered.
|
||||
|
||||
> [!TIP]
|
||||
> A camera's `triggers` are not the same as an
|
||||
> [automation's](../automations.md) [`triggers`](../conditions-triggers.md). Camera
|
||||
> triggers take action on per-camera events such as motion; automation triggers
|
||||
> _initiate [automations](../automations.md)_. They share only the word
|
||||
> "trigger".
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [`cameras`](README.md)
|
||||
- [`live_provider`](live-provider.md)
|
||||
- [`engine`](engine.md)
|
||||
- [`conditions`](../conditions.md)
|
||||
- [`conditions` / `triggers`](../conditions-triggers.md)
|
||||
- [`dimensions`](../dimensions.md)
|
||||
- [`folders`](../folders.md)
|
||||
- [`elements`](../elements/README.md)
|
||||
|
||||
@@ -0,0 +1,793 @@
|
||||
# Conditions & triggers
|
||||
|
||||
Conditions and triggers are designed to mirror Home Assistant's own
|
||||
[conditions](https://www.home-assistant.io/docs/scripts/conditions/) and
|
||||
[triggers](https://www.home-assistant.io/docs/automation/trigger/) as closely as
|
||||
possible: for the standard types Home Assistant's own documentation applies, and
|
||||
you can copy conditions and triggers straight out of an existing Home Assistant
|
||||
automation. The card adds a number of card-specific types, and is a little more
|
||||
permissive in places; any differences are noted per type below.
|
||||
|
||||
A **trigger** is what wakes an [automation](automations.md) up. The moment a
|
||||
trigger fires, the card checks any **conditions** you have set, and if they all
|
||||
pass it runs the [actions](actions/README.md). The two therefore play different
|
||||
roles:
|
||||
|
||||
- A **trigger** is a _momentary_ occurrence. Used only under `triggers:`, and
|
||||
only in automations.
|
||||
- A **condition** is an _ongoing_ predicate, true or false at a point in time.
|
||||
Besides gating automations (checked the instant a trigger fires), conditions
|
||||
also drive [overrides](overrides.md) and [picture elements](elements/README.md).
|
||||
|
||||
The same type can usually be used either way, but the meaning differs: as a
|
||||
**condition** it asks _"is this true right now?"_; as a **trigger** it fires
|
||||
_"when this becomes true"_. A few types are restricted to one role (`config` is
|
||||
trigger-only; the composites and `user` / `user_agent` are condition-only), as
|
||||
noted at the top of each type below.
|
||||
|
||||
For the card-state types (`camera`, `view`, `fullscreen`, `expand`, `call`,
|
||||
`display_mode`, `media_loaded`, `microphone`, `interaction`, `triggered`) a
|
||||
trigger's value is **optional**: give it a value to fire only when the state
|
||||
changes _to_ that value, or **omit it to fire on any change**. (The stock `state`
|
||||
trigger behaves the same way when `from`/`to` are omitted). As a condition the
|
||||
value keeps its usual per-type meaning, as described below.
|
||||
|
||||
```yaml
|
||||
# A trigger initiates an automation; conditions are then checked.
|
||||
triggers:
|
||||
- [trigger_1]
|
||||
conditions:
|
||||
- [condition_1]
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Automation `triggers` are not the same as a camera's
|
||||
> [`triggers`](cameras/README.md?id=triggers). Automation triggers _initiate
|
||||
> [automations](automations.md)_; camera triggers take action on per-camera
|
||||
> events such as motion. They share only the word "trigger".
|
||||
|
||||
## Universal fields <!-- {docsify-ignore} -->
|
||||
|
||||
Every condition and trigger accepts an optional `enabled` field, mirroring Home
|
||||
Assistant.
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `true` (the default) keeps it active; `false`, or a [template](templates.md) that renders falsey, skips it. |
|
||||
|
||||
> [!NOTE]
|
||||
> An `enabled` template can turn a condition or trigger on or off at runtime:
|
||||
> point it at an `input_boolean` (or any live value) and the change takes effect
|
||||
> immediately, because the card re-evaluates `enabled` every time the condition
|
||||
> is evaluated or trigger fires. This is an intentional extension: Home Assistant
|
||||
> fixes `enabled` once when the automation loads, for both conditions and
|
||||
> triggers.
|
||||
|
||||
Home Assistant's `id`, `alias` and `variables` keys are also _accepted_ on any
|
||||
condition or trigger (so automations pasted from Home Assistant will validate),
|
||||
but will have no effect.
|
||||
|
||||
## `and`
|
||||
|
||||
_Condition only._
|
||||
|
||||
Evaluates to `true` if _all_ embedded conditions evaluate to `true`. At least one condition is required.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: and
|
||||
# [...]
|
||||
```
|
||||
|
||||
Or, in shorthand form:
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- and:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `and`. |
|
||||
| `conditions` | A list of other conditions _all_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `call`
|
||||
|
||||
Matches whether a [two-way audio](../usage/2-way-audio.md) call is in progress.
|
||||
As a **condition**, true while the call state matches; as a **trigger**, fires
|
||||
when it becomes a match (e.g. `call: true` fires when a call starts).
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: call
|
||||
call: true
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: call
|
||||
call: true
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `call`. |
|
||||
| `call` | If `true` or `false`, matches when a two-way audio call is or is not in progress respectively. |
|
||||
|
||||
## `camera`
|
||||
|
||||
Matches the selected camera. As a **condition**, true while the selection
|
||||
matches; as a **trigger**, fires when the selection changes to a match. Does not
|
||||
match other cameras (whether visible or not).
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: camera
|
||||
cameras: [front_door]
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: camera
|
||||
cameras: [front_door]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `camera`. |
|
||||
| `cameras` | An optional list of camera IDs. **A list** matches one of those cameras; **omitted** matches the presence of any selected camera (as a trigger: any selected camera change); **`[]`** matches when no camera is selected. See the camera [id](cameras/README.md) parameter. |
|
||||
|
||||
## `config`
|
||||
|
||||
_Trigger only._
|
||||
|
||||
Fires when the card configuration changes (e.g. on startup, or when [overrides](./overrides.md) are applied).
|
||||
|
||||
```yaml
|
||||
triggers:
|
||||
- trigger: config
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `trigger` | Must be `config`. |
|
||||
| `paths` | An optional list of configuration paths (e.g. `menu.style`). If provided, fires only when _any_ of those paths changes; otherwise fires on any configuration change. |
|
||||
|
||||
## `display_mode`
|
||||
|
||||
Matches the card display mode (`single` or `grid`). As a **condition**, true
|
||||
while in that mode; as a **trigger**, fires when the display mode changes to it.
|
||||
See the display settings for [`live`](live.md?id=display) or
|
||||
[`media_viewer`](media-viewer.md?id=display).
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: display_mode
|
||||
display_mode: single
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: display_mode
|
||||
display_mode: single
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | --------------------------- |
|
||||
| `condition` / `trigger` | Must be `display_mode`. |
|
||||
| `display_mode` | Must be `single` or `grid`. |
|
||||
|
||||
## `expand`
|
||||
|
||||
Matches whether the card is in "expanded" mode (in a dialog/popup). As a
|
||||
**condition**, true while the mode matches; as a **trigger**, fires when it
|
||||
becomes a match.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: expand
|
||||
expand: true
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: expand
|
||||
expand: true
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `expand`. |
|
||||
| `expand` | If `true` or `false`, matches when the card is or is not in expanded mode (in a dialog/popup) respectively. |
|
||||
|
||||
## `fullscreen`
|
||||
|
||||
Matches whether the card (or media within it) is in fullscreen mode. As a
|
||||
**condition**, true while the mode matches; as a **trigger**, fires when it
|
||||
becomes a match.
|
||||
|
||||
> [!WARNING]
|
||||
> When fullscreen is entered via a video player's built-in controls (rather than
|
||||
> the card's own fullscreen [action](actions/custom/README.md) or menu button),
|
||||
> the browser fullscreens the video element itself rather than the card. Any
|
||||
> automation action that replaces that video element (e.g. switching substreams)
|
||||
> will immediately exit fullscreen. A partial workaround may be to use the
|
||||
> card's fullscreen action instead. See [Fullscreen with HD substream
|
||||
> switching](../examples.md?id=fullscreen-with-hd-substream-switching) for an
|
||||
> approach that combines substream switching with the card's fullscreen.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
# As a trigger, on entering fullscreen:
|
||||
triggers:
|
||||
- trigger: fullscreen
|
||||
fullscreen: true
|
||||
# As a trigger, on any fullscreen change:
|
||||
triggers:
|
||||
- trigger: fullscreen
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `fullscreen`. |
|
||||
| `fullscreen` | If `true` or `false`, matches when the card is or is not in fullscreen mode respectively. |
|
||||
|
||||
## `initialized`
|
||||
|
||||
Matches whether the card has finished initializing. As a **condition**, true
|
||||
once the card is initialized; as a **trigger**, fires when the card initializes
|
||||
(useful for running an [automation](./automations.md) on card start).
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: initialized
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: initialized
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ---------------------- |
|
||||
| `condition` / `trigger` | Must be `initialized`. |
|
||||
|
||||
## `interaction`
|
||||
|
||||
Matches whether the card has recently been interacted with. As a **condition**,
|
||||
true while the interaction state matches; as a **trigger**, fires when it becomes
|
||||
a match.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: interaction
|
||||
interaction: true
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: interaction
|
||||
interaction: true
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `condition` / `trigger` | Must be `interaction`. |
|
||||
| `interaction` | If `true` or `false`, matches when the card has or has not had human interaction within `view.interaction_seconds` elapsed seconds respectively. |
|
||||
|
||||
## `key`
|
||||
|
||||
Matches a keyboard key. As a **condition**, true while the key matches the given
|
||||
state; as a **trigger**, fires on the matching key event.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: key
|
||||
key: ArrowLeft
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: key
|
||||
key: ArrowLeft
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | - | Must be `key`. |
|
||||
| `alt` | `false` | An optional value to match whether the `alt` key is being held. |
|
||||
| `ctrl` | `false` | An optional value to match whether the `ctrl` key is being held. |
|
||||
| `key` | | Any [keyboard key value](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values), e.g. `ArrowLeft`. |
|
||||
| `meta` | `false` | An optional value to match whether the `meta` key is being held. |
|
||||
| `shift` | `false` | An optional value to match whether the `shift` key is being held. |
|
||||
| `state` | `down` | An optional value to match the state of the key. Must be one of `down` or `up`. |
|
||||
|
||||
## `media_loaded`
|
||||
|
||||
Matches whether the selected live or media stream has loaded. As a **condition**,
|
||||
true while the load state matches; as a **trigger**, fires when it becomes a
|
||||
match.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: media_loaded
|
||||
media_loaded: true
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: media_loaded
|
||||
media_loaded: true
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `media_loaded`. |
|
||||
| `media_loaded` | If `true` or `false`, matches when there is or is not media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. |
|
||||
|
||||
> [!NOTE]
|
||||
> Toggling a substream on or off does not cause this condition to transition.
|
||||
> Substream is treated as a playback-layer detail of the same logical camera, so
|
||||
> the condition remains satisfied while any stream of the camera continues to
|
||||
> render.
|
||||
|
||||
## `microphone`
|
||||
|
||||
Matches the microphone state. As a **condition**, true while the mute state
|
||||
matches; as a **trigger**, fires when it becomes a match.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: microphone
|
||||
muted: true
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: microphone
|
||||
muted: true
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `microphone`. |
|
||||
| `muted` | If `true` or `false`, matches when the microphone is muted or unmuted respectively. |
|
||||
|
||||
## `not`
|
||||
|
||||
_Condition only._
|
||||
|
||||
Evaluates to `true` if every embedded condition is `false`. At least one
|
||||
condition is required.
|
||||
|
||||
> [!IMPORTANT] > `not` is a **NOR** operation, not a **NAND**. If _any_ sub-condition is `true`,
|
||||
> the `not` condition evaluates to `false` -- even if other sub-conditions are
|
||||
> `false`. To pass, _all_ sub-conditions must be `false`. This behavior matches
|
||||
> the [Home Assistant equivalent](https://www.home-assistant.io/docs/scripts/conditions/#not-condition).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: not
|
||||
# [...]
|
||||
```
|
||||
|
||||
Or, in shorthand form:
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- not:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `not`. |
|
||||
| `conditions` | A list of other conditions _none_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `numeric_state`
|
||||
|
||||
Matches a numeric Home Assistant value (an entity's state or attribute, or a
|
||||
template). As a **condition**, true while the value is within range; as a
|
||||
**trigger**, fires when the value crosses into range. At least one of `above` /
|
||||
`below` is required.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: numeric_state
|
||||
entity: sensor.office_temperature
|
||||
above: 10
|
||||
below: 20
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: numeric_state
|
||||
entity_id: sensor.office_temperature
|
||||
above: 10
|
||||
below: 20
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `numeric_state`. |
|
||||
| `entity` / `entity_id` | The entity (or list of entities) to read. |
|
||||
| `above` | Match when the value is above this: a number, or an entity ID whose state supplies the threshold. |
|
||||
| `below` | Match when the value is below this: a number, or an entity ID whose state supplies the threshold. |
|
||||
| `value_template` | A template whose rendered numeric value is compared instead of the entity's state. |
|
||||
| `attribute` | Compare this attribute instead of the entity's state. |
|
||||
| `for` | _Trigger only._ A duration (`hh:mm:ss` or a template) the value must stay in range before firing. |
|
||||
|
||||
See the [Home Assistant numeric_state condition](https://www.home-assistant.io/docs/scripts/conditions/#numeric-state-condition) and [numeric_state trigger](https://www.home-assistant.io/docs/automation/trigger/#numeric-state-trigger).
|
||||
|
||||
## `or`
|
||||
|
||||
_Condition only._
|
||||
|
||||
Evaluates to `true` if _any_ embedded condition evaluates to `true`. At least one condition is required.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: or
|
||||
# [...]
|
||||
```
|
||||
|
||||
Or, in shorthand form:
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- or:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `or`. |
|
||||
| `conditions` | A list of conditions _any_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `screen`
|
||||
|
||||
Matches a CSS [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using).
|
||||
As a **condition**, true while the query matches; as a **trigger**, fires when
|
||||
the match changes (e.g. on a change of orientation or viewport size).
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: screen
|
||||
media_query: '(orientation: landscape)'
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: screen
|
||||
media_query: '(orientation: landscape)'
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `screen`. |
|
||||
| `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. |
|
||||
|
||||
See the [screen conditions examples](../examples.md?id=screen-conditions).
|
||||
|
||||
## `state`
|
||||
|
||||
Matches a Home Assistant entity's state. Unlike most types, the **condition** and
|
||||
**trigger** forms take different fields: a condition compares the _current_ value
|
||||
(requiring `state` or `state_not`), while a trigger matches the _transition_
|
||||
(`from` / `to`, both optional).
|
||||
|
||||
Both forms accept `entity` (or its `entity_id` alias) as a single entity or a
|
||||
list.
|
||||
|
||||
### As a condition
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: state
|
||||
entity: binary_sensor.door
|
||||
state: 'on'
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `state`. |
|
||||
| `entity` / `entity_id` | The entity (or list of entities) to check. |
|
||||
| `state` | A state, or list of states, the entity must match. |
|
||||
| `state_not` | A state, or list of states, the entity must not match. |
|
||||
| `match` | With a list of entities: `all` (the default) requires every entity to match, `any` requires at least one. |
|
||||
| `for` | A duration (`hh:mm:ss` or a template) the match must have held. |
|
||||
| `attribute` | Compare this attribute instead of the entity's state. |
|
||||
|
||||
See the [Home Assistant state condition](https://www.home-assistant.io/docs/scripts/conditions/#state-condition).
|
||||
|
||||
### As a trigger
|
||||
|
||||
```yaml
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.door
|
||||
to: 'on'
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------- |
|
||||
| `trigger` | Must be `state`. |
|
||||
| `entity` / `entity_id` | The entity (or list of entities) to watch. |
|
||||
| `from` / `not_from` | Match (or exclude) the prior state. A single value, a list, or `null` (any). |
|
||||
| `to` / `not_to` | Match (or exclude) the new state. A single value, a list, or `null` (any). |
|
||||
| `for` | A duration (`hh:mm:ss` or a template) the new state must hold before firing. |
|
||||
| `attribute` | Watch this attribute instead of the entity's state. |
|
||||
|
||||
See the [Home Assistant state trigger](https://www.home-assistant.io/docs/automation/trigger/#state-trigger).
|
||||
|
||||
## `template`
|
||||
|
||||
Matches a Home Assistant template. As a **condition**, true while the template
|
||||
renders `true`; as a **trigger**, fires when it changes from non-true to true.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ states('switch.office') == 'on' }}"
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: template
|
||||
value_template: "{{ states('switch.office') == 'on' }}"
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `condition` / `trigger` | Must be `template`. |
|
||||
| `value_template` | The Home Assistant template to evaluate, e.g. `{{ states('switch.office') == 'on' }}`. |
|
||||
| `for` | _Trigger only._ A duration (`hh:mm:ss` or a template) the template must stay true before firing. |
|
||||
|
||||
See the [Home Assistant template condition](https://www.home-assistant.io/docs/scripts/conditions/#template-condition) and [template trigger](https://www.home-assistant.io/docs/automation/trigger/#template-trigger).
|
||||
|
||||
> [!NOTE]
|
||||
> In order to match native Home Assistant behavior, condition and trigger
|
||||
> truthiness differ: a **condition** passes only when the template renders
|
||||
> `true` (case-insensitive), whereas a **trigger** also accepts broader truthy
|
||||
> values (`1`, `yes`, `on`, `enable`).
|
||||
|
||||
> [!NOTE]
|
||||
> A **trigger** is re-evaluated when card or Home Assistant state changes, not on
|
||||
> a timer, so a template that depends only on time (e.g. `{{ now().hour == 8 }}`)
|
||||
> will not fire on its own.
|
||||
|
||||
> [!TIP]
|
||||
> The Advanced Camera Card uses
|
||||
> [ha-nunjucks](https://github.com/Nerwyn/ha-nunjucks) to process templates.
|
||||
> Consult its documentation for the wide variety of different template values
|
||||
> supported.
|
||||
|
||||
## `triggered`
|
||||
|
||||
Matches the set of cameras currently [triggered](cameras/README.md?id=triggers).
|
||||
As a **condition**, true while the set matches; as a **trigger**, fires when it
|
||||
becomes a match.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: triggered
|
||||
triggered: [camera.office]
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: triggered
|
||||
triggered: [camera.office]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` / `trigger` | Must be `triggered`. |
|
||||
| `triggered` | An optional list of camera IDs. Matches when one of them is triggered. **Omit** to match while _any_ camera is triggered; use an empty list `[]` to match while _none_ is. |
|
||||
|
||||
## `user`
|
||||
|
||||
_Condition only._
|
||||
|
||||
Matches the logged-in Home Assistant user. See the [Home Assistant user condition](https://www.home-assistant.io/dashboards/conditional/#user).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| `condition` | Must be `user`. |
|
||||
| `users` | A list of Home Assistant user IDs to match. |
|
||||
|
||||
## `user_agent`
|
||||
|
||||
_Condition only._
|
||||
|
||||
Matches the [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: user_agent
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `user_agent`. |
|
||||
| `user_agent` | Exactly matches a user-agent, e.g. `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` |
|
||||
| `user_agent_re` | Matches a user-agent based on a regular expression, e.g. `Chrome/`. |
|
||||
| `casting` | If `true` matches if the card is being cast to a Chromecast / TV device, if `false` matches if the card is _NOT_ being cast. |
|
||||
| `companion` | If `true` matches if the user-agent is the Home Assistant companion app, if `false` matches if the user-agent is _NOT_ the Home Assistant companion app. |
|
||||
|
||||
At least one of these parameters is required. When multiple are specified they
|
||||
must all match for the condition to match.
|
||||
|
||||
See the [user-agent overrides example](../examples.md?id=disable-ptz-controls-in-the-home-assistant-companion-app).
|
||||
|
||||
## `view`
|
||||
|
||||
Matches the selected view. As a **condition**, true while a matching view is
|
||||
selected; as a **trigger**, fires when the selected view changes to a matching
|
||||
one.
|
||||
|
||||
```yaml
|
||||
# As a condition:
|
||||
conditions:
|
||||
- condition: view
|
||||
views: [live]
|
||||
# As a trigger:
|
||||
triggers:
|
||||
- trigger: view
|
||||
views: [live]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `condition` / `trigger` | Must be `view`. |
|
||||
| `views` | A list of [views](view.md?id=supported-views) to match (e.g. `clips`). **Required** as a condition; optional as a trigger (omit to fire on any view change). |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Internally, views associated with the media viewer (e.g. `clip`, `snapshot`,
|
||||
> `review`, `recording`) are translated to the `media` view after the relevant
|
||||
> media is fetched. When naming views in a condition or trigger, you may need to
|
||||
> refer to the `media` view.
|
||||
|
||||
## Unsupported Home Assistant conditions and triggers
|
||||
|
||||
Several Home Assistant condition types are **not** currently supported: `time`,
|
||||
`zone`, `sun`, `location`, `device`, and `condition: trigger` (matching on the
|
||||
`id` of the trigger that fired).
|
||||
|
||||
On the trigger side, only the stock `state`, `numeric_state` and `template`
|
||||
platforms are supported, alongside the card-specific triggers listed above.
|
||||
Other Home Assistant trigger platforms -- including `event`, `time`,
|
||||
`time_pattern`, `sun`, `zone`, `calendar`, `webhook`, `tag`, `device` and
|
||||
`mqtt` -- are **not** supported.
|
||||
|
||||
If you need any of these, please [open an
|
||||
issue](https://github.com/dermotduffy/advanced-camera-card/issues).
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
[](common/expanded-warning.md ':include')
|
||||
|
||||
### Conditions
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- and:
|
||||
- condition: camera
|
||||
cameras: [front_door]
|
||||
- condition: view
|
||||
views: [live]
|
||||
- condition: call
|
||||
call: true
|
||||
- condition: camera
|
||||
cameras:
|
||||
- camera.office
|
||||
- condition: display_mode
|
||||
display_mode: single
|
||||
- condition: expand
|
||||
expand: true
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
- condition: initialized
|
||||
- condition: interaction
|
||||
interaction: true
|
||||
- condition: key
|
||||
alt: false
|
||||
ctrl: false
|
||||
key: F
|
||||
meta: false
|
||||
shift: false
|
||||
state: down
|
||||
- condition: media_loaded
|
||||
media_loaded: true
|
||||
- condition: microphone
|
||||
muted: true
|
||||
- not:
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
- condition: numeric_state
|
||||
entity: sensor.office_temperature
|
||||
above: 10
|
||||
below: 20
|
||||
- or:
|
||||
- condition: camera
|
||||
cameras: [front_door]
|
||||
- condition: view
|
||||
views: [live]
|
||||
- condition: screen
|
||||
media_query: '(orientation: landscape)'
|
||||
- condition: state
|
||||
entity: climate.office
|
||||
state: heat
|
||||
state_not: 'off'
|
||||
- condition: template
|
||||
value_template: "{{ is_state('switch.office', 'on') }}"
|
||||
- condition: triggered
|
||||
triggered:
|
||||
- camera.office
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
- condition: user_agent
|
||||
user_agent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
||||
user_agent_re: 'Chrome/'
|
||||
casting: true
|
||||
companion: true
|
||||
- condition: view
|
||||
views:
|
||||
- live
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
```yaml
|
||||
triggers:
|
||||
- trigger: call
|
||||
call: true
|
||||
- trigger: camera
|
||||
cameras:
|
||||
- camera.office
|
||||
- trigger: config
|
||||
paths:
|
||||
- 'menu.style'
|
||||
- trigger: display_mode
|
||||
display_mode: single
|
||||
- trigger: expand
|
||||
expand: true
|
||||
- trigger: fullscreen
|
||||
fullscreen: true
|
||||
- trigger: initialized
|
||||
- trigger: interaction
|
||||
interaction: true
|
||||
- trigger: key
|
||||
alt: false
|
||||
ctrl: false
|
||||
key: F
|
||||
meta: false
|
||||
shift: false
|
||||
state: down
|
||||
- trigger: media_loaded
|
||||
media_loaded: true
|
||||
- trigger: microphone
|
||||
muted: true
|
||||
- trigger: numeric_state
|
||||
entity_id: sensor.office_temperature
|
||||
above: 10
|
||||
below: 20
|
||||
for: '00:00:05'
|
||||
- trigger: screen
|
||||
media_query: '(orientation: landscape)'
|
||||
- trigger: state
|
||||
entity_id: climate.office
|
||||
from: 'off'
|
||||
to: heat
|
||||
for: '00:00:05'
|
||||
- trigger: template
|
||||
value_template: "{{ is_state('switch.office', 'on') }}"
|
||||
for: '00:00:05'
|
||||
- trigger: triggered
|
||||
triggered:
|
||||
- camera.office
|
||||
- trigger: view
|
||||
views:
|
||||
- live
|
||||
```
|
||||
@@ -1,457 +0,0 @@
|
||||
# `conditions`
|
||||
|
||||
`conditions` is not a top-level configuration block, but can be used as part of
|
||||
multiple other blocks.
|
||||
|
||||
Conditions are used to conditionally take action (in `automations`), to apply
|
||||
certain configurations (in `overrides`) or to display "picture elements" (in
|
||||
`elements`) depending on runtime evaluation.
|
||||
|
||||
```yaml
|
||||
[used as part of other configuration]
|
||||
conditions:
|
||||
- [condition_1]
|
||||
- [condition_2]
|
||||
```
|
||||
|
||||
## `and`
|
||||
|
||||
Evaluates to `true` if _all_ embedded conditions evaluate to `true`. At least one condition is required.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: and
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `and`. |
|
||||
| `conditions` | A list of other conditions _all_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `call`
|
||||
|
||||
Matches based on whether a [two-way audio](../usage/2-way-audio.md) call is in progress.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: call
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `call`. |
|
||||
| `call` | If `true` (the default) or `false`, the condition is satisfied when a two-way audio call is or is not in progress respectively. |
|
||||
|
||||
## `camera`
|
||||
|
||||
Matches based on the selected camera. Does not match other cameras (whether
|
||||
visible or not).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: camera
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `camera`. |
|
||||
| `cameras` | An optional list of camera IDs in which this condition is satisfied. If not specified, any camera change will satisy the condition. See the camera [id](cameras/README.md) parameter. |
|
||||
|
||||
## `config`
|
||||
|
||||
Matches when card configuration changes (e.g. on startup, or when [Configuration Overrides](./overrides.md) are applied).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: config
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `config`. |
|
||||
| `paths` | An optional array of configuration paths (e.g. `menu.style`). If provided condition matches if _ANY_ of the provided configuration paths has changed. |
|
||||
|
||||
## `display_mode`
|
||||
|
||||
Matches when card display mode changes (e.g. `single` or `grid` mode). See the display settings for [`live`](live.md?id=display) or [`media_viewer`](media-viewer.md?id=display).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: display_mode
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| -------------- | --------------------------- |
|
||||
| `condition` | Must be `display_mode`. |
|
||||
| `display_mode` | Must be `single` or `grid`. |
|
||||
|
||||
## `expand`
|
||||
|
||||
Matches based on whether the card is in "expanded" mode.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: expand
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `expand`. |
|
||||
| `expand` | If `true` the condition is satisfied if the card is in expanded mode (in a dialog/popup). If `false` the condition is satisfied if the card is **NOT** in expanded mode (in a dialog/popup). |
|
||||
|
||||
## `fullscreen`
|
||||
|
||||
Matches based on whether the card (or media within it) is in fullscreen mode.
|
||||
|
||||
> [!WARNING]
|
||||
> When fullscreen is entered via a video player's built-in controls (rather than
|
||||
> the card's own fullscreen [action](actions/custom/README.md) or menu button),
|
||||
> the browser fullscreens the video element itself rather than the card. Any
|
||||
> automation action that replaces that video element (e.g. switching substreams)
|
||||
> will immediately exit fullscreen. A partial workaround may be to use the
|
||||
> card's fullscreen action instead. See [Fullscreen with HD substream
|
||||
> switching](../examples.md?id=fullscreen-with-hd-substream-switching) for an
|
||||
> approach that combines substream switching with the card's fullscreen.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: fullscreen
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `fullscreen`. |
|
||||
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode. |
|
||||
|
||||
## `initialized`
|
||||
|
||||
Matches when the card is first initialized.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: initialized
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ---------------------- |
|
||||
| `condition` | Must be `initialized`. |
|
||||
|
||||
> [!NOTE]
|
||||
> This is exclusively useful for running [automations](./automations.md) on card start.
|
||||
|
||||
## `interaction`
|
||||
|
||||
Matches based on whether the card has been interacted with.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: interaction
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `interaction`. |
|
||||
| `interaction` | If `true` the condition is satisfied if the card has had human interaction within `view.interaction_seconds` elapsed seconds. If `false` the condition is satisfied if the card has **NOT** had human interaction in that time. |
|
||||
|
||||
## `key`
|
||||
|
||||
Matches based on key state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: key
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | - | Must be `key`. |
|
||||
| `alt` | `false` | An optional value to match whether the `alt` key is being held. |
|
||||
| `ctrl` | `false` | An optional value to match whether the `ctrl` key is being held. |
|
||||
| `key` | | Any [keyboard key value](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values), e.g. `ArrowLeft`. |
|
||||
| `meta` | `false` | An optional value to match whether the `meta` key is being held. |
|
||||
| `shift` | `false` | An optional value to match whether the `shift` key is being held. |
|
||||
| `state` | `down` | An optional value to match the state of the key. Must be one of `down` or `up`. |
|
||||
|
||||
## `media_loaded`
|
||||
|
||||
Matches based on whether the selected live or media stream has loaded.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: media_loaded
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `media_loaded`. |
|
||||
| `media_loaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. |
|
||||
|
||||
> [!NOTE]
|
||||
> Toggling a substream on or off does not cause this condition to transition.
|
||||
> Substream is treated as a playback-layer detail of the same logical camera, so
|
||||
> the condition remains satisfied while any stream of the camera continues to
|
||||
> render.
|
||||
|
||||
## `microphone`
|
||||
|
||||
Matches based on microphone state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: microphone
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `condition` | Must be `microphone`. |
|
||||
| `muted` | If `true` or `false`, the condition is satisfied when the microphone is muted or unmuted respectively. |
|
||||
|
||||
## `not`
|
||||
|
||||
Evaluates to `true` if every embedded condition is `false`. At least one
|
||||
condition is required.
|
||||
|
||||
> [!IMPORTANT] > `not` is a **NOR** operation, not a **NAND**. If _any_ sub-condition is `true`, the `not` condition evaluates to `false` — even if other sub-conditions are `false`. To pass, _all_ sub-conditions must be `false`. This behavior matches the [Home Assistant equivalent](https://www.home-assistant.io/docs/scripts/conditions/#not-condition).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: not
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `not`. |
|
||||
| `conditions` | A list of other conditions _none_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `numeric_state`
|
||||
|
||||
Matches based on numeric Home Assistant state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: numeric_state
|
||||
# [...]
|
||||
```
|
||||
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#numeric-state).
|
||||
|
||||
## `or`
|
||||
|
||||
Evaluates to `true` if _any_ embedded condition evaluates to `true`. At least one condition is required.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: or
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | -------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `or`. |
|
||||
| `conditions` | A list of conditions _any_ of which must evaluate `true` in order for this condition to evaluate `true`. |
|
||||
|
||||
## `screen`
|
||||
|
||||
Matches based on [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: screen
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `screen`. |
|
||||
| `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. |
|
||||
|
||||
See the [screen conditions examples](../examples.md?id=screen-conditions).
|
||||
|
||||
## `state`
|
||||
|
||||
Matches based on Home Assistant state.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: state
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `condition` | Must be `state`. |
|
||||
| `entity` | The entity to check the state of. |
|
||||
| `state` | A single entity state, or list of entity states, against which the entity state is compared. |
|
||||
| `state_not` | A single entity state, or list of entity states, against which the entity state is inversely compared. |
|
||||
|
||||
> [!NOTE]
|
||||
> If multiple state conditions are used together with neither `state` nor
|
||||
> `state_not` specified, this effectively means the state for multiple entities
|
||||
> needs to _change_ simultaneously. This is unlikely to happen in reality, and
|
||||
> almost certainly not useful / reliable as a condition.
|
||||
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#state).
|
||||
|
||||
## `template`
|
||||
|
||||
Matches based on a template.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: template
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------- | ---------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `template`. |
|
||||
| `value_template` | The Home Assistant template to check, e.g. `{{ states('switch.office') == 'on' }}` |
|
||||
|
||||
See [Home Assistant conditions documentation](https://www.home-assistant.io/docs/scripts/conditions/#template-condition).
|
||||
|
||||
> [!TIP]
|
||||
> The Advanced Camera Card uses
|
||||
> [ha-nunjucks](https://github.com/Nerwyn/ha-nunjucks) to process templates.
|
||||
> Consult its documentation for the wide variety of different template values
|
||||
> supported.
|
||||
|
||||
## `triggered`
|
||||
|
||||
Matches based on whether the selected camera has been triggered.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: triggered
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `triggered`. |
|
||||
| `triggered` | A list of camera IDs which, if [triggered](cameras/README.md?id=triggers), satisfy the condition. |
|
||||
|
||||
## `user`
|
||||
|
||||
Matches based on the Home Assistant user that is logged in. See [Home Assistant conditions documentation](https://www.home-assistant.io/dashboards/conditional/#user).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: user
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `user_agent`
|
||||
|
||||
Matches based on the [User-Agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent).
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: user_agent
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | Must be `user_agent`. |
|
||||
| `user_agent` | Exactly matches a user-agent, e.g. `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` |
|
||||
| `user_agent_re` | Matches a user-agent based on a regular expression, e.g. `Chrome/`. |
|
||||
| `casting` | If `true` matches if the card is being cast to a Chromecast / TV device, if `false` matches if the card is _NOT_ being cast. |
|
||||
| `companion` | If `true` matches if the user-agent is the Home Assistant companion app, if `false` matches if the user-agent is _NOT_ the Home Assistant companion app. |
|
||||
|
||||
When multiple parameters are specified they must all match for the condition to
|
||||
match.
|
||||
|
||||
See the [user-agent overrides example](../examples.md?id=disable-ptz-controls-in-the-home-assistant-companion-app).
|
||||
|
||||
## `view`
|
||||
|
||||
Matches based on the selected view.
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: view
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `condition` | Must be `view`. |
|
||||
| `views` | An optional list of [views](view.md?id=supported-views) in which this condition is satified (e.g. `clips`). If not specified, any view change will satisy the condition. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Internally, views associated with the media viewer (e.g. `clip`, `snapshot`,
|
||||
> `review` `recording`) are translated to the `media` view after the relevant
|
||||
> media is fetched. When including views as part of a
|
||||
> [condition](conditions.md), you may need to refer to the `media` view.
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
[](common/expanded-warning.md ':include')
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: call
|
||||
call: true
|
||||
- condition: camera
|
||||
cameras:
|
||||
- camera.office
|
||||
- condition: config
|
||||
paths:
|
||||
- 'menu.style'
|
||||
- condition: display_mode
|
||||
display_mode: single
|
||||
- condition: expand
|
||||
expand: true
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
- condition: initialized
|
||||
- condition: interaction
|
||||
interaction: true
|
||||
- condition: key
|
||||
alt: false
|
||||
ctrl: false
|
||||
key: F
|
||||
meta: false
|
||||
shift: false
|
||||
state: down
|
||||
- condition: media_loaded
|
||||
media_loaded: true
|
||||
- condition: microphone
|
||||
muted: true
|
||||
- condition: numeric_state
|
||||
entity: sensor.office_temperature
|
||||
above: 10
|
||||
below: 20
|
||||
- condition: screen
|
||||
media_query: '(orientation: landscape)'
|
||||
- condition: state
|
||||
entity: climate.office
|
||||
state: heat
|
||||
state_not: off
|
||||
- condition: triggered
|
||||
triggered:
|
||||
- camera.office
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
- condition: user_agent
|
||||
user_agent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
||||
user_agent_re: 'Chrome/'
|
||||
casting: true
|
||||
companion: true
|
||||
- condition: view
|
||||
views:
|
||||
- live
|
||||
```
|
||||
@@ -3,7 +3,7 @@
|
||||
- [`actions`](../actions/README.md)
|
||||
- [`automations`](../automations.md)
|
||||
- [`cameras`](../cameras/README.md)
|
||||
- [`conditions`](../conditions.md)
|
||||
- [`conditions` / `triggers`](../conditions-triggers.md)
|
||||
- [`dimensions`](../dimensions.md)
|
||||
- [`elements`](README.md)
|
||||
- [Custom Elements](./custom/README.md)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## `conditional`
|
||||
|
||||
Restrict a set of elements to only render when the card is matches a set of [conditions](../../conditions.md). This is analogous to the stock [`conditional`](../stock/README.md?id=conditional) element except supporting a rich set of Advanced Camera Card [conditions](../../conditions.md).
|
||||
Restrict a set of elements to only render when the card is matches a set of [conditions](../../conditions-triggers.md). This is analogous to the stock [`conditional`](../stock/README.md?id=conditional) element except supporting a rich set of Advanced Camera Card [conditions](../../conditions-triggers.md).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
@@ -12,11 +12,11 @@ elements:
|
||||
|
||||
Parameters for the `custom:advanced-camera-card-conditional` element:
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Must be `custom:advanced-camera-card-conditional`. |
|
||||
| `conditions` | A list of [conditions](../../conditions.md) that must evaluate to true in order for the elements to be rendered. |
|
||||
| `elements` | The elements to render. Can be any supported element. |
|
||||
| Parameter | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Must be `custom:advanced-camera-card-conditional`. |
|
||||
| `conditions` | A list of [conditions](../../conditions-triggers.md) that must evaluate to true in order for the elements to be rendered. |
|
||||
| `elements` | The elements to render. Can be any supported element. |
|
||||
|
||||
See the [conditional elements example](../../../examples.md?id=conditional-elements).
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- [`actions`](../README.md)
|
||||
- [`automations`](../../automations.md)
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`conditions` / `triggers`](../../conditions-triggers.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [Custom Actions](README.md)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- [`actions`](../README.md)
|
||||
- [`automations`](../../automations.md)
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`conditions` / `triggers`](../../conditions-triggers.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [Custom Actions](../custom/README.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `overrides`
|
||||
|
||||
The card configuration may [conditionally](conditions.md) be overridden (e.g. to
|
||||
The card configuration may [conditionally](conditions-triggers.md) be overridden (e.g. to
|
||||
hide the menu in fullscreen mode).
|
||||
|
||||
```yaml
|
||||
@@ -16,12 +16,12 @@ overrides:
|
||||
The top-level `overrides` configuration block expects a list, with each list
|
||||
item containing `conditions` and at least one of `merge`, `delete` or `set` specified.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `conditions` | | A list of [conditions](conditions.md) that must evaluate to `true` in order for the overrides to be applied. |
|
||||
| `delete` | | An array of configuration paths to delete. See [`delete`](#delete). |
|
||||
| `merge` | | A dictionary of configuration paths to merge. See [`merge`](#merge). |
|
||||
| `set` | | A dictionary of configuration paths to set. See [`set`](#set). |
|
||||
| Option | Default | Description |
|
||||
| ------------ | ------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `conditions` | | A list of [conditions](conditions-triggers.md) that must evaluate to `true` in order for the overrides to be applied. |
|
||||
| `delete` | | An array of configuration paths to delete. See [`delete`](#delete). |
|
||||
| `merge` | | A dictionary of configuration paths to merge. See [`merge`](#merge). |
|
||||
| `set` | | A dictionary of configuration paths to set. See [`set`](#set). |
|
||||
|
||||
## Configuration Paths
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ Advanced Camera Card data, to be accessible. Templates may be used in:
|
||||
- [Actions / Automations](./actions/README.md)
|
||||
- [Folder Media Matchers](./folders.md?id=matchers)
|
||||
|
||||
> [!NOTE]
|
||||
> Templates substitute into action _values_ (e.g. a `camera` or `message`), not
|
||||
> into the action _type_. The type discriminator (`action` and
|
||||
> `advanced_camera_card_action`) must be a literal (as in Home Assistant
|
||||
> itself).
|
||||
|
||||
## Stock Templates
|
||||
|
||||
The Advanced Camera Card uses
|
||||
@@ -19,13 +25,13 @@ accesses Home Assistant state.
|
||||
|
||||
## Custom Templates
|
||||
|
||||
Custom template values must be proceeded by `advanced_camera_card` (or `acc` for
|
||||
short).
|
||||
Custom template values must be prefixed with `acc`.
|
||||
|
||||
| Template | Replaced with |
|
||||
| -------- | ------------------------------------------------- |
|
||||
| `camera` | The currently selected camera. |
|
||||
| `view` | The current [view](./view.md?id=supported-views). |
|
||||
| `config` | The current card configuration. |
|
||||
|
||||
See [an example](../examples.md?id=accessing-advanced-camera-card-state) that
|
||||
accesses Advanced Camera Card state.
|
||||
@@ -35,8 +41,7 @@ accesses Advanced Camera Card state.
|
||||
If templates are used for [Folder Media Matching](./folders.md?id=matchers) an
|
||||
additional `media` variable is available with these properties:
|
||||
|
||||
Media template values must be proceeded by `advanced_camera_card.media` (or
|
||||
`acc.media` for short).
|
||||
Media template values must be prefixed with `acc.media`.
|
||||
|
||||
| Template | Replaced with |
|
||||
| ----------- | --------------------------------------------------------------------------------- |
|
||||
@@ -45,34 +50,35 @@ Media template values must be proceeded by `advanced_camera_card.media` (or
|
||||
|
||||
### Triggers
|
||||
|
||||
If the action is called by an [Advanced Camera Card
|
||||
Automation](./automations.md), additional data is available representing the
|
||||
current and prior state of whatever triggered the action.
|
||||
When an action runs from an [automation](./automations.md), a top-level
|
||||
`trigger` variable describes what fired it (as in native Home Assistant
|
||||
actions), including the state before and after the change. Its fields depend on
|
||||
the kind of trigger.
|
||||
|
||||
Trigger template values must be proceeded by `advanced_camera_card.trigger` (or
|
||||
`acc.trigger` for short).
|
||||
The stock `state` and `numeric_state` [triggers](./conditions-triggers.md) carry
|
||||
Home-Assistant-faithful entity data (a subset of Home Assistant's own [trigger
|
||||
data](https://www.home-assistant.io/docs/automation/templating/#available-trigger-data):
|
||||
the card does not currently surface `id`, `idx`, `for`, `attribute`, `above` /
|
||||
`below` or `alias`, so [request](https://github.com/dermotduffy/advanced-camera-card/issues)
|
||||
if you need more). The `template` trigger has no entity, so it carries only
|
||||
`trigger.platform` (`template`).
|
||||
|
||||
| Template | Replaced with |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `camera.to` | For [camera conditions](./conditions.md?id=camera), the currently selected camera. |
|
||||
| `camera.from` | For [camera conditions](./conditions.md?id=camera), the previously selected camera. |
|
||||
| `view.to` | For [view conditions](./conditions.md?id=view), the currently selected view. |
|
||||
| `view.from` | For [view conditions](./conditions.md?id=view), the previously selected view. |
|
||||
| `state.entity` | For [state conditions](./conditions.md?id=state), the entity state that triggered the condition. |
|
||||
| `state.to` | For [state conditions](./conditions.md?id=state), the current state of the entity. |
|
||||
| `state.from` | For [state conditions](./conditions.md?id=state), the previous state of the entity. |
|
||||
| Template | Replaced with |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `trigger.platform` | The trigger platform (`state` or `numeric_state`). |
|
||||
| `trigger.entity_id` | The entity that triggered (also available as `trigger.entity`). |
|
||||
| `trigger.from_state` | The full Home Assistant [state object](https://www.home-assistant.io/docs/configuration/state_object/) before the change, e.g. `trigger.from_state.state`. |
|
||||
| `trigger.to_state` | The full Home Assistant state object after the change, e.g. `trigger.to_state.state` or `trigger.to_state.attributes.<name>`. |
|
||||
|
||||
> [!NOTE]
|
||||
> If an action is triggered with multiple [state
|
||||
> conditions](./conditions.md?id=state), only data from the last listed state
|
||||
> condition is available.
|
||||
The card-specific [triggers](./conditions-triggers.md) (e.g. `camera`, `view`, `config`)
|
||||
carry the card state before and after the change:
|
||||
|
||||
> [!NOTE]
|
||||
> If you use an [`or`](./conditions.md?id=or) condition, only the trigger data
|
||||
> for the first matching trigger will be included.
|
||||
|
||||
Please [request](https://github.com/dermotduffy/advanced-camera-card/issues) if
|
||||
you need data from additional conditions.
|
||||
| Template | Replaced with |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| `trigger.platform` | `acc` for card-specific triggers. |
|
||||
| `trigger.type` | The card trigger kind (e.g. `camera`, `view`, `config`). |
|
||||
| `trigger.from_acc` | The card state before the change, with `camera`, `view` and `config` (e.g. `trigger.from_acc.camera`). |
|
||||
| `trigger.to_acc` | The card state after the change, with `camera`, `view` and `config` (e.g. `trigger.to_acc.camera`). |
|
||||
|
||||
See [an example](../examples.md?id=accessing-trigger-state) that accesses
|
||||
trigger state.
|
||||
|
||||
@@ -14,7 +14,7 @@ view:
|
||||
| `dim` | `false` | Whether or not to 'dim' the brightness of the card (by 25%) if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time). |
|
||||
| `default` | `auto` | The view to show in the card by default. If `auto`, the card will choose `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (default embedded image). The default camera is the first one listed. See [Supported Views](view.md?id=supported-views). |
|
||||
| `default_reset` | | The circumstances and behavior that cause the card to reset to the default view. See [`default_reset`](#default_reset). |
|
||||
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `default_reset.after_interaction` to reset the view after the interaction is complete. |
|
||||
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions-triggers.md?id=interaction) or with `default_reset.after_interaction` to reset the view after the interaction is complete. |
|
||||
| `issues` | | How the card handles issues and retries. See [`issues`](#issues). |
|
||||
| `keyboard_shortcuts` | See [usage](../usage/keyboard-shortcuts.md) for defaults. | Configure keyboard shortcuts. See [`keyboard_shortcuts`](#keyboard_shortcuts). |
|
||||
| `render_entities` | | **YAML only**: A list of entity ids that should cause the card to re-render 'in-place'. The view/camera is not changed. This should **very** rarely be needed, but could be useful if the card is both setting and changing HA state of the same object as could be the case for some complex `card_mod` scenarios ([example](https://github.com/dermotduffy/advanced-camera-card/issues/343)). |
|
||||
@@ -201,7 +201,7 @@ This card supports several different views.
|
||||
The default view is `auto`. It will select `live` when cameras are configured, `folders` when folders are configured, or `image` otherwise (default embedded image). You can override this with `view.default`.
|
||||
|
||||
> [!NOTE]
|
||||
> When using views in a [`view` condition](conditions.md?id=view), the single-item viewer views (`clip`, `snapshot`, `review`, `recording`) are translated internally to `media` once the relevant media is fetched. You may need to match on `media` rather than the original view name in your condition.
|
||||
> When using views in a [`view` condition](conditions-triggers.md?id=view), the single-item viewer views (`clip`, `snapshot`, `review`, `recording`) are translated internally to `media` once the relevant media is fetched. You may need to match on `media` rather than the original view name in your condition.
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
|
||||
+61
-32
@@ -47,7 +47,7 @@ fullscreen mode.
|
||||
> When fullscreen is entered via a video player's built-in controls (rather than
|
||||
> the card's fullscreen menu button), automation actions that replace _that_ video
|
||||
> element (e.g. switching substreams from one video to another) will immediately exit fullscreen (as the browser sees the video the user clicked on be destroyed). See the
|
||||
> [fullscreen condition](configuration/conditions.md?id=fullscreen) for details.
|
||||
> [fullscreen condition](configuration/conditions-triggers.md?id=fullscreen) for details.
|
||||
|
||||
```yaml
|
||||
type: custom:advanced-camera-card
|
||||
@@ -72,13 +72,18 @@ cameras:
|
||||
# Optionally allow PTZ controls on the substream.
|
||||
- ptz
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: fullscreen
|
||||
# Entering fullscreen: turn the substream on.
|
||||
- triggers:
|
||||
- trigger: fullscreen
|
||||
fullscreen: true
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: substream_on
|
||||
actions_not:
|
||||
# Exiting fullscreen: turn it off again.
|
||||
- triggers:
|
||||
- trigger: fullscreen
|
||||
fullscreen: false
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: substream_off
|
||||
```
|
||||
@@ -121,8 +126,8 @@ elements:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: fullscreen
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: fullscreen
|
||||
- triggers:
|
||||
- trigger: fullscreen
|
||||
fullscreen: false
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
@@ -134,7 +139,7 @@ automations:
|
||||
|
||||
### Responding to key input
|
||||
|
||||
In addition to a handful of reconfigurable [built-in keyboard shortcuts](./usage/keyboard-shortcuts.md), `automations` can be used to take any action based on any keyboard input. These examples use [`key` conditions](./configuration/conditions.md?id=key) to assess keyboard state before taking action.
|
||||
In addition to a handful of reconfigurable [built-in keyboard shortcuts](./usage/keyboard-shortcuts.md), `automations` can be used to take any action based on any keyboard input. These examples use [`key` conditions](./configuration/conditions-triggers.md?id=key) to assess keyboard state before taking action.
|
||||
|
||||
#### Change to `live` temporarily
|
||||
|
||||
@@ -142,8 +147,8 @@ In this example, the view will change to `live`, when `Alt+Z` is pressed, and ch
|
||||
|
||||
```yaml
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: key
|
||||
- triggers:
|
||||
- trigger: key
|
||||
key: z
|
||||
alt: true
|
||||
actions:
|
||||
@@ -163,14 +168,22 @@ In this example, the view will change to `live`, when `Alt+Z` is _held_ down, an
|
||||
|
||||
```yaml
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: key
|
||||
# Held down: switch to live.
|
||||
- triggers:
|
||||
- trigger: key
|
||||
key: z
|
||||
alt: true
|
||||
state: down
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: live
|
||||
actions_not:
|
||||
# Released: switch back to clips.
|
||||
- triggers:
|
||||
- trigger: key
|
||||
key: z
|
||||
alt: true
|
||||
state: up
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: clips
|
||||
```
|
||||
@@ -708,15 +721,20 @@ cameras:
|
||||
# Also allow PTZ controls on the substream.
|
||||
- ptz
|
||||
automations:
|
||||
- actions:
|
||||
# On interaction: turn the HD substream on.
|
||||
- triggers:
|
||||
- trigger: interaction
|
||||
interaction: true
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: substream_on
|
||||
actions_not:
|
||||
# Once the interaction lapses: turn it off again.
|
||||
- triggers:
|
||||
- trigger: interaction
|
||||
interaction: false
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: substream_off
|
||||
conditions:
|
||||
- condition: interaction
|
||||
interaction: true
|
||||
```
|
||||
|
||||
## Media layout
|
||||
@@ -880,7 +898,7 @@ elements:
|
||||
|
||||
## Overriding configuration
|
||||
|
||||
You can override card configuration when certain [conditions](configuration/conditions.md) are met.
|
||||
You can override card configuration when certain [conditions](configuration/conditions-triggers.md) are met.
|
||||
|
||||
### Change menu position based on HA state
|
||||
|
||||
@@ -1165,8 +1183,9 @@ type: custom:advanced-camera-card
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: triggered
|
||||
# Camera triggered: show the alert.
|
||||
- triggers:
|
||||
- trigger: triggered
|
||||
triggered:
|
||||
- camera.office
|
||||
actions:
|
||||
@@ -1182,7 +1201,11 @@ automations:
|
||||
expand: true
|
||||
exclusive: true
|
||||
sufficient: true
|
||||
actions_not:
|
||||
# No longer triggered: reset the status bar.
|
||||
- triggers:
|
||||
- trigger: triggered
|
||||
triggered: []
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: status_bar
|
||||
status_bar_action: reset
|
||||
@@ -1368,8 +1391,8 @@ tap_action:
|
||||
action: perform-action
|
||||
perform_action: homeassistant.service
|
||||
data:
|
||||
camera: '{{ advanced_camera_card.camera }}'
|
||||
view: '{{ advanced_camera_card.view }}'
|
||||
camera: '{{ acc.camera }}'
|
||||
view: '{{ acc.view }}'
|
||||
```
|
||||
|
||||
See [Custom Templates](./configuration/templates.md?id=custom-templates).
|
||||
@@ -1381,14 +1404,14 @@ and values associated with the triggering are included in the action.
|
||||
|
||||
```yaml
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: camera
|
||||
- triggers:
|
||||
- trigger: camera
|
||||
actions:
|
||||
- action: perform-action
|
||||
perform_action: homeassistant.service
|
||||
data:
|
||||
from_camera: '{{ acc.trigger.camera.from }}'
|
||||
to_camera: '{{ acc.trigger.camera.to }}'
|
||||
from_camera: '{{ trigger.from_acc.camera }}'
|
||||
to_camera: '{{ trigger.to_acc.camera }}'
|
||||
```
|
||||
|
||||
See [Trigger Templates](./configuration/templates.md?id=triggers).
|
||||
@@ -1685,10 +1708,11 @@ cameras:
|
||||
debug:
|
||||
logging: true
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: state
|
||||
entity: binary_sensor.door_contact
|
||||
state: 'on'
|
||||
# Door opens: zoom in.
|
||||
- triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.door_contact
|
||||
to: 'on'
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: ptz_digital
|
||||
@@ -1698,7 +1722,12 @@ automations:
|
||||
pan:
|
||||
x: 38
|
||||
y: 20
|
||||
actions_not:
|
||||
# Door closes: zoom back out.
|
||||
- triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.door_contact
|
||||
to: 'off'
|
||||
actions:
|
||||
- action: custom:advanced-camera-card-action
|
||||
advanced_camera_card_action: ptz_digital
|
||||
target_id: camera.living_room
|
||||
|
||||
@@ -43,6 +43,23 @@ To upgrade:
|
||||
If the automatic upgrade button is not visible, your configuration may already
|
||||
be up to date. Try clearing your browser cache and reloading.
|
||||
|
||||
### Configuration could not be fully upgraded
|
||||
|
||||
Automatic configuration upgrades are not perfect. If you see a notification that
|
||||
your configuration could not be fully upgraded, you will find part of your
|
||||
configuration set aside -- untouched -- under an `__UPGRADE_FAILURE__` field.
|
||||
|
||||
To resolve it:
|
||||
|
||||
1. Edit your card's YAML manually and find the `__UPGRADE_FAILURE__` key.
|
||||
1. Re-create each entry under it in the current format (see the relevant
|
||||
configuration documentation in the sidebar). An upgrade usually fails this
|
||||
way when a feature has been retired, or changed enough that the migration
|
||||
needs human judgement.
|
||||
1. Once you're happy with the result, delete the `__UPGRADE_FAILURE__` key.
|
||||
|
||||
The notification clears once the `__UPGRADE_FAILURE__` key is gone.
|
||||
|
||||
### Media does not load
|
||||
|
||||
Media not loading? Permanent "loading circle"?
|
||||
|
||||
@@ -79,7 +79,7 @@ Calls can also be controlled programmatically with the
|
||||
[`call_answer`](../configuration/actions/custom/README.md?id=call_answer), and
|
||||
[`call_end`](../configuration/actions/custom/README.md?id=call_end) actions --
|
||||
for example, from an [automation](../configuration/automations.md) that fires
|
||||
when a doorbell sensor triggers. The [`call` condition](../configuration/conditions.md?id=call)
|
||||
when a doorbell sensor triggers. The [`call` condition](../configuration/conditions-triggers.md?id=call)
|
||||
can be used to show or hide elements while a call is in progress.
|
||||
|
||||
### Call lifecycle
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { KeyedSubscriptionManager } from '../../utils/keyed-subscription-manager';
|
||||
import { KeyedSubscriptionManager } from '../../utils/concurrency/keyed-subscription-manager';
|
||||
import {
|
||||
FrigateEventChange,
|
||||
FrigateReviewChange,
|
||||
|
||||
@@ -203,7 +203,8 @@ export class CameraManager {
|
||||
const engine = engineType
|
||||
? engines.get(engineType) ??
|
||||
(await this._engineFactory.createEngine(engineType, {
|
||||
eventCallback: (ev) => this._api.getTriggersManager().handleCameraEvent(ev),
|
||||
eventCallback: (ev) =>
|
||||
this._api.getCameraTriggersManager().handleCameraEvent(ev),
|
||||
stateWatcher: this._api.getHASSManager().getStateWatcher(),
|
||||
eventWatcher: this._api.getHASSManager().getEventWatcher(),
|
||||
resolvedMediaCache: this._api.getResolvedMediaCache(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ActionContext } from 'action';
|
||||
import { z } from 'zod';
|
||||
import { TriggerData } from '../../condition-trigger/triggers/types.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
Actions,
|
||||
@@ -14,7 +15,11 @@ import { allPromises, errorToConsole } from '../../utils/basic.js';
|
||||
import { TemplateRenderer } from '../templates/index.js';
|
||||
import { CardActionsManagerAPI } from '../types.js';
|
||||
import { ActionSet } from './actions/set.js';
|
||||
import { ActionsExecutionRequest, ActionsExecutor } from './types.js';
|
||||
import {
|
||||
ActionPrepareCallback,
|
||||
ActionsExecutionRequest,
|
||||
ActionsExecutor,
|
||||
} from './types.js';
|
||||
|
||||
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
||||
export type InteractionName = (typeof INTERACTIONS)[number];
|
||||
@@ -138,23 +143,31 @@ export class ActionsManager implements ActionsExecutor {
|
||||
request: ActionsExecutionRequest,
|
||||
renderTemplates = true,
|
||||
): Promise<void> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
const renderedAction: ActionConfig | ActionConfig[] =
|
||||
renderTemplates && hass && this._templateRenderer
|
||||
? (this._templateRenderer.renderRecursively(hass, request.actions, {
|
||||
conditionState: this._api.getConditionStateManager().getState(),
|
||||
triggerData: request?.triggerData,
|
||||
}) as ActionConfig | ActionConfig[])
|
||||
: request.actions;
|
||||
|
||||
const allowedActions = this._api.getLockManager().getAllowedActions(renderedAction);
|
||||
// Lock filtering and the factory both classify on the raw (unrendered)
|
||||
// discriminator (`action` / `advanced_camera_card_action`). A templated
|
||||
// `advanced_camera_card_action` (permitted only by the loose custom-action
|
||||
// schema) is left unresolved, matches no action type, and is dropped.
|
||||
const allowedActions = this._api.getLockManager().getAllowedActions(request.actions);
|
||||
if (!allowedActions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Each action prepares itself (via Action.prepare) just before it runs, so
|
||||
// it observes what an earlier action may have changed. Skip when the caller
|
||||
// opts out (the templates are already rendered) or there is no renderer.
|
||||
const renderer = this._templateRenderer;
|
||||
const actionPrepareCallback =
|
||||
renderTemplates && renderer
|
||||
? this._createActionPrepareCallback(renderer, request.triggerData)
|
||||
: undefined;
|
||||
|
||||
const actionSet = new ActionSet(this._actionContext, allowedActions, {
|
||||
config: request.config,
|
||||
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
||||
factoryOptions: {
|
||||
config: request.config,
|
||||
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
||||
triggerData: request?.triggerData,
|
||||
},
|
||||
actionPrepareCallback,
|
||||
});
|
||||
|
||||
this._actionsInFlight.push(actionSet);
|
||||
@@ -168,4 +181,22 @@ export class ActionsManager implements ActionsExecutor {
|
||||
}
|
||||
this._actionsInFlight = this._actionsInFlight.filter((a) => a !== actionSet);
|
||||
}
|
||||
|
||||
private _createActionPrepareCallback(
|
||||
renderer: TemplateRenderer,
|
||||
triggerData?: TriggerData,
|
||||
): ActionPrepareCallback {
|
||||
// Render against the state (incl. HASS) as it is *when the action runs* --
|
||||
// fixed trigger context, fresh card/HASS state per step. The one cast lives
|
||||
// here as renderRecursively returns `unknown`.
|
||||
return <T>(value: T): T => {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
return hass
|
||||
? (renderer.renderRecursively(hass, value, {
|
||||
conditionState: this._api.getConditionStateManager().getState(),
|
||||
triggerData,
|
||||
}) as T)
|
||||
: value;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,35 @@ import {
|
||||
AuxillaryActionConfig,
|
||||
} from '../../../config/schema/actions/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import { isAdvancedCameraCardCustomAction } from '../../../utils/action';
|
||||
import { getActionName } from '../../../utils/action';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { Action, ActionAbortError } from '../types';
|
||||
import { Action, ActionAbortError, ActionPrepareCallback } from '../types';
|
||||
|
||||
export class BaseAction<T extends ActionConfig> implements Action {
|
||||
protected _context: ActionContext;
|
||||
protected _action: T;
|
||||
protected _rawAction: T;
|
||||
protected _preparedAction: T | null = null;
|
||||
protected _config?: AuxillaryActionConfig;
|
||||
|
||||
constructor(context: ActionContext, action: T, config?: AuxillaryActionConfig) {
|
||||
this._context = context;
|
||||
this._action = action;
|
||||
this._rawAction = action;
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public prepare(actionPrepareCallback: ActionPrepareCallback): void {
|
||||
this._preparedAction = actionPrepareCallback(this._rawAction);
|
||||
}
|
||||
|
||||
// The config to act on: the prepared (rendered) form once prepare() has run,
|
||||
// otherwise the raw config (e.g. under direct execution without a prepare).
|
||||
protected _getAction(): T {
|
||||
return this._preparedAction ?? this._rawAction;
|
||||
}
|
||||
|
||||
protected _shouldSeekConfirmation(api: CardActionsAPI): boolean {
|
||||
const hass = api.getHASSManager().getHASS();
|
||||
const action: ActionConfig = this._action;
|
||||
const action: ActionConfig = this._getAction();
|
||||
|
||||
return (
|
||||
(typeof action.confirmation === 'boolean' && action.confirmation) ||
|
||||
@@ -33,14 +44,10 @@ export class BaseAction<T extends ActionConfig> implements Action {
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
if (this._shouldSeekConfirmation(api)) {
|
||||
const action: ActionConfig = this._action;
|
||||
const baseAction = action.action;
|
||||
const actionName = isAdvancedCameraCardCustomAction(action)
|
||||
? action.advanced_camera_card_action
|
||||
: baseAction;
|
||||
const action: ActionConfig = this._getAction();
|
||||
const text =
|
||||
(typeof action.confirmation === 'object' ? action.confirmation.text : null) ??
|
||||
`${localize('actions.confirmation')}: ${actionName}`;
|
||||
`${localize('actions.confirmation')}: ${getActionName(action)}`;
|
||||
if (!confirm(text)) {
|
||||
throw new ActionAbortError(localize('actions.abort'));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ export class CallServiceAction extends AdvancedCameraCardAction<CallServiceActio
|
||||
return;
|
||||
}
|
||||
|
||||
const [domain, service] = this._action.service.split('.', 2);
|
||||
await hass.callService(domain, service, this._action.data, this._action.target);
|
||||
const action = this._getAction();
|
||||
const [domain, service] = action.service.split('.', 2);
|
||||
await hass.callService(domain, service, action.data, action.target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ export class CallStartAction extends AdvancedCameraCardAction<CallStartActionCon
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
await api.getCallManager().start({
|
||||
cameraID: this._action.camera,
|
||||
streamID: this._action.stream,
|
||||
cameraID: action.camera,
|
||||
streamID: action.stream,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ export class CameraSelectAction extends AdvancedCameraCardAction<CameraSelectAct
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
const selectCameraID =
|
||||
this._action.camera ??
|
||||
(this._action.triggered
|
||||
? api.getTriggersManager().getMostRecentlyTriggeredCameraID()
|
||||
action.camera ??
|
||||
(action.triggered
|
||||
? api.getCameraTriggersManager().getMostRecentlyTriggeredCameraID()
|
||||
: null);
|
||||
const view = api.getViewManager().getView();
|
||||
const config = api.getConfigManager().getConfig();
|
||||
|
||||
@@ -7,6 +7,10 @@ export class CustomAction extends AdvancedCameraCardAction<CustomActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
fireHASSEvent(api.getCardElementManager().getElement(), 'll-custom', this._action);
|
||||
fireHASSEvent(
|
||||
api.getCardElementManager().getElement(),
|
||||
'll-custom',
|
||||
this._getAction(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export class DisplayModeSelectAction extends AdvancedCameraCardAction<DisplayMod
|
||||
|
||||
await api.getViewManager().setViewByParametersWithNewQuery({
|
||||
params: {
|
||||
displayMode: this._action.display_mode,
|
||||
displayMode: this._getAction().display_mode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ export class EffectAction extends AdvancedCameraCardAction<EffectActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
switch (this._action.effect_action) {
|
||||
const action = this._getAction();
|
||||
switch (action.effect_action) {
|
||||
case 'start':
|
||||
api.getEffectsManager().startEffect(this._action.effect);
|
||||
api.getEffectsManager().startEffect(action.effect);
|
||||
break;
|
||||
case 'stop':
|
||||
api.getEffectsManager().stopEffect(this._action.effect);
|
||||
api.getEffectsManager().stopEffect(action.effect);
|
||||
break;
|
||||
case 'toggle':
|
||||
api.getEffectsManager().toggleEffect(this._action.effect);
|
||||
api.getEffectsManager().toggleEffect(action.effect);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ActionContext } from 'action';
|
||||
import { createConditionEvaluator } from '../../../condition-trigger/conditions/factory';
|
||||
import { TriggerData } from '../../../condition-trigger/triggers/types';
|
||||
import {
|
||||
AuxillaryActionConfig,
|
||||
IfActionConfig,
|
||||
} from '../../../config/schema/actions/types';
|
||||
import { TemplateRenderer } from '../../templates/index';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { ActionPrepareCallback } from '../types';
|
||||
import { BaseAction } from './base';
|
||||
|
||||
export class IfAction extends BaseAction<IfActionConfig> {
|
||||
private _triggerData?: TriggerData;
|
||||
|
||||
constructor(
|
||||
context: ActionContext,
|
||||
action: IfActionConfig,
|
||||
config?: AuxillaryActionConfig,
|
||||
triggerData?: TriggerData,
|
||||
) {
|
||||
super(context, action, config);
|
||||
|
||||
this._triggerData = triggerData;
|
||||
}
|
||||
|
||||
public prepare(actionPrepareCallback: ActionPrepareCallback): void {
|
||||
// Render this action's own fields (including the `if` conditions, so their
|
||||
// `trigger.*` templates resolve), but leave `then`/`else` raw: they are
|
||||
// nested action sequences that render per-step when their own branch runs.
|
||||
const { then: thenBranch, else: elseBranch, ...rest } = this._rawAction;
|
||||
this._preparedAction = {
|
||||
...actionPrepareCallback(rest),
|
||||
then: thenBranch,
|
||||
...(elseBranch !== undefined && { else: elseBranch }),
|
||||
};
|
||||
}
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
const evaluatorContext = { templateRenderer: new TemplateRenderer() };
|
||||
const state = api.getConditionStateManager().getState();
|
||||
const conditionsHold = action.if.every(
|
||||
(condition) =>
|
||||
createConditionEvaluator(condition, evaluatorContext).evaluate(state).result,
|
||||
);
|
||||
|
||||
const branch = conditionsHold ? action.then : action.else;
|
||||
if (!branch?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The branch renders per-step as it runs, so each action observes state an
|
||||
// earlier branch action changed. The trigger data is forwarded so the
|
||||
// branch can still resolve `trigger.*` templates.
|
||||
await api.getActionsManager().executeActions({
|
||||
actions: branch,
|
||||
config: this._config,
|
||||
triggerData: this._triggerData,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,6 @@ export class InternalCallbackAction extends AdvancedCameraCardAction<InternalCal
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
await this._action.callback(api);
|
||||
await this._getAction().callback(api);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export class LogAction extends AdvancedCameraCardAction<LogActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
console[this._action.level](this._action.message);
|
||||
const action = this._getAction();
|
||||
console[action.level](action.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActio
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const mediaPlayer = this._action.media_player;
|
||||
const action = this._getAction();
|
||||
const mediaPlayer = action.media_player;
|
||||
const mediaPlayerController = api.getMediaPlayerManager();
|
||||
|
||||
if (this._action.media_player_action === 'stop') {
|
||||
if (action.media_player_action === 'stop') {
|
||||
await mediaPlayerController.stop(mediaPlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export class MoreInfoAction extends AdvancedCameraCardAction<MoreInfoActionConfi
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const entityID = this._action.entity ?? this._config?.entity ?? null;
|
||||
const entityID = this._getAction().entity ?? this._config?.entity ?? null;
|
||||
if (!entityID) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ export class NavigateAction extends AdvancedCameraCardAction<NavigateActionConfi
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
if (!!this._action.navigation_replace) {
|
||||
history.replaceState(null, '', this._action.navigation_path);
|
||||
const action = this._getAction();
|
||||
if (!!action.navigation_replace) {
|
||||
history.replaceState(null, '', action.navigation_path);
|
||||
} else {
|
||||
history.pushState(null, '', this._action.navigation_path);
|
||||
history.pushState(null, '', action.navigation_path);
|
||||
}
|
||||
fireHASSEvent(window, 'location-changed', {
|
||||
replace: !!this._action.navigation_replace,
|
||||
replace: !!action.navigation_replace,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ import { AdvancedCameraCardAction } from './base';
|
||||
export class NotificationAction extends AdvancedCameraCardAction<NotificationActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
api.getNotificationManager().setNotification(this._action.notification);
|
||||
api.getNotificationManager().setNotification(this._getAction().notification);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ export class PerformActionAction extends AdvancedCameraCardAction<PerformActionA
|
||||
return;
|
||||
}
|
||||
|
||||
const [domain, service] = this._action.perform_action.split('.', 2);
|
||||
await hass.callService(domain, service, this._action.data, this._action.target);
|
||||
const action = this._getAction();
|
||||
const [domain, service] = action.perform_action.split('.', 2);
|
||||
await hass.callService(domain, service, action.data, action.target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActio
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
const currentEnabled = api.getViewManager().getView()?.context?.ptzControls?.enabled;
|
||||
|
||||
// If `enabled` is explicit, use it. If only `type` is being changed, leave
|
||||
@@ -13,8 +14,8 @@ export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActio
|
||||
// toggle the current enabled value — this is the menu-button show/hide use
|
||||
// case.
|
||||
const enabled =
|
||||
this._action.enabled ??
|
||||
(this._action.type
|
||||
action.enabled ??
|
||||
(action.type
|
||||
? undefined
|
||||
: currentEnabled === undefined
|
||||
? undefined
|
||||
@@ -23,7 +24,7 @@ export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActio
|
||||
api.getViewManager().setViewWithMergedContext({
|
||||
ptzControls: {
|
||||
...(enabled !== undefined && { enabled }),
|
||||
...(this._action.type && { type: this._action.type }),
|
||||
...(action.type && { type: action.type }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,25 +48,26 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
const view = api.getViewManager().getView();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetID =
|
||||
this._action.target_id ??
|
||||
action.target_id ??
|
||||
getPTZTarget(view, { type: 'digital', cameraManager: api.getCameraManager() })
|
||||
?.targetID;
|
||||
if (!targetID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!this._action.absolute || !this._action.ptz_phase) {
|
||||
if (!!action.absolute || !action.ptz_phase) {
|
||||
return await this._stepChange(api, targetID);
|
||||
}
|
||||
|
||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||
if (this._action.ptz_phase === 'start') {
|
||||
if (action.ptz_phase === 'start') {
|
||||
await stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
||||
setInProgressForThisTarget(targetID, this._context, 'ptzDigital', this);
|
||||
|
||||
@@ -74,21 +75,22 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
|
||||
this._timer.startRepeated(STEP_DELAY_SECONDS, () =>
|
||||
this._stepChange(api, targetID),
|
||||
);
|
||||
} else if (this._action.ptz_phase === 'stop') {
|
||||
} else if (action.ptz_phase === 'stop') {
|
||||
await stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
||||
delete this._context.ptzDigital?.[targetID];
|
||||
}
|
||||
}
|
||||
|
||||
private _convertActionToZoomSettings(base?: PartialZoomSettings): PartialZoomSettings {
|
||||
if (!this._action.absolute && !this._action.ptz_action) {
|
||||
const action = this._getAction();
|
||||
if (!action.absolute && !action.ptz_action) {
|
||||
// If neither an absolute position nor an action are specified, the request
|
||||
// is assumed to be to return to default.
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this._action.absolute) {
|
||||
return this._action.absolute;
|
||||
if (action.absolute) {
|
||||
return action.absolute;
|
||||
}
|
||||
|
||||
const zoom = base?.zoom ?? ZOOM_DEFAULT_SCALE;
|
||||
@@ -98,21 +100,21 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
|
||||
};
|
||||
|
||||
const zoomDelta =
|
||||
this._action.ptz_action === 'zoom_in'
|
||||
action.ptz_action === 'zoom_in'
|
||||
? STEP_ZOOM
|
||||
: this._action.ptz_action === 'zoom_out'
|
||||
: action.ptz_action === 'zoom_out'
|
||||
? -STEP_ZOOM
|
||||
: 0;
|
||||
const xDelta =
|
||||
this._action.ptz_action === 'left'
|
||||
action.ptz_action === 'left'
|
||||
? -STEP_PAN
|
||||
: this._action.ptz_action === 'right'
|
||||
: action.ptz_action === 'right'
|
||||
? STEP_PAN
|
||||
: 0;
|
||||
const yDelta =
|
||||
this._action.ptz_action === 'up'
|
||||
action.ptz_action === 'up'
|
||||
? -STEP_PAN
|
||||
: this._action.ptz_action === 'down'
|
||||
: action.ptz_action === 'down'
|
||||
? STEP_PAN
|
||||
: 0;
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfi
|
||||
let targetID: string | null = null;
|
||||
let type: PTZType | null = null;
|
||||
|
||||
if (this._action.target_id) {
|
||||
targetID = this._action.target_id;
|
||||
const action = this._getAction();
|
||||
if (action.target_id) {
|
||||
targetID = action.target_id;
|
||||
type = hasCameraTruePTZ(api.getCameraManager(), targetID) ? 'ptz' : 'digital';
|
||||
} else if (view) {
|
||||
const multiTarget = getPTZTarget(view, { cameraManager: api.getCameraManager() });
|
||||
@@ -34,26 +35,28 @@ export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfi
|
||||
}
|
||||
|
||||
private _toPTZAction(targetID: string): PTZAction {
|
||||
const action = this._getAction();
|
||||
return new PTZAction(
|
||||
this._context,
|
||||
createPTZAction({
|
||||
cardID: this._action.card_id,
|
||||
cardID: action.card_id,
|
||||
cameraID: targetID,
|
||||
ptzAction: this._action.ptz_action,
|
||||
ptzPhase: this._action.ptz_phase,
|
||||
ptzPreset: this._action.ptz_preset,
|
||||
ptzAction: action.ptz_action,
|
||||
ptzPhase: action.ptz_phase,
|
||||
ptzPreset: action.ptz_preset,
|
||||
}),
|
||||
this._config,
|
||||
);
|
||||
}
|
||||
|
||||
private _toPTZDigitalAction(targetID: string): PTZDigitalAction {
|
||||
const action = this._getAction();
|
||||
return new PTZDigitalAction(
|
||||
this._context,
|
||||
createPTZDigitalAction({
|
||||
cardID: this._action.card_id,
|
||||
ptzPhase: this._action.ptz_phase,
|
||||
ptzAction: this._action.ptz_action,
|
||||
cardID: action.card_id,
|
||||
ptzPhase: action.ptz_phase,
|
||||
ptzAction: action.ptz_action,
|
||||
targetID: targetID,
|
||||
}),
|
||||
this._config,
|
||||
|
||||
@@ -33,13 +33,15 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
|
||||
const view = api.getViewManager().getView();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ptzCameraID =
|
||||
this._action.camera ??
|
||||
action.camera ??
|
||||
getPTZTarget(view, { type: 'ptz', cameraManager: api.getCameraManager() })
|
||||
?.targetID ??
|
||||
null;
|
||||
@@ -53,34 +55,34 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._action.ptz_action) {
|
||||
if (!action.ptz_action) {
|
||||
if (ptzCapabilities.presets && ptzCapabilities.presets.length >= 1) {
|
||||
await api.getCameraManager().executePTZAction(ptzCameraID, 'preset', {
|
||||
phase: this._action.ptz_phase,
|
||||
phase: action.ptz_phase,
|
||||
preset: ptzCapabilities.presets[0],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const capabilityKey = ptzActionToCapabilityKey(this._action.ptz_action);
|
||||
const capabilityKey = ptzActionToCapabilityKey(action.ptz_action);
|
||||
if (
|
||||
(capabilityKey &&
|
||||
ptzCapabilities[capabilityKey]?.includes(
|
||||
this._action.ptz_phase ? PTZMovementType.Continuous : PTZMovementType.Relative,
|
||||
action.ptz_phase ? PTZMovementType.Continuous : PTZMovementType.Relative,
|
||||
)) ||
|
||||
this._action.ptz_action === 'preset'
|
||||
action.ptz_action === 'preset'
|
||||
) {
|
||||
// Scenario: Camera natively supports requested move type.
|
||||
return await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
phase: this._action.ptz_phase,
|
||||
preset: this._action.ptz_preset,
|
||||
.executePTZAction(ptzCameraID, action.ptz_action, {
|
||||
phase: action.ptz_phase,
|
||||
preset: action.ptz_preset,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._action.ptz_phase === 'start') {
|
||||
if (action.ptz_phase === 'start') {
|
||||
// Scenario: Asked to start a continuous move, camera only supports relative moves natively.
|
||||
await stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
||||
setInProgressForThisTarget(ptzCameraID, this._context, 'ptz', this);
|
||||
@@ -88,12 +90,10 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
const singleStep = async (): Promise<void> => {
|
||||
/* istanbul ignore else: the else path cannot be reached as ptz_action
|
||||
being present is checked above -- @preserve */
|
||||
if (this._action.ptz_action) {
|
||||
await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
});
|
||||
if (action.ptz_action) {
|
||||
await api.getCameraManager().executePTZAction(ptzCameraID, action.ptz_action, {
|
||||
preset: action.ptz_preset,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._stopped) {
|
||||
@@ -109,30 +109,26 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
|
||||
this._stopped = false;
|
||||
await singleStep();
|
||||
} else if (this._action.ptz_phase === 'stop') {
|
||||
} else if (action.ptz_phase === 'stop') {
|
||||
// Scenario: Asked to stop continuous move, camera only supports relative moves natively.
|
||||
await stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
||||
} else {
|
||||
this._stopped = false;
|
||||
|
||||
// Relative move (but camera only supports continuous).
|
||||
await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
phase: 'start',
|
||||
});
|
||||
await api.getCameraManager().executePTZAction(ptzCameraID, action.ptz_action, {
|
||||
preset: action.ptz_preset,
|
||||
phase: 'start',
|
||||
});
|
||||
|
||||
this._timer.start(ptzConfiguration.c2r_delay_between_calls_seconds, async () => {
|
||||
/* istanbul ignore else: the else path cannot be reached as ptz_action
|
||||
being present is checked above -- @preserve */
|
||||
if (this._action.ptz_action) {
|
||||
await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
phase: 'stop',
|
||||
});
|
||||
if (action.ptz_action) {
|
||||
await api.getCameraManager().executePTZAction(ptzCameraID, action.ptz_action, {
|
||||
preset: action.ptz_preset,
|
||||
phase: 'stop',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class SetReviewAction extends AdvancedCameraCardAction<SetReviewActionCon
|
||||
return;
|
||||
}
|
||||
|
||||
const targetReviewedState = this._action.reviewed;
|
||||
const targetReviewedState = this._getAction().reviewed;
|
||||
if (targetReviewedState !== undefined && targetReviewedState === item.isReviewed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import { ActionContext } from 'action';
|
||||
import {
|
||||
ActionConfig,
|
||||
AuxillaryActionConfig,
|
||||
} from '../../../config/schema/actions/types';
|
||||
import { ActionConfig } from '../../../config/schema/actions/types';
|
||||
import { arrayify } from '../../../utils/basic';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { ActionFactory } from '../factory';
|
||||
import { Action } from '../types';
|
||||
import { ActionFactory, ActionFactoryOptions } from '../factory';
|
||||
import { ActionPrepareCallback } from '../types';
|
||||
|
||||
export class ActionSet implements Action {
|
||||
interface ActionSetOptions {
|
||||
factoryOptions?: ActionFactoryOptions;
|
||||
actionPrepareCallback?: ActionPrepareCallback;
|
||||
}
|
||||
|
||||
export class ActionSet {
|
||||
private _context: ActionContext;
|
||||
private _actions: Action[] = [];
|
||||
private _actions: ActionConfig[];
|
||||
private _factoryOptions?: ActionFactoryOptions;
|
||||
private _actionPrepareCallback?: ActionPrepareCallback;
|
||||
private _factory = new ActionFactory();
|
||||
private _stopped = false;
|
||||
|
||||
constructor(
|
||||
context: ActionContext,
|
||||
actions: ActionConfig | ActionConfig[],
|
||||
options?: {
|
||||
config?: AuxillaryActionConfig;
|
||||
cardID?: string;
|
||||
},
|
||||
options?: ActionSetOptions,
|
||||
) {
|
||||
this._context = context;
|
||||
for (const actionObj of arrayify(actions)) {
|
||||
const action = this._factory.createAction(context, actionObj, options);
|
||||
if (action) {
|
||||
this._actions.push(action);
|
||||
}
|
||||
}
|
||||
this._actions = arrayify(actions);
|
||||
this._actionPrepareCallback = options?.actionPrepareCallback;
|
||||
this._factoryOptions = options?.factoryOptions;
|
||||
}
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
@@ -37,7 +35,20 @@ export class ActionSet implements Action {
|
||||
break;
|
||||
}
|
||||
|
||||
await action.execute(api);
|
||||
const concreteAction = this._factory.createAction(
|
||||
this._context,
|
||||
action,
|
||||
this._factoryOptions,
|
||||
);
|
||||
if (concreteAction) {
|
||||
// Prepare against the state as it is now, so an action observes what an
|
||||
// earlier action in the sequence changed. A prepare error aborts the
|
||||
// rest of the sequence (it propagates to the caller's handler).
|
||||
if (this._actionPrepareCallback) {
|
||||
concreteAction.prepare(this._actionPrepareCallback);
|
||||
}
|
||||
await concreteAction.execute(api);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ export class SleepAction extends AdvancedCameraCardAction<SleepActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
await sleep(timeDeltaToSeconds(this._action.duration));
|
||||
await sleep(timeDeltaToSeconds(this._getAction().duration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@ export class StatusBarAction extends AdvancedCameraCardAction<StatusBarActionCon
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
switch (this._action.status_bar_action) {
|
||||
const action = this._getAction();
|
||||
switch (action.status_bar_action) {
|
||||
case 'reset':
|
||||
api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
||||
break;
|
||||
case 'add':
|
||||
this._action.items?.forEach((item) =>
|
||||
action.items?.forEach((item) =>
|
||||
api.getStatusBarItemManager().addDynamicStatusBarItem(item),
|
||||
);
|
||||
break;
|
||||
case 'remove':
|
||||
this._action.items?.forEach((item) =>
|
||||
action.items?.forEach((item) =>
|
||||
api.getStatusBarItemManager().removeDynamicStatusBarItem(item),
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -8,7 +8,7 @@ export class SubstreamOffAction extends AdvancedCameraCardAction<SubstreamOffAct
|
||||
await super.execute(api);
|
||||
|
||||
api.getViewManager().setViewByParameters({
|
||||
modifiers: [new SubstreamViewModifier({ camera: this._action.camera })],
|
||||
modifiers: [new SubstreamViewModifier({ camera: this._getAction().camera })],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@ export class SubstreamOnAction extends AdvancedCameraCardAction<SubstreamOnActio
|
||||
return;
|
||||
}
|
||||
|
||||
const cameraID = this._action.camera ?? view.camera;
|
||||
const action = this._getAction();
|
||||
const cameraID = action.camera ?? view.camera;
|
||||
if (!cameraID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream =
|
||||
this._action.stream ??
|
||||
action.stream ??
|
||||
this._getCycledSubstreamID(view, cameraID, api.getCameraManager());
|
||||
|
||||
api.getViewManager().setViewByParameters({
|
||||
|
||||
@@ -6,6 +6,6 @@ export class URLAction extends AdvancedCameraCardAction<URLActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
window.open(this._action.url_path);
|
||||
window.open(this._getAction().url_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ export class ViewAction extends AdvancedCameraCardAction<ViewActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const action = this._getAction();
|
||||
await api.getViewManager().setViewByParametersWithNewQuery({
|
||||
params: {
|
||||
view: this._action.advanced_camera_card_action,
|
||||
view: action.advanced_camera_card_action,
|
||||
},
|
||||
...(this._action.folder && {
|
||||
...(action.folder && {
|
||||
queryExecutorOptions: {
|
||||
folder: this._action.folder,
|
||||
folder: action.folder,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ActionContext } from 'action';
|
||||
import { TriggerData } from '../../condition-trigger/triggers/types';
|
||||
import { INTERNAL_CALLBACK_ACTION } from '../../config/schema/actions/custom/internal';
|
||||
import { ActionConfig, AuxillaryActionConfig } from '../../config/schema/actions/types';
|
||||
import { isAdvancedCameraCardCustomAction } from '../../utils/action';
|
||||
import { isAdvancedCameraCardCustomAction, isIfAction } from '../../utils/action';
|
||||
import { CallAnswerAction } from './actions/call-answer';
|
||||
import { CallEndAction } from './actions/call-end';
|
||||
import { CallServiceAction } from './actions/call-service';
|
||||
@@ -15,6 +16,7 @@ import { DownloadAction } from './actions/download';
|
||||
import { EffectAction } from './actions/effect';
|
||||
import { ExpandAction } from './actions/expand';
|
||||
import { FullscreenAction } from './actions/fullscreen';
|
||||
import { IfAction } from './actions/if';
|
||||
import { InfoAction } from './actions/info';
|
||||
import { InternalCallbackAction } from './actions/internal-callback';
|
||||
import { LogAction } from './actions/log';
|
||||
@@ -50,23 +52,36 @@ import { URLAction } from './actions/url';
|
||||
import { ViewAction } from './actions/view';
|
||||
import { Action } from './types';
|
||||
|
||||
export interface ActionFactoryOptions {
|
||||
config?: AuxillaryActionConfig;
|
||||
cardID?: string;
|
||||
|
||||
// The firing automation's trigger payload (if any), forwarded to actions with
|
||||
// nested actions (e.g. `if`) so their branches can still resolve `trigger.*`
|
||||
// templates when they render per-step.
|
||||
triggerData?: TriggerData;
|
||||
}
|
||||
|
||||
export class ActionFactory {
|
||||
public createAction(
|
||||
context: ActionContext,
|
||||
action: ActionConfig,
|
||||
options?: {
|
||||
config?: AuxillaryActionConfig;
|
||||
cardID?: string;
|
||||
},
|
||||
options?: ActionFactoryOptions,
|
||||
): Action | null {
|
||||
if (
|
||||
// Command not intended for this card (e.g. query string command).
|
||||
// `card_id` is a static routing identifier, matched on the raw (template
|
||||
// unrendered) config.
|
||||
action.card_id &&
|
||||
action.card_id !== options?.cardID
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isIfAction(action)) {
|
||||
return new IfAction(context, action, options?.config, options?.triggerData);
|
||||
}
|
||||
|
||||
switch (action.action) {
|
||||
case 'more-info':
|
||||
return new MoreInfoAction(context, action, options?.config);
|
||||
@@ -182,11 +197,12 @@ export class ActionFactory {
|
||||
return new InternalCallbackAction(context, action, options?.config);
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
// Reached when the discriminator is not a known action type -- e.g. a
|
||||
// templated `advanced_camera_card_action`, which is classified on the raw
|
||||
// (unrendered) action and so never matches a case.
|
||||
console.warn(
|
||||
`Advanced Camera Card received unknown card action: ${action['advanced_camera_card_action']}`,
|
||||
);
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConditionsTriggerData } from '../../conditions/types.js';
|
||||
import { TriggerData } from '../../condition-trigger/triggers/types.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
AuxillaryActionConfig,
|
||||
@@ -6,7 +6,18 @@ import {
|
||||
import { AdvancedCameraCardError } from '../../types.js';
|
||||
import { CardActionsAPI } from '../types';
|
||||
|
||||
// Renders a value's templates, returning a rendered copy. Generic so it
|
||||
// preserves the value's type (the one cast lives in the renderer that supplies
|
||||
// it).
|
||||
export type ActionPrepareCallback = <T>(value: T) => T;
|
||||
|
||||
export interface Action {
|
||||
// Prepare this action for execution by rendering its templates against the
|
||||
// current state. The rendered copy is stored separately; the original config
|
||||
// is left intact, so the action stays reusable. Structural actions (`if`)
|
||||
// override this to leave their nested action sequences raw, so those render
|
||||
// per-step when their branch runs.
|
||||
prepare(actionPrepareCallback: ActionPrepareCallback): void;
|
||||
execute(api: CardActionsAPI): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
@@ -14,7 +25,7 @@ export interface Action {
|
||||
export interface ActionsExecutionRequest {
|
||||
actions: ActionConfig[] | ActionConfig;
|
||||
config?: AuxillaryActionConfig;
|
||||
triggerData?: ConditionsTriggerData;
|
||||
triggerData?: TriggerData;
|
||||
}
|
||||
|
||||
export interface ActionsExecutor {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ConditionsManager } from '../conditions/conditions-manager.js';
|
||||
import { ConditionsEvaluationResult } from '../conditions/types.js';
|
||||
import { ConditionEvaluator } from '../condition-trigger/conditions/conditions/types.js';
|
||||
import { createConditionEvaluator } from '../condition-trigger/conditions/factory.js';
|
||||
import { TriggersManager } from '../condition-trigger/triggers/manager.js';
|
||||
import { TriggerData } from '../condition-trigger/triggers/types.js';
|
||||
import { Automation, AutomationActions } from '../config/schema/automations.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { TemplateRenderer } from './templates/index.js';
|
||||
import { CardAutomationsAPI, TaggedAutomation } from './types.js';
|
||||
|
||||
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
||||
@@ -9,7 +12,7 @@ const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
||||
export class AutomationsManager {
|
||||
private _api: CardAutomationsAPI;
|
||||
|
||||
private _automations = new Map<TaggedAutomation, ConditionsManager>();
|
||||
private _automations = new Map<TaggedAutomation, TriggersManager>();
|
||||
|
||||
// A counter to avoid infinite loops, increases every time actions are run,
|
||||
// decreases every time actions are complete.
|
||||
@@ -20,28 +23,39 @@ export class AutomationsManager {
|
||||
}
|
||||
|
||||
public deleteAutomations(tag?: unknown) {
|
||||
for (const [automation, conditionManager] of this._automations) {
|
||||
for (const [automation, triggers] of this._automations) {
|
||||
if (automation.tag === tag) {
|
||||
this._automations.delete(automation);
|
||||
conditionManager.destroy();
|
||||
triggers.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addAutomations(automations: TaggedAutomation[]): void {
|
||||
const context = { templateRenderer: new TemplateRenderer() };
|
||||
for (const automation of automations) {
|
||||
const conditionManager = new ConditionsManager(
|
||||
automation.conditions,
|
||||
const triggers = new TriggersManager(
|
||||
automation.triggers,
|
||||
this._api.getConditionStateManager(),
|
||||
);
|
||||
conditionManager.addListener((result: ConditionsEvaluationResult) =>
|
||||
this._execute(automation, result),
|
||||
|
||||
// The ongoing `conditions:` block is pull-evaluated at trigger time, so
|
||||
// its evaluators are never subscribed and hold no resources to tear down.
|
||||
// They live in the trigger callback and are released when `triggers` is
|
||||
// destroyed.
|
||||
const conditions = (automation.conditions ?? []).map((condition) =>
|
||||
createConditionEvaluator(condition, context),
|
||||
);
|
||||
this._automations.set(automation, conditionManager);
|
||||
triggers.addListener((data) => this._execute(automation, conditions, data));
|
||||
this._automations.set(automation, triggers);
|
||||
}
|
||||
}
|
||||
|
||||
private _execute(automation: Automation, result: ConditionsEvaluationResult): void {
|
||||
private _execute(
|
||||
automation: Automation,
|
||||
conditions: ConditionEvaluator[],
|
||||
triggerData: TriggerData,
|
||||
): void {
|
||||
if (
|
||||
!this._api.getHASSManager().hasHASS() ||
|
||||
// Never execute automations if the card hasn't finished initializing, as
|
||||
@@ -55,17 +69,25 @@ export class AutomationsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldExecute = result.result;
|
||||
const actions = shouldExecute ? automation.actions : automation.actions_not;
|
||||
// Evaluate the ongoing conditions against the current state at the instant
|
||||
// the automation is triggered. The state manager updates its stored state
|
||||
// before dispatching to listeners, so this already reflects the triggering
|
||||
// change.
|
||||
const state = this._api.getConditionStateManager().getState();
|
||||
const ongoingConditionsHold = conditions.every(
|
||||
(evaluator) => evaluator.evaluate(state).result,
|
||||
);
|
||||
|
||||
if (!actions?.length) {
|
||||
if (!ongoingConditionsHold || !automation.actions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runActions = async (actions: AutomationActions): Promise<void> => {
|
||||
++this._nestedAutomationExecutions;
|
||||
|
||||
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
||||
// Check the limit *before* incrementing, so the overflow path holds no
|
||||
// increment to leak; the `finally` then guarantees the decrement even if
|
||||
// executing the actions throws. Either leak would permanently inflate the
|
||||
// counter and eventually block all automations.
|
||||
if (this._nestedAutomationExecutions >= MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
||||
this._api.getNotificationManager().setNotification({
|
||||
heading: {
|
||||
text: localize('error.too_many_automations'),
|
||||
@@ -76,12 +98,13 @@ export class AutomationsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._api
|
||||
.getActionsManager()
|
||||
.executeActions({ actions, triggerData: result.triggerData });
|
||||
|
||||
--this._nestedAutomationExecutions;
|
||||
++this._nestedAutomationExecutions;
|
||||
try {
|
||||
await this._api.getActionsManager().executeActions({ actions, triggerData });
|
||||
} finally {
|
||||
--this._nestedAutomationExecutions;
|
||||
}
|
||||
};
|
||||
runActions(actions);
|
||||
runActions(automation.actions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createNotificationFromText } from '../../components-lib/notification/factory';
|
||||
import { ConditionStateChange } from '../../conditions/types';
|
||||
import { ConditionStateChange } from '../../condition-trigger/conditions/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { Timer } from '../../utils/timer';
|
||||
import { getStreamCameraID } from '../../view/substream';
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ import { maxBy, throttle } from 'lodash-es';
|
||||
import { CameraEvent } from '../camera-manager/types';
|
||||
import { isTriggeredState } from '../ha/is-triggered-state';
|
||||
import { Timer } from '../utils/timer';
|
||||
import { CardTriggersAPI } from './types';
|
||||
import { CardCameraTriggersAPI } from './types';
|
||||
|
||||
interface CameraTriggerState {
|
||||
// The time of the most recent trigger event. Used to determine the most
|
||||
@@ -26,15 +26,15 @@ interface CameraTriggerState {
|
||||
untriggerForceTimer?: Timer;
|
||||
}
|
||||
|
||||
export class TriggersManager {
|
||||
private _api: CardTriggersAPI;
|
||||
export class CameraTriggersManager {
|
||||
private _api: CardCameraTriggersAPI;
|
||||
private _states: Map<string, CameraTriggerState> = new Map();
|
||||
|
||||
private _throttledTriggerAction = throttle(this._triggerAction.bind(this), 1000, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
constructor(api: CardTriggersAPI) {
|
||||
constructor(api: CardCameraTriggersAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ export class CardElementManager {
|
||||
// reconnection, to ensure the state subscription/unsubscription works
|
||||
// correctly and triggers that changed while detached are picked up.
|
||||
// Reset trigger state first to stop stale timers and clear condition state.
|
||||
this._api.getTriggersManager().reset();
|
||||
this._api.getCameraTriggersManager().reset();
|
||||
|
||||
this._api.getCallManager().uninitialize();
|
||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
||||
|
||||
@@ -22,9 +22,9 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
|
||||
const automations: TaggedAutomation[] = [
|
||||
{
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'config' as const,
|
||||
trigger: 'config' as const,
|
||||
paths: ['cameras', 'remote_control.entities.camera'],
|
||||
},
|
||||
],
|
||||
@@ -38,9 +38,9 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
tag: automationTag,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'camera' as const,
|
||||
trigger: 'camera' as const,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
@@ -63,9 +63,9 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
// this one, as automations only run *after* the card is initialized (and
|
||||
// it very likely will not yet be). Instead, wait to be initialized, then
|
||||
// set the camera.
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'initialized' as const,
|
||||
trigger: 'initialized' as const,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
@@ -81,15 +81,15 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
tag: automationTag,
|
||||
},
|
||||
{
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'state' as const,
|
||||
entity: cameraControlEntity,
|
||||
trigger: 'state' as const,
|
||||
entity_id: cameraControlEntity,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
// When the entity state changes, updated the selected option.
|
||||
createCameraAction('{{ advanced_camera_card.trigger.state.to }}'),
|
||||
createCameraAction('{{ trigger.to_state.state }}'),
|
||||
],
|
||||
tag: automationTag,
|
||||
},
|
||||
|
||||
@@ -64,9 +64,9 @@ const convertKeyboardShortcutsToAutomations = (
|
||||
}
|
||||
|
||||
automations.push({
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'key' as const,
|
||||
trigger: 'key' as const,
|
||||
key: shortcut.key,
|
||||
state: 'down',
|
||||
shift: shortcut.shift,
|
||||
@@ -85,9 +85,9 @@ const convertKeyboardShortcutsToAutomations = (
|
||||
});
|
||||
|
||||
automations.push({
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'key' as const,
|
||||
trigger: 'key' as const,
|
||||
key: shortcut.key,
|
||||
state: 'up',
|
||||
},
|
||||
@@ -105,9 +105,9 @@ const convertKeyboardShortcutsToAutomations = (
|
||||
const homeShortcut = shortcuts.ptz_home;
|
||||
if (homeShortcut) {
|
||||
automations.push({
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'key' as const,
|
||||
trigger: 'key' as const,
|
||||
key: homeShortcut.key,
|
||||
state: 'down',
|
||||
shift: homeShortcut.shift,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { get, merge } from 'lodash-es';
|
||||
import { ConditionsManager } from '../../conditions/conditions-manager';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../conditions/types';
|
||||
import { ConditionsManager } from '../../condition-trigger/conditions/conditions-manager';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../condition-trigger/conditions/types';
|
||||
import {
|
||||
copyConfig,
|
||||
deleteConfigValue,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactiveController } from 'lit';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { ConditionStateManager } from '../conditions/state-manager';
|
||||
import { ConditionStateManager } from '../condition-trigger/conditions/state-manager';
|
||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||
import { DeviceCache } from '../ha/registry/device/types';
|
||||
@@ -11,6 +11,7 @@ import { LovelaceCardEditor } from '../ha/types';
|
||||
import { ActionsManager } from './actions/actions-manager';
|
||||
import { AutomationsManager } from './automations-manager';
|
||||
import { CallManager } from './call/manager';
|
||||
import { CameraTriggersManager } from './camera-triggers-manager';
|
||||
import { CameraURLManager } from './camera-url-manager';
|
||||
import {
|
||||
CardElementManager,
|
||||
@@ -40,11 +41,11 @@ import { QueryStringManager } from './query-string-manager';
|
||||
import { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import { StyleManager } from './style-manager';
|
||||
import { TemplateRenderer } from './templates';
|
||||
import { TriggersManager } from './triggers-manager';
|
||||
import {
|
||||
CardActionsManagerAPI,
|
||||
CardAutomationsAPI,
|
||||
CardCameraAPI,
|
||||
CardCameraTriggersAPI,
|
||||
CardCameraURLAPI,
|
||||
CardConditionAPI,
|
||||
CardConfigAPI,
|
||||
@@ -66,7 +67,6 @@ import {
|
||||
CardPIPAPI,
|
||||
CardQueryStringAPI,
|
||||
CardStyleAPI,
|
||||
CardTriggersAPI,
|
||||
CardViewAPI,
|
||||
} from './types';
|
||||
import { ViewItemManager } from './view/item-manager';
|
||||
@@ -98,7 +98,7 @@ export class CardController
|
||||
CardNotificationAPI,
|
||||
CardQueryStringAPI,
|
||||
CardStyleAPI,
|
||||
CardTriggersAPI,
|
||||
CardCameraTriggersAPI,
|
||||
CardViewAPI,
|
||||
ReactiveController
|
||||
{
|
||||
@@ -137,7 +137,7 @@ export class CardController
|
||||
private _queryStringManager = new QueryStringManager(this);
|
||||
private _statusBarItemManager = new StatusBarItemManager(this);
|
||||
private _styleManager = new StyleManager(this);
|
||||
private _triggersManager = new TriggersManager(this);
|
||||
private _cameraTriggersManager = new CameraTriggersManager(this);
|
||||
private _viewManager = new ViewManager(this);
|
||||
private _viewItemManager = new ViewItemManager(this);
|
||||
|
||||
@@ -305,8 +305,8 @@ export class CardController
|
||||
return this._styleManager;
|
||||
}
|
||||
|
||||
public getTriggersManager(): TriggersManager {
|
||||
return this._triggersManager;
|
||||
public getCameraTriggersManager(): CameraTriggersManager {
|
||||
return this._cameraTriggersManager;
|
||||
}
|
||||
|
||||
public getViewManager(): ViewManager {
|
||||
|
||||
@@ -55,9 +55,9 @@ export class DefaultManager {
|
||||
this._api.getAutomationsManager().addAutomations([
|
||||
{
|
||||
actions: [createGeneralAction('default')],
|
||||
conditions: [
|
||||
triggers: [
|
||||
{
|
||||
condition: 'interaction' as const,
|
||||
trigger: 'interaction' as const,
|
||||
interaction: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { FolderConfig, FolderType, folderTypeSchema } from '../../config/schema/folders';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sub } from 'date-fns';
|
||||
import { NonEmptyTuple } from 'type-fest';
|
||||
import { ConditionState } from '../../../conditions/types';
|
||||
import { ConditionState } from '../../../condition-trigger/conditions/types';
|
||||
import {
|
||||
FolderConfig,
|
||||
folderTypeSchema,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sub } from 'date-fns';
|
||||
import { ConditionState } from '../../../conditions/types';
|
||||
import { ConditionState } from '../../../condition-trigger/conditions/types';
|
||||
import {
|
||||
DateMatcher,
|
||||
Matcher,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { FolderConfig, FolderConfigWithoutID } from '../../config/schema/folders';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { hasUnsupportedFilters } from '../../query-source.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NonEmptyTuple } from 'type-fest';
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { FolderConfig, HAFolderPathComponent } from '../../config/schema/folders';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConditionStateChange } from '../../../conditions/types';
|
||||
import { ConditionStateChange } from '../../../condition-trigger/conditions/types';
|
||||
import { WebkitHTMLVideoElement } from '../../../types';
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { FullscreenProviderBase } from '../provider';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HassEvent } from 'home-assistant-js-websocket';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { KeyedSubscriptionManager } from '../../utils/keyed-subscription-manager';
|
||||
import { KeyedSubscriptionManager } from '../../utils/concurrency/keyed-subscription-manager';
|
||||
|
||||
export interface EventSubscriptionRequest {
|
||||
event_type: string;
|
||||
|
||||
@@ -160,7 +160,7 @@ export class InitializationManager {
|
||||
this._initializer.initializeIfNecessary(
|
||||
InitializationAspect.INITIAL_TRIGGER,
|
||||
async () => {
|
||||
await this._api.getTriggersManager().handleInitialCameraTriggers();
|
||||
await this._api.getCameraTriggersManager().handleInitialCameraTriggers();
|
||||
|
||||
// Force a card update to continue the initialization.
|
||||
this._api.getCardElementManager().update();
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CardIssueManagerAPI } from '../types';
|
||||
import { IssueManager } from './issue-manager';
|
||||
import { ConfigErrorIssue } from './issues/config-error';
|
||||
import { ConfigUpgradeIssue } from './issues/config-upgrade';
|
||||
import { ConfigUpgradeFailureIssue } from './issues/config-upgrade-failure';
|
||||
import { ConnectionIssue } from './issues/connection';
|
||||
import { InitializationIssue } from './issues/initialization';
|
||||
import { LegacyResourceIssue } from './issues/legacy-resource';
|
||||
@@ -19,6 +20,7 @@ export const createIssueManager = (api: CardIssueManagerAPI): IssueManager => {
|
||||
// full-card issue. Register broader/more critical issues first.
|
||||
manager.addIssue(new ConfigErrorIssue());
|
||||
manager.addIssue(new ConfigUpgradeIssue(api));
|
||||
manager.addIssue(new ConfigUpgradeFailureIssue(api));
|
||||
manager.addIssue(new ViewIncompatibleIssue(api));
|
||||
manager.addIssue(new ConnectionIssue());
|
||||
manager.addIssue(new InitializationIssue(api));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IssueTriggerContext } from 'issue';
|
||||
import { ConditionStateChange } from '../../conditions/types';
|
||||
import { ConditionStateChange } from '../../condition-trigger/conditions/types';
|
||||
import { isActionAllowedBasedOnInteractionState } from '../../utils/interaction-mode';
|
||||
import { Timer } from '../../utils/timer';
|
||||
import { CardIssueManagerAPI } from '../types';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { hasConfigUpgradeFailures } from '../../../config/management.js';
|
||||
import { TROUBLESHOOTING_CONFIG_UPGRADE_FAILURE_URL } from '../../../const.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import { CardIssueManagerAPI } from '../../types';
|
||||
import { Issue, IssueDescription } from '../types';
|
||||
|
||||
// Raised when the configuration upgrade could not faithfully upgrade part of
|
||||
// the config.
|
||||
export class ConfigUpgradeFailureIssue implements Issue {
|
||||
public readonly key = 'config_upgrade_failure' as const;
|
||||
|
||||
private _api: CardIssueManagerAPI;
|
||||
private _hasFailure = false;
|
||||
|
||||
constructor(api: CardIssueManagerAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public async detectStatic(): Promise<void> {
|
||||
this._hasFailure = hasConfigUpgradeFailures(
|
||||
this._api.getConfigManager().getRawConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
public hasIssue(): boolean {
|
||||
return this._hasFailure;
|
||||
}
|
||||
|
||||
public getIssue(): IssueDescription | null {
|
||||
if (!this._hasFailure) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium',
|
||||
notification: {
|
||||
heading: {
|
||||
text: localize('issues.config_upgrade_failure.heading'),
|
||||
icon: 'mdi:update',
|
||||
severity: 'medium',
|
||||
},
|
||||
body: { text: localize('issues.config_upgrade_failure.text') },
|
||||
link: {
|
||||
url: TROUBLESHOOTING_CONFIG_UPGRADE_FAILURE_URL,
|
||||
title: localize('issues.troubleshooting_guide'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { STATE_RUNNING } from 'home-assistant-js-websocket';
|
||||
import { ConditionState } from '../../../conditions/types.js';
|
||||
import { ConditionState } from '../../../condition-trigger/conditions/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import { Issue, IssueDescription } from '../types.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IssueTriggerContext } from 'issue';
|
||||
import { ConditionState } from '../../../conditions/types.js';
|
||||
import { ConditionState } from '../../../condition-trigger/conditions/types.js';
|
||||
import { Notification } from '../../../config/schema/actions/types.js';
|
||||
import { TROUBLESHOOTING_MEDIA_URL } from '../../../const.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IssueTriggerContext } from 'issue';
|
||||
import { summarizeNotification } from '../../components-lib/notification/summarize';
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { Notification } from '../../config/schema/actions/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { isTruthy } from '../../utils/basic';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IssueTriggerContext } from 'issue';
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { Notification } from '../../config/schema/actions/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Severity } from '../../severity';
|
||||
@@ -7,6 +7,7 @@ import { Severity } from '../../severity';
|
||||
export type IssueKey =
|
||||
| 'config_error'
|
||||
| 'config_upgrade'
|
||||
| 'config_upgrade_failure'
|
||||
| 'connection'
|
||||
| 'initialization'
|
||||
| 'legacy_resource'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConditionStateChange } from '../conditions/types';
|
||||
import { ConditionStateChange } from '../condition-trigger/conditions/types';
|
||||
import { PIPElement } from '../types';
|
||||
import { CardPIPAPI } from './types';
|
||||
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
|
||||
import { ConditionState, ConditionsTriggerData } from '../../conditions/types';
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { TriggerData } from '../../condition-trigger/triggers/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
|
||||
interface TemplateMediaData {
|
||||
title: string;
|
||||
is_folder: boolean;
|
||||
}
|
||||
interface TemplateContextInternal {
|
||||
camera?: string;
|
||||
view?: string;
|
||||
trigger?: ConditionsTriggerData;
|
||||
media?: TemplateMediaData;
|
||||
}
|
||||
import { isRecord } from '../../utils/basic';
|
||||
import { TemplateACCNamespace, TemplateMediaData } from './types';
|
||||
|
||||
interface TemplateContext {
|
||||
advanced_camera_card: TemplateContextInternal;
|
||||
acc: TemplateACCNamespace;
|
||||
|
||||
// Convenient alias.
|
||||
acc: TemplateContextInternal;
|
||||
// The HA-native top-level `trigger`, set only when a trigger fired.
|
||||
trigger?: TriggerData;
|
||||
}
|
||||
|
||||
interface TemplateRenderOptions {
|
||||
conditionState?: ConditionState;
|
||||
triggerData?: ConditionsTriggerData;
|
||||
triggerData?: TriggerData;
|
||||
mediaData?: TemplateMediaData;
|
||||
}
|
||||
|
||||
@@ -45,22 +37,23 @@ export class TemplateRenderer {
|
||||
if (
|
||||
!options?.conditionState?.camera &&
|
||||
!options?.conditionState?.view &&
|
||||
!options?.conditionState?.config &&
|
||||
!options?.triggerData &&
|
||||
!options?.mediaData
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const advancedCameraCardContext: TemplateContextInternal = {
|
||||
const acc: TemplateACCNamespace = {
|
||||
...(options?.conditionState?.camera && { camera: options.conditionState.camera }),
|
||||
...(options?.conditionState?.view && { view: options.conditionState.view }),
|
||||
...(options?.triggerData && { trigger: options.triggerData }),
|
||||
...(options?.conditionState?.config && { config: options.conditionState.config }),
|
||||
...(options?.mediaData && { media: options.mediaData }),
|
||||
};
|
||||
|
||||
return {
|
||||
acc: advancedCameraCardContext,
|
||||
advanced_camera_card: advancedCameraCardContext,
|
||||
acc,
|
||||
...(options?.triggerData && { trigger: options.triggerData }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,7 +74,7 @@ export class TemplateRenderer {
|
||||
return data.map((item) =>
|
||||
this._renderTemplateRecursively(hass, item, templateContext),
|
||||
);
|
||||
} else if (typeof data === 'object' && data !== null) {
|
||||
} else if (isRecord(data)) {
|
||||
const result = {};
|
||||
for (const key in data) {
|
||||
result[key] = this._renderTemplateRecursively(hass, data[key], templateContext);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AdvancedCameraCardConfig } from '../../config/schema/types';
|
||||
|
||||
// The card state exposed via the `acc` namespace AND as the
|
||||
// `trigger.from_acc`/`to_acc` before/after snapshots (the card analogue of HA's
|
||||
// full `trigger.from_state`/`to_state`).
|
||||
export interface TemplateAdvancedCameraCardState {
|
||||
camera?: string;
|
||||
view?: string;
|
||||
config?: AdvancedCameraCardConfig;
|
||||
}
|
||||
|
||||
export interface TemplateMediaData {
|
||||
title: string;
|
||||
is_folder: boolean;
|
||||
}
|
||||
|
||||
// The ambient `acc` namespace: card state plus `media` (the item currently being
|
||||
// templated, e.g. a folder-match candidate). `media` is not card state, so it is
|
||||
// not part of the shared snapshot type above.
|
||||
export interface TemplateACCNamespace extends TemplateAdvancedCameraCardState {
|
||||
media?: TemplateMediaData;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CameraManager } from '../camera-manager/manager';
|
||||
import type { ConditionStateManager } from '../conditions/state-manager';
|
||||
import type { ConditionStateManager } from '../condition-trigger/conditions/state-manager';
|
||||
import type { Automation } from '../config/schema/automations';
|
||||
import type { DeviceRegistryManager } from '../ha/registry/device';
|
||||
import type { EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
@@ -8,6 +8,7 @@ import type { EffectsManagerInterface } from '../types';
|
||||
import type { ActionsManager } from './actions/actions-manager';
|
||||
import type { AutomationsManager } from './automations-manager';
|
||||
import type { CallManager } from './call/manager';
|
||||
import type { CameraTriggersManager } from './camera-triggers-manager';
|
||||
import type { CameraURLManager } from './camera-url-manager';
|
||||
import type { CardElementManager } from './card-element-manager';
|
||||
import type { ConfigManager } from './config/config-manager';
|
||||
@@ -29,7 +30,6 @@ import type { PIPManager } from './pip-manager';
|
||||
import type { QueryStringManager } from './query-string-manager';
|
||||
import type { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import type { StyleManager } from './style-manager';
|
||||
import type { TriggersManager } from './triggers-manager';
|
||||
import type { ViewItemManager } from './view/item-manager';
|
||||
import type { ViewManager } from './view/view-manager';
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface CardActionsAPI {
|
||||
getPIPManager(): PIPManager;
|
||||
getIssueManager(): IssueManager;
|
||||
getStatusBarItemManager(): StatusBarItemManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
getViewItemManager(): ViewItemManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export interface CardCameraAPI {
|
||||
getEntityRegistryManager(): EntityRegistryManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getResolvedMediaCache(): ResolvedMediaCache;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
}
|
||||
|
||||
export interface CardCameraURLAPI {
|
||||
@@ -139,7 +139,7 @@ export interface CardDefaultManagerAPI {
|
||||
getConfigManager(): ConfigManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getInteractionManager(): InteractionManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export interface CardElementAPI {
|
||||
getPIPManager(): PIPManager;
|
||||
getIssueManager(): IssueManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ export interface CardHASSAPI {
|
||||
getInteractionManager(): InteractionManager;
|
||||
getMediaPlayerManager(): MediaPlayerManager;
|
||||
getStyleManager(): StyleManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ export interface CardInitializerAPI {
|
||||
getIssueManager(): IssueManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
getResolvedMediaCache(): ResolvedMediaCache;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ export interface CardInteractionAPI {
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getStyleManager(): StyleManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ export interface CardStyleAPI {
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
export interface CardTriggersAPI {
|
||||
export interface CardCameraTriggersAPI {
|
||||
getCallManager(): CallManager;
|
||||
getCameraManager(): CameraManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
@@ -339,7 +339,7 @@ export interface CardViewAPI {
|
||||
getIssueManager(): IssueManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
getStyleManager(): StyleManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getCameraTriggersManager(): CameraTriggersManager;
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import './components/status-bar';
|
||||
import './components/thumbnail-carousel.js';
|
||||
import './components/views.js';
|
||||
import { AdvancedCameraCardViews } from './components/views.js';
|
||||
import { ConditionStateManagerGetEvent } from './conditions/state-manager-via-event.js';
|
||||
import { ConditionStateManagerGetEvent } from './condition-trigger/conditions/state-manager-via-event.js';
|
||||
import { StatusBarItem } from './config/schema/actions/types.js';
|
||||
import { MenuItem } from './config/schema/elements/custom/menu/types.js';
|
||||
import { AdvancedCameraCardConfig } from './config/schema/types.js';
|
||||
@@ -457,7 +457,7 @@ class AdvancedCameraCard extends LitElement {
|
||||
.locked=${this._controller.getLockManager().isLocked()}
|
||||
.conditionStateManager=${this._controller.getConditionStateManager()}
|
||||
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
||||
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
||||
? this._controller.getCameraTriggersManager().getTriggeredCameraIDs()
|
||||
: undefined}
|
||||
.deviceRegistryManager=${this._controller.getDeviceRegistryManager()}
|
||||
.issues=${this._controller
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MENU_PRIORITY_MAX } from '../config/schema/common/const.js';
|
||||
import type { MenuItem } from '../config/schema/elements/custom/menu/types.js';
|
||||
import type { MenuConfig } from '../config/schema/menu.js';
|
||||
import type { Interaction } from '../types.js';
|
||||
import { getActionConfigGivenAction } from '../utils/action';
|
||||
import { getActionConfigGivenAction, isStandardAction } from '../utils/action';
|
||||
import { arrayify, isTruthy } from '../utils/basic.js';
|
||||
import { AutoHideState, isAutoHidden as evaluateAutoHidden } from './auto-hide.js';
|
||||
|
||||
@@ -225,6 +225,7 @@ export class MenuController {
|
||||
|
||||
private _isMenuToggleAction(action: ActionConfig): boolean {
|
||||
return (
|
||||
isStandardAction(action) &&
|
||||
action.action === 'fire-dom-event' &&
|
||||
action.advanced_camera_card_action === 'menu_toggle'
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { FoldersManager } from '../../card-controller/folders/manager';
|
||||
import { ViewItemManager } from '../../card-controller/view/item-manager';
|
||||
import { MergeContextViewModifier } from '../../card-controller/view/modifiers/merge-context';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../conditions/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../condition-trigger/conditions/types';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||
import { ThumbnailsControlBaseConfig } from '../../config/schema/common/controls/thumbnails';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { RecordingSegment } from '../../camera-manager/types';
|
||||
import { capEndDate } from '../../camera-manager/utils/cap-end-date';
|
||||
import { convertRangeToCacheFriendlyTimes } from '../../camera-manager/utils/range-to-cache-friendly';
|
||||
import { FoldersManager } from '../../card-controller/folders/manager';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../conditions/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../condition-trigger/conditions/types';
|
||||
import { FolderConfig } from '../../config/schema/folders';
|
||||
import { errorToConsole } from '../../utils/basic.js';
|
||||
import { ViewItem, ViewMedia } from '../../view/item';
|
||||
|
||||
@@ -10,9 +10,9 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { IssueTriggerEventData } from '../card-controller/issues/types.js';
|
||||
import { TemplateRenderer } from '../card-controller/templates/index.js';
|
||||
import { ConditionsManager } from '../conditions/conditions-manager.js';
|
||||
import { getConditionStateManagerViaEvent } from '../conditions/state-manager-via-event.js';
|
||||
import { ConditionStateManager } from '../conditions/state-manager.js';
|
||||
import { ConditionsManager } from '../condition-trigger/conditions/conditions-manager.js';
|
||||
import { getConditionStateManagerViaEvent } from '../condition-trigger/conditions/state-manager-via-event.js';
|
||||
import { ConditionStateManager } from '../condition-trigger/conditions/state-manager.js';
|
||||
import {
|
||||
StatusBarIcon,
|
||||
StatusBarImage,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
navigateToMedia,
|
||||
navigateUp,
|
||||
} from '../../components-lib/navigation.js';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../conditions/types.js';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../../condition-trigger/conditions/types.js';
|
||||
import { MediaGalleryConfig } from '../../config/schema/media-gallery.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../../const.js';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { FoldersManager } from '../card-controller/folders/manager.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../conditions/types.js';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../condition-trigger/conditions/types.js';
|
||||
import { ThumbnailsControlConfig } from '../config/schema/common/controls/thumbnails.js';
|
||||
import { MiniTimelineControlConfig } from '../config/schema/common/controls/timeline.js';
|
||||
import { CardWideConfig } from '../config/schema/types.js';
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ThumbnailDataRequestEvent,
|
||||
TimelineItemClickAction,
|
||||
} from '../components-lib/timeline/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../conditions/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../condition-trigger/conditions/types';
|
||||
import { ThumbnailsControlBaseConfig } from '../config/schema/common/controls/thumbnails';
|
||||
import { TimelineCoreConfig } from '../config/schema/common/controls/timeline';
|
||||
import { CardWideConfig } from '../config/schema/types';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CameraManager } from '../camera-manager/manager';
|
||||
import { FoldersManager } from '../card-controller/folders/manager';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../conditions/types';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../condition-trigger/conditions/types';
|
||||
import { TimelineConfig } from '../config/schema/timeline';
|
||||
import { CardWideConfig } from '../config/schema/types';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
|
||||
@@ -16,7 +16,7 @@ import { MicrophoneState } from '../card-controller/types.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { CallSession } from '../card-controller/call/types.js';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../conditions/types.js';
|
||||
import { ConditionStateManagerReadonlyInterface } from '../condition-trigger/conditions/types.js';
|
||||
import { AdvancedCameraCardConfig, CardWideConfig } from '../config/schema/types.js';
|
||||
import { RawAdvancedCameraCardConfig } from '../config/types.js';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device/index.js';
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# `condition-trigger`
|
||||
|
||||
The runtime behind `automations:`, `overrides:` and conditional
|
||||
picture-`elements:`, built to mirror Home Assistant's conditions and triggers.
|
||||
User-facing reference: [`conditions-triggers.md`](../../docs/configuration/conditions-triggers.md).
|
||||
|
||||
## Condition vs trigger
|
||||
|
||||
The same type (`state`, `camera`, ...) exists as both, but they are opposite shapes:
|
||||
|
||||
| | Condition | Trigger |
|
||||
| ------- | ------------------------------------------------- | -------------------------------------- |
|
||||
| Asks | _"is this true right now?"_ | _"did this just become true?"_ |
|
||||
| Is a | level / predicate, pulled via `evaluate()` | edge / event, pushed via `subscribe()` |
|
||||
| Used in | `automations` `conditions`, `overrides`, elements | `automations` `triggers` only |
|
||||
|
||||
Both read one source of truth, the **`ConditionStateManager`** -- the card's
|
||||
live state (`camera`/`view`/`config`/`hass`/...), which notifies on change (the
|
||||
lone exception is `screen`, which watches a `window.matchMedia` query). The Zod
|
||||
schema
|
||||
([`config/schema/condition-trigger/`](../config/schema/condition-trigger/))
|
||||
shares each type's fields between its condition and trigger (`common/`) so the
|
||||
two cannot drift.
|
||||
|
||||
## Conditions
|
||||
|
||||
Each type has a pure level-predicate evaluator (`evaluate(state) -> result`),
|
||||
built by `createConditionEvaluator`. An evaluator may also declare
|
||||
`externalSources` -- change sources outside `ConditionState` (currently only
|
||||
`screen`'s `matchMedia`, via a `MediaQueryWatcher`) -- which `ConditionsManager`
|
||||
subscribes to so a change there triggers a re-evaluation. The manager ANDs a set
|
||||
of evaluators and notifies when the combined result flips; it backs
|
||||
`overrides`/`elements` and the automation ongoing-`conditions` pull (below).
|
||||
|
||||
## The bridge
|
||||
|
||||
A type's meaning lives in exactly one place -- its condition evaluator. A
|
||||
card-state trigger reuses that same evaluator as a point-in-time value-filter,
|
||||
built directly from the trigger by `createConditionEvaluatorForTrigger` (the
|
||||
trigger and condition schemas share a `common/` base, so no discriminator-swap
|
||||
or cast). One definition of meaning, two readings: the condition asks the
|
||||
predicate, the trigger watches for change and filters it through the predicate.
|
||||
|
||||
## Triggers: four kinds
|
||||
|
||||
`createTriggerEvaluator` picks one. Each emits a `TriggerData` payload (the
|
||||
`trigger.*` template variable); stock triggers report their HA `platform`, card
|
||||
triggers report `platform: acc` + the kind in `type`.
|
||||
|
||||
1. **Stock entity** (`state`, `numeric_state`) -- `EntityStateTriggerBase`:
|
||||
per-`entity_id` fan-out, HA `from_state`/`to_state`, `for:` via a `Timer`.
|
||||
2. **Stock template** (`template`) -- the non-true -> true edge of `value_template`.
|
||||
3. **Screen** (`screen`) -- `ScreenTrigger`: watches a `matchMedia` query, whose
|
||||
state lives outside `ConditionState`, so it owns a shared `MediaQueryWatcher`
|
||||
(the same watcher the screen condition uses) and fires on the rising edge of
|
||||
the match.
|
||||
4. **Card-state** (every other type: `camera`, `view`, `config`, `fullscreen`,
|
||||
...) -- `ConditionStateTriggerBase`: subscribe to the
|
||||
`ConditionStateManager`, fire when the watched field (`_getValue`) changes,
|
||||
and emit `from_acc`/`to_acc` snapshots. A value filters the change through
|
||||
the matching condition (the bridge); no value fires on any change; `config`
|
||||
is trigger-only (no matching condition).
|
||||
|
||||
## Automations: push, then pull
|
||||
|
||||
`AutomationsManager` runs a `TriggersManager` per automation and, when a trigger
|
||||
pushes, **pull-evaluates** the ongoing `conditions` against the current state
|
||||
(this matches Home Assistant actions). The state store updates _before_
|
||||
dispatching, so a pulled condition already sees the triggering change -- no
|
||||
ordering needed between the two.
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TemplateRenderer } from '../../card-controller/templates';
|
||||
import { ConditionState } from '../conditions/types';
|
||||
|
||||
// The shared `enabled` gate for triggers and conditions (equivalent to HA's
|
||||
// `vol.Any(boolean, template)`): a boolean, or a template rendered against the
|
||||
// current state. Returns whether the trigger/condition is active.
|
||||
//
|
||||
// `enabledWithoutHass` is the fallback when a template `enabled` cannot be
|
||||
// rendered (no hass yet, e.g. at startup). It differs by caller because
|
||||
// "disabled" has opposite consequences: a disabled *trigger* simply does not
|
||||
// fire (so triggers fail closed -- pass `false`), whereas a disabled
|
||||
// *condition* is skipped (so the condition may evaluate to `true`)
|
||||
export const isEnabled = (
|
||||
templateRenderer: TemplateRenderer,
|
||||
enabled?: boolean | string,
|
||||
state?: ConditionState,
|
||||
enabledWithoutHass = true,
|
||||
): boolean => {
|
||||
if (enabled === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (typeof enabled === 'boolean') {
|
||||
return enabled;
|
||||
}
|
||||
if (!state?.hass) {
|
||||
return enabledWithoutHass;
|
||||
}
|
||||
return (
|
||||
templateRenderer.renderRecursively(state.hass, enabled, {
|
||||
conditionState: state,
|
||||
}) === true
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export type MediaQueryWatcherUnsubscribeCallback = () => void;
|
||||
|
||||
// Watches a single CSS media query via `window.matchMedia`. The shared change
|
||||
// source for the `screen` condition and trigger, whose match state lives
|
||||
// outside the card's `ConditionState`.
|
||||
export class MediaQueryWatcher {
|
||||
private _query: string;
|
||||
private _mediaQuery: MediaQueryList | null = null;
|
||||
private _callback: (() => void) | null = null;
|
||||
|
||||
constructor(query: string) {
|
||||
this._query = query;
|
||||
}
|
||||
|
||||
public matches(): boolean {
|
||||
return window.matchMedia(this._query).matches;
|
||||
}
|
||||
|
||||
public subscribe(callback: () => void): MediaQueryWatcherUnsubscribeCallback {
|
||||
this._callback = callback;
|
||||
this._mediaQuery = window.matchMedia(this._query);
|
||||
this._mediaQuery.addEventListener('change', this._handler);
|
||||
|
||||
return (): void => {
|
||||
this._mediaQuery?.removeEventListener('change', this._handler);
|
||||
this._mediaQuery = null;
|
||||
this._callback = null;
|
||||
};
|
||||
}
|
||||
|
||||
private _handler = (): void => this._callback?.();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { TemplateRenderer } from '../../card-controller/templates';
|
||||
import { NumericStateBase } from '../../config/schema/condition-trigger/common/numeric-state';
|
||||
import { ConditionState } from '../conditions/types';
|
||||
|
||||
// The numeric value of `entityID` to compare: the rendered `value_template`,
|
||||
// else the `attribute`, else the state. Returns null when the entity is absent
|
||||
// or the value is non-numeric (the cases where HA raises a ConditionError).
|
||||
export const readNumericStateValue = (
|
||||
entityID: string,
|
||||
state: ConditionState,
|
||||
config: NumericStateBase,
|
||||
templateRenderer: TemplateRenderer,
|
||||
): number | null => {
|
||||
const hass = state.hass;
|
||||
if (!hass) {
|
||||
return null;
|
||||
}
|
||||
const stateObj = hass.states?.[entityID];
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rawValue: unknown;
|
||||
if (config.value_template) {
|
||||
rawValue = templateRenderer.renderRecursively(hass, config.value_template, {
|
||||
conditionState: state,
|
||||
});
|
||||
} else if (config.attribute !== undefined) {
|
||||
rawValue = stateObj.attributes?.[config.attribute];
|
||||
} else {
|
||||
rawValue = stateObj.state;
|
||||
}
|
||||
|
||||
const value = Number(rawValue);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
};
|
||||
|
||||
// Whether `entityID`'s numeric value currently satisfies the `above`/`below`
|
||||
// thresholds. A threshold is a number, or an entity id whose state supplies it;
|
||||
// an unspecified threshold imposes no constraint and an unresolvable one fails.
|
||||
// Shared by the numeric_state condition and trigger, which match identically.
|
||||
export const matchesNumericState = (
|
||||
entityID: string,
|
||||
state: ConditionState,
|
||||
config: NumericStateBase,
|
||||
templateRenderer: TemplateRenderer,
|
||||
): boolean => {
|
||||
const hass = state.hass;
|
||||
if (!hass) {
|
||||
return false;
|
||||
}
|
||||
const value = readNumericStateValue(entityID, state, config, templateRenderer);
|
||||
if (value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkBound = (
|
||||
compare: (value: number, bound: number) => boolean,
|
||||
threshold?: number | string,
|
||||
): boolean => {
|
||||
if (threshold === undefined) {
|
||||
return true;
|
||||
}
|
||||
const bound =
|
||||
typeof threshold === 'number'
|
||||
? threshold
|
||||
: Number(hass.states?.[threshold]?.state);
|
||||
return Number.isFinite(bound) && compare(value, bound);
|
||||
};
|
||||
|
||||
return (
|
||||
checkBound((v, bound) => v > bound, config.above) &&
|
||||
checkBound((v, bound) => v < bound, config.below)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { TemplateRenderer } from '../../card-controller/templates';
|
||||
import { TimePeriod } from '../../config/schema/common/time-period';
|
||||
import { isRecord } from '../../utils/basic';
|
||||
import { ConditionState } from '../conditions/types';
|
||||
|
||||
// Parses a Home Assistant time-period value (a condition/trigger `for:`) to
|
||||
// seconds, matching HA's `cv.time_period`:
|
||||
// - a number, or a bare numeric string, is a count of seconds;
|
||||
// - a colon string is `HH:MM` or `HH:MM:SS` — HA reads TWO parts as
|
||||
// hours:minutes (not minutes:seconds);
|
||||
// - a `{days, hours, minutes, seconds, milliseconds}` dict (each field a
|
||||
// number or a numeric string, e.g. once a template field has been rendered).
|
||||
// Accepts `unknown` so a freshly-rendered value can be parsed directly; returns
|
||||
// null when unparseable or negative (HA `for:` requires a positive period).
|
||||
const parseTimePeriodToSeconds = (value: unknown): number | null => {
|
||||
const num = (field: unknown): number => Number(field ?? 0);
|
||||
let seconds: number;
|
||||
if (typeof value === 'number') {
|
||||
seconds = value;
|
||||
} else if (typeof value === 'string') {
|
||||
if (value.includes(':')) {
|
||||
const parts = value.split(':');
|
||||
if (parts.length < 2 || parts.length > 3 || parts.some((part) => !part.trim())) {
|
||||
return null;
|
||||
}
|
||||
const [hours, minutes, secs = 0] = parts.map(Number);
|
||||
seconds = hours * 3600 + minutes * 60 + secs;
|
||||
} else if (!value.trim()) {
|
||||
return null;
|
||||
} else {
|
||||
seconds = Number(value);
|
||||
}
|
||||
} else if (isRecord(value)) {
|
||||
seconds =
|
||||
num(value.days) * 86400 +
|
||||
num(value.hours) * 3600 +
|
||||
num(value.minutes) * 60 +
|
||||
num(value.seconds) +
|
||||
num(value.milliseconds) / 1000;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
||||
};
|
||||
|
||||
// Renders any templates within a `for:` time period (HA's
|
||||
// `cv.positive_time_period_template`) against the current state, then parses it
|
||||
// to seconds. The whole value or any dict field may be a template; a
|
||||
// template-free value renders to itself. Without `hass` the value cannot be
|
||||
// rendered, so it is parsed as-is (a literal duration still works; a template
|
||||
// yields null).
|
||||
export const renderTimePeriodToSeconds = (
|
||||
templateRenderer: TemplateRenderer,
|
||||
value: TimePeriod,
|
||||
conditionState?: ConditionState,
|
||||
): number | null => {
|
||||
if (!conditionState?.hass) {
|
||||
return parseTimePeriodToSeconds(value);
|
||||
}
|
||||
return parseTimePeriodToSeconds(
|
||||
templateRenderer.renderRecursively(conditionState.hass, value, { conditionState }),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { TemplateRenderer } from '../../card-controller/templates';
|
||||
import { Condition } from '../../config/schema/condition-trigger/conditions/types';
|
||||
import { isEnabled } from '../common/is-enabled';
|
||||
import {
|
||||
ConditionEvaluator,
|
||||
ExternalInvalidationUnsubscribeCallback,
|
||||
} from './conditions/types';
|
||||
import { createConditionEvaluator } from './factory';
|
||||
import {
|
||||
ConditionsEvaluationResult,
|
||||
ConditionsListener,
|
||||
ConditionsManagerReadonlyInterface,
|
||||
ConditionStateChange,
|
||||
ConditionStateManagerReadonlyInterface,
|
||||
} from './types';
|
||||
|
||||
// A condition evaluator paired with its config, so `enabled` can be re-checked
|
||||
// against the config each time conditions are evaluated.
|
||||
interface ManagedCondition {
|
||||
config: Condition;
|
||||
evaluator: ConditionEvaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to evaluate an array of conditions, and notify listeners when the
|
||||
* evaluation result changes.
|
||||
*/
|
||||
export class ConditionsManager implements ConditionsManagerReadonlyInterface {
|
||||
private _stateManager: ConditionStateManagerReadonlyInterface | null;
|
||||
private _templateRenderer = new TemplateRenderer();
|
||||
private _conditions: ManagedCondition[];
|
||||
|
||||
private _listeners: ConditionsListener[] = [];
|
||||
private _evaluation: ConditionsEvaluationResult = { result: false };
|
||||
private _unsubscribeCallbacks: ExternalInvalidationUnsubscribeCallback[] = [];
|
||||
|
||||
constructor(
|
||||
conditions: Condition[],
|
||||
stateManager?: ConditionStateManagerReadonlyInterface | null,
|
||||
) {
|
||||
const context = { templateRenderer: this._templateRenderer };
|
||||
this._conditions = conditions.map((config) => ({
|
||||
config,
|
||||
evaluator: createConditionEvaluator(config, context),
|
||||
}));
|
||||
|
||||
this._stateManager = stateManager ?? null;
|
||||
|
||||
// Subscribe to evaluators' external invalidation sources, including those
|
||||
// nested inside composites, so a change there triggers a re-evaluation
|
||||
// (this is not necessary for most conditions since they are purely based on
|
||||
// ConditionState, but there are exceptions, e.g. screen).
|
||||
this._conditions.forEach(({ evaluator }) =>
|
||||
(evaluator.externalSources ?? []).forEach((source) =>
|
||||
this._unsubscribeCallbacks.push(source.subscribe(() => this._evaluate())),
|
||||
),
|
||||
);
|
||||
|
||||
// Do an initial condition evaluation, but without calling listeners.
|
||||
this._evaluate({ callListeners: false });
|
||||
|
||||
this._stateManager?.addListener(this._stateManagerHandler);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this._stateManager?.removeListener(this._stateManagerHandler);
|
||||
|
||||
this._listeners.forEach((l) => this.removeListener(l));
|
||||
|
||||
this._unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
|
||||
this._unsubscribeCallbacks = [];
|
||||
this._conditions = [];
|
||||
}
|
||||
|
||||
public addListener(listener: ConditionsListener): void {
|
||||
if (!this._listeners.includes(listener)) {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public removeListener(listener: ConditionsListener): void {
|
||||
this._listeners = this._listeners.filter((l) => l !== listener);
|
||||
}
|
||||
|
||||
public getEvaluation(): ConditionsEvaluationResult {
|
||||
return this._evaluation;
|
||||
}
|
||||
|
||||
private _stateManagerHandler = (stateChange: ConditionStateChange): void => {
|
||||
this._evaluate({ stateChange });
|
||||
};
|
||||
|
||||
private _evaluate(options?: {
|
||||
stateChange?: ConditionStateChange;
|
||||
callListeners?: boolean;
|
||||
}): void {
|
||||
const state = options?.stateChange?.new ?? this._stateManager?.getState();
|
||||
|
||||
let result = true;
|
||||
|
||||
for (const { config, evaluator } of this._conditions) {
|
||||
if (!isEnabled(this._templateRenderer, config.enabled, state)) {
|
||||
continue;
|
||||
}
|
||||
if (!evaluator.evaluate(state, options?.stateChange?.old).result) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const evaluation: ConditionsEvaluationResult = { result };
|
||||
|
||||
if (result !== this._evaluation.result) {
|
||||
this._evaluation = evaluation;
|
||||
if (options?.callListeners ?? true) {
|
||||
this._listeners.forEach((listener) =>
|
||||
listener(this._evaluation, options?.stateChange),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||
import { CompositeConditionEvaluator } from './composite';
|
||||
|
||||
export class AndConditionEvaluator extends CompositeConditionEvaluator {
|
||||
public evaluate(
|
||||
newState?: ConditionState,
|
||||
oldState?: ConditionState,
|
||||
): ConditionsEvaluationResult {
|
||||
for (const child of this._children) {
|
||||
if (!child.evaluate(newState, oldState).result) {
|
||||
return { result: false };
|
||||
}
|
||||
}
|
||||
return { result: true };
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
import { CallBase } from '../../../config/schema/condition-trigger/common/call';
|
||||
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||
import { ConditionEvaluator, ConditionOfType } from './types';
|
||||
import { ConditionEvaluator } from './types';
|
||||
|
||||
export class CallConditionEvaluator implements ConditionEvaluator {
|
||||
private _condition: ConditionOfType<'call'>;
|
||||
private _condition: CallBase;
|
||||
|
||||
constructor(condition: ConditionOfType<'call'>) {
|
||||
constructor(condition: CallBase) {
|
||||
this._condition = condition;
|
||||
}
|
||||
|
||||
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||
return {
|
||||
result: (this._condition.call ?? true) === (newState?.call ?? false),
|
||||
result: this._condition.call === (newState?.call ?? false),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CameraBase } from '../../../config/schema/condition-trigger/common/camera';
|
||||
import { ConditionsEvaluationResult, ConditionState } from '../types';
|
||||
import { ConditionEvaluator } from './types';
|
||||
|
||||
export class CameraConditionEvaluator implements ConditionEvaluator {
|
||||
private _condition: CameraBase;
|
||||
|
||||
constructor(condition: CameraBase) {
|
||||
this._condition = condition;
|
||||
}
|
||||
|
||||
public evaluate(newState?: ConditionState): ConditionsEvaluationResult {
|
||||
const camera = newState?.camera;
|
||||
const cameras = this._condition.cameras;
|
||||
if (cameras === undefined) {
|
||||
// Omitted: a camera is selected.
|
||||
return { result: !!camera };
|
||||
}
|
||||
if (cameras.length === 0) {
|
||||
// `[]`: no camera is selected.
|
||||
return { result: !camera };
|
||||
}
|
||||
// A list: the selected camera is one of these.
|
||||
return { result: !!camera && cameras.includes(camera) };
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user