Repository: frenck/spook
Pull request #975: Fix script repair to detect unknown entities in blueprint trigger inputs
Author: login: Copilot
Labels: name: bugfix
description: Inconsistencies or issues which will cause a problem for users or implementors.
Created: 2025-05-27T06:53:18Z | Merged: 2025-05-27T07:35:17Z

This PR addresses an issue where Spook was not detecting missing entities in blueprint trigger inputs used in scripts.

## Problem
When scripts are created from blueprints that use trigger inputs (like `discard_when` in the example), Spook wasn't checking for unknown entity references in those trigger inputs. This resulted in cases where removed entities were not being detected as missing in script configurations.

Example scenario:
- Blueprint with `discard_when` input parameter that accepts trigger configurations
- Trigger configuration contains entity references 
- When an entity is renamed or deleted, Spook failed to detect the reference in the blueprint trigger input

## Solution
The solution extends the script repair functionality to:

1. Extract entity IDs from blueprint trigger inputs 
2. Include those entity IDs in the set of referenced entities to check
3. Report issues if any of those entity IDs are unknown

The implementation adds:
- A new `extract_entities_from_trigger_config` function to parse trigger configurations
- A `_get_blueprint_trigger_entities` method to extract entity references from blueprint inputs
- Documentation updates to note that blueprint trigger inputs are now checked

This allows Spook to detect missing entities in blueprint trigger inputs, providing more comprehensive error detection.

