Skip to content

Commit

Permalink
refactor: Significantly refactor how conditions work internally (#1886)
Browse files Browse the repository at this point in the history
- Much improved API cleanliness and testability to allow further
extensibility in future
 - Simplified code in `live` view

This technically contains a small change in how overrides work in the
`live` view. Since that change is _closer_ to the documentation, and
since this is likely to be rarely used, this is not considered a
breaking change. Previously, overrides for a given live camera would
always render _as if_ that camera was selected, vs was actually
selected. Now, overrides will only apply in the live view when the
camera is _actually_ selected. If this is an issue for you in practice,
lets discuss.
  • Loading branch information
dermotduffy authored Feb 12, 2025
1 parent a92074b commit 8d3cf07
Show file tree
Hide file tree
Showing 51 changed files with 2,441 additions and 2,116 deletions.
31 changes: 22 additions & 9 deletions docs/configuration/conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ certain configurations (in `overrides`) or to display "picture elements" (in
## `camera`

Matches based on the selected 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` | A list of camera IDs in which this condition is satisfied. See the camera [id](cameras/README.md) parameter. |
| 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. |

## `expand`

Expand Down Expand Up @@ -167,6 +168,18 @@ conditions:
# [...]
```

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

!> 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).

## `triggered`
Expand Down Expand Up @@ -226,10 +239,10 @@ conditions:
# [...]
```

| Parameter | Description |
| ----------- | ------------------------------------------------------------------------------------------------- |
| `condition` | Must be `view`. |
| `views` | A list of [views](view.md?id=supported-views) in which this condition is satified (e.g. `clips`). |
| 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. |

?> Internally, views associated with the media viewer (e.g. `clip`, `snapshot`,
`recording`) are translated to a special view called `media` after the relevant
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
4px 4px;
}
#__sidebar img {
max-width: 100px;
max-width: 48px;
}
</style>
</head>
Expand Down
47 changes: 26 additions & 21 deletions src/card-controller/automations-manager.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ConditionsManager } from '../conditions/conditions-manager.js';
import { ConditionsEvaluationResult } from '../conditions/types.js';
import { Automation, AutomationActions } from '../config/types.js';
import { localize } from '../localize/localize.js';
import { CardAutomationsAPI, TaggedAutomations } from './types.js';
import { CardAutomationsAPI, TaggedAutomation } from './types.js';

const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;

export class AutomationsManager {
protected _api: CardAutomationsAPI;

protected _automations: TaggedAutomations = [];
protected _priorEvaluations: Map<Automation, boolean> = new Map();
protected _automations = new Map<TaggedAutomation, ConditionsManager>();

// A counter to avoid infinite loops, increases every time actions are run,
// decreases every time actions are complete.
Expand All @@ -19,14 +20,28 @@ export class AutomationsManager {
}

public deleteAutomations(tag?: unknown) {
this._automations = this._automations.filter((automation) => automation.tag !== tag);
for (const [automation, conditionManager] of this._automations) {
if (automation.tag === tag) {
this._automations.delete(automation);
conditionManager.destroy();
}
}
}

public addAutomations(automations: TaggedAutomations): void {
this._automations.push(...automations);
public addAutomations(automations: TaggedAutomation[]): void {
for (const automation of automations) {
const conditionManager = new ConditionsManager(
automation.conditions,
this._api.getConditionStateManager(),
);
conditionManager.addListener((result: ConditionsEvaluationResult) =>
this._execute(automation, result),
);
this._automations.set(automation, conditionManager);
}
}

public execute(): void {
protected _execute(automation: Automation, result: ConditionsEvaluationResult): void {
if (
!this._api.getHASSManager().hasHASS() ||
// Never execute automations if the card hasn't finished initializing, as
Expand All @@ -40,20 +55,10 @@ export class AutomationsManager {
return;
}

const actionsToRun: AutomationActions = [];
for (const automation of this._automations) {
const shouldExecute = this._api
.getConditionsManager()
.evaluateConditions(automation.conditions);
const actions = shouldExecute ? automation.actions : automation.actions_not;
const priorEvaluation = this._priorEvaluations.get(automation);
this._priorEvaluations.set(automation, shouldExecute);
if (shouldExecute !== priorEvaluation && actions) {
actionsToRun.push(...actions);
}
}
const shouldExecute = result.result;
const actions = shouldExecute ? automation.actions : automation.actions_not;

if (!actionsToRun.length) {
if (!actions?.length) {
return;
}

Expand All @@ -70,6 +75,6 @@ export class AutomationsManager {
await this._api.getActionsManager().executeActions(actions);
--this._nestedAutomationExecutions;
};
runActions(actionsToRun);
runActions(actions);
}
}
2 changes: 1 addition & 1 deletion src/card-controller/card-element-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class CardElementManager {
this._api.getQueryStringManager().requestExecution,
);

this._api.getConditionsManager()?.setState({
this._api.getConditionStateManager()?.setState({
userAgent: navigator.userAgent,
});

Expand Down
Loading

0 comments on commit 8d3cf07

Please sign in to comment.