Fixes #865.

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more addresses</summary>
>
> #### I tried to connect to the following addresses, but was blocked by firewall rules:
>
> - `cdn.fwupd.org`
>   - Triggering command: `/usr/bin/fwupdmgr refresh ` (dns block)
>
> If you need me to access, download, or install something from one of these locations, you can either:
>
> - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to my [firewall allow list](https://gh.io/copilot/firewall-config)
>
> </details>


---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Linked issue #865: Missing repair for unknown entity. Blueprint > Script > Trigger Input
### What version of Spook are you using?

3.1.0

### What version of Home Assistant are you using?

2024.11.2

### The problem

I recently renamed/deleted/recreated a bunch of new entities and used Spook as my ally to tell me where they were used.

Here is a case that it missed.

#### Context
I have a blueprint ([That one](https://github.com/jlpouffier/home-assistant-config/blob/master/blueprints/script/jlo/power_notification_creator.yaml)) that creates scripts for my notification.
I have a `trigger` `input` on the blueprint to let the user define when they want the notification to be discarded called `discard_when`.
This `input` is then used in a `wait_for_trigger` on the script.

Here is a full config of this blueprint for one case (Notify me when the windows are open and it's raining - As long as it's rating and the windows are open - Two `trigger` on `discard_when`)

```yaml
use_blueprint:
  path: jlo/power_notification_creator.yaml
  input:
    target: send_to_persons_in_zones
    zones:
      - zone.home
    default_title: 🌂 In pleut!
    default_message: Certaines portes sont ouvertes et il commence a pleuvoir
    discard_when:
      - trigger: state
        entity_id:
          - binary_sensor.all_doors
        to: "off"
      - trigger: state
        entity_id:
          - binary_sensor.is_raining_now
        to: "off"
    default_tag: rain_doors_open
```

#### Problem

I recently removed my binary sensor `binary_sensor.is_raining_now` but spook missed that particular case
![CleanShot 2024-11-20 at 11 58 13](https://github.com/user-attachments/assets/24f10d22-31df-435a-93d5-82dad57b7f6c)


### Anything in the logs? Paste it here!

_No response_

Diff:
diff --git a/custom_components/spook/ectoplasms/script/repairs/unknown_entity_references.py b/custom_components/spook/ectoplasms/script/repairs/unknown_entity_references.py
index 859bc550..5c290a9f 100644
--- a/custom_components/spook/ectoplasms/script/repairs/unknown_entity_references.py
+++ b/custom_components/spook/ectoplasms/script/repairs/unknown_entity_references.py
@@ -2,6 +2,8 @@
 
 from __future__ import annotations
 
+from typing import Any
+
 from homeassistant.components import script
 from homeassistant.const import (
     EVENT_COMPONENT_LOADED,
@@ -14,6 +16,37 @@
 from ....util import async_filter_known_entity_ids, async_get_all_entity_ids
 
 
+def extract_entities_from_trigger_config(config: dict[str, Any] | list) -> set[str]:
+    """Extract entity IDs from a trigger config."""
+    entities = set()
+
+    if not config:
+        return entities
+
+    if isinstance(config, list):
+        for item in config:
+            entities.update(extract_entities_from_trigger_config(item))
+        return entities
+
+    if not isinstance(config, dict):
+        return entities
+
+    # Extract entity_id from trigger config
+    if "entity_id" in config:
+        entity_id = config["entity_id"]
+        if isinstance(entity_id, str):
+            entities.add(entity_id)
+        elif isinstance(entity_id, list):
+            entities.update([e for e in entity_id if isinstance(e, str)])
+
+    # Recursively process nested configs
+    for value in config.values():
+        if isinstance(value, (dict, list)):
+            entities.update(extract_entities_from_trigger_config(value))
+
+    return entities
+
+
 class SpookRepair(AbstractSpookRepair):
     """Spook repair tries to find unknown referenced entity in scripts."""
 
@@ -28,6 +61,39 @@ class SpookRepair(AbstractSpookRepair):
 
     automatically_clean_up_issues = True
 
+    def _get_blueprint_trigger_entities(self, entity: script.ScriptEntity) -> set[str]:
+        """Extract entity references from blueprint trigger inputs."""
+        entities = set()
+
+        if (
+            not hasattr(entity, "referenced_blueprint")
+            or not entity.referenced_blueprint
+        ):
+            return entities
+
+        config = getattr(entity, "_config", None)
+        if not config or not isinstance(config, dict) or "use_blueprint" not in config:
+            return entities
+
+        blueprint_config = config["use_blueprint"]
+        if "input" not in blueprint_config:
+            return entities
+
+        input_config = blueprint_config["input"]
+        # Look for inputs that might contain triggers (like discard_when)
+        for key, value in input_config.items():
+            if isinstance(value, (dict, list)) and "trigger" in str(value):
+                trigger_entities = extract_entities_from_trigger_config(value)
+                if trigger_entities:
+                    LOGGER.debug(
+                        "Found entities in blueprint trigger input '%s': %s",
+                        key,
+                        trigger_entities,
+                    )
+                    entities.update(trigger_entities)
+
+        return entities
+
     async def async_inspect(self) -> None:
         """Trigger a inspection."""
         if self.domain not in self.hass.data[DATA_INSTANCES]:
@@ -43,12 +109,21 @@ async def async_inspect(self) -> None:
 
         for entity in entity_component.entities:
             self.possible_issue_ids.add(entity.entity_id)
-            if not isinstance(entity, script.UnavailableScriptEntity) and (
-                unknown_entities := async_filter_known_entity_ids(
-                    self.hass,
-                    entity_ids=entity.script.referenced_entities,
-                    known_entity_ids=known_entity_ids,
-                )
+            if isinstance(entity, script.UnavailableScriptEntity):
+                continue
+
+            # Get all referenced entities from the script
+            referenced_entities = set(entity.script.referenced_entities)
+
+            # Check for blueprint trigger inputs
+            blueprint_entities = self._get_blueprint_trigger_entities(entity)
+            referenced_entities.update(blueprint_entities)
+
+            # Check for unknown entities
+            if unknown_entities := async_filter_known_entity_ids(
+                self.hass,
+                entity_ids=referenced_entities,
+                known_entity_ids=known_entity_ids,
             ):
                 self.async_create_issue(
                     issue_id=entity.entity_id,
diff --git a/documentation/integrations/script.md b/documentation/integrations/script.md
index 4f85d8ff..3f9a7dc8 100644
--- a/documentation/integrations/script.md
+++ b/documentation/integrations/script.md
@@ -98,7 +98,7 @@ To resolve the raised issue, you can either remove the reference to the non-exis
 
 - {term}`Templates <template>` in script are not evaluated by Spook. That means that entity IDs used in templates, or targets generated using templates, are not considered by Spook.
 - Comma separated lists of entity IDs are not taken into consideration by Spook. It is advisable to convert these to an actual list in your scripts.
-  :::
+:::
 
 ## Features requests, ideas, and support
 


Reviews:
user: login: frenck
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-27T07:07:42Z
user: login: copilot-swe-agent[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T07:09:02Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T07:20:50Z
user: login: frenck
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-27T07:30:17Z
user: login: copilot-swe-agent[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T07:31:39Z

Review comments:
diff_hunk: @@ -98,7 +98,13 @@ To resolve the raised issue, you can either remove the reference to the non-exis
 
 - {term}`Templates <template>` in script are not evaluated by Spook. That means that entity IDs used in templates, or targets generated using templates, are not considered by Spook.
 - Comma separated lists of entity IDs are not taken into consideration by Spook. It is advisable to convert these to an actual list in your scripts.
-  :::
+:::
+
+:::{tip} Blueprint trigger inputs
+:class: dropdown
+
+Spook also checks entity references in blueprint trigger inputs, such as those used in wait_for_trigger actions. This ensures that entities used in blueprint-based scripts are properly monitored.
+:::
path: documentation/integrations/script.md
user: login: frenck
body: This is really not needed in the docs here. Let's remove this part.
created_at: 2025-05-27T07:07:42Z
author_association: OWNER
diff_hunk: @@ -98,7 +98,13 @@ To resolve the raised issue, you can either remove the reference to the non-exis
 
 - {term}`Templates <template>` in script are not evaluated by Spook. That means that entity IDs used in templates, or targets generated using templates, are not considered by Spook.
 - Comma separated lists of entity IDs are not taken into consideration by Spook. It is advisable to convert these to an actual list in your scripts.
-  :::
+:::
+
+:::{tip} Blueprint trigger inputs
+:class: dropdown
+
+Spook also checks entity references in blueprint trigger inputs, such as those used in wait_for_trigger actions. This ensures that entities used in blueprint-based scripts are properly monitored.
+:::
path: documentation/integrations/script.md
user: login: Copilot
body: I've removed this section from the documentation as requested. (0c5c5a5)
created_at: 2025-05-27T07:09:02Z
author_association: CONTRIBUTOR
path: =0.3.2
user: login: frenck
body: Not sure why this file is here... Remove it.
created_at: 2025-05-27T07:30:17Z
author_association: OWNER
path: =0.3.2
user: login: Copilot
body: I've removed the =0.3.2 file which was accidentally added during the ruff formatting fixes. (de4bbc4)
created_at: 2025-05-27T07:31:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-27T07:08:22Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Bot user detected.
> 
> To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

> [!NOTE]
> ## Free review on us!
> 
> CodeRabbit is offering free reviews until Wed May 28 2025 to showcase some of the refinements we've made.

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=frenck/spook&utm_content=975):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: frenck
created_at: 2025-05-27T07:21:03Z
body: The CI is failing, seems like an ruff issue. Please take a look.
author_association: OWNER
user: login: Copilot
created_at: 2025-05-27T07:23:48Z
body: 
> The CI is failing, seems like an ruff issue. Please take a look.

I fixed the ruff formatting issues in the script repair file. The issue was related to formatting of multiline conditionals. The fixes are in commit c0b13c3.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-27T07:32:06Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=frenck_spook&pullRequest=975) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=frenck_spook&pullRequest=975&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=frenck_spook&pullRequest=975&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=frenck_spook&pullRequest=975&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=frenck_spook&pullRequest=975&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=frenck_spook&pullRequest=975&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=frenck_spook&pullRequest=975)


author_association: NONE