Last active
December 12, 2025 23:10
-
-
Save Nasawa/3de5049337cacb75e31296eea2e13d4b to your computer and use it in GitHub Desktop.
Home Assistant Blueprint for monitoring a 3D printer using an AI Task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================ | |
| # AI 3D Printer Status Check Blueprint | |
| # ============================================================================ | |
| # Purpose: Monitors camera/image entities and uses AI to detect print failures | |
| # Author: Anigeek | |
| # Version: 2.0.0 | |
| # ============================================================================ | |
| blueprint: | |
| name: AI 3D Printer Status Check 2.0 | |
| description: > | |
| Monitors a camera or image entity on a repeating cadence **while** a state sensor | |
| equals a target value (case-insensitive). | |
| Each cycle runs a Home Assistant **AI Task** on a frame and exposes the AI result | |
| + snapshot path to an optional user script for platform-specific notifications. | |
| • **Local AI** = attach **camera** directly to the AI Task (fast, high quality). | |
| • **Remote AI** = attach **snapshot** file via Local Media (requires allowlisted media path). | |
| • **Image entities** = analyzed via entity_picture URL. | |
| • Optional confidence threshold (fixed number or entity) gates the built-in text alert. | |
| • Built-in **debug** and **failure** messages are text-only; image sending is delegated to your script. | |
| • Snapshot naming: overwrite a single file or write timestamped files (no automatic cleanup). | |
| • **Test mode** = run a single check immediately for testing configuration. | |
| **Notes:** | |
| - Image sending is delegated to **script_on_result**, keeping this blueprint notify-platform agnostic. | |
| - The script receives **nested objects** for clarity and long-term stability (see Scripts section). | |
| Community: [Community link for this blueprint](community.home-assistant.io/t/3d-printer-ai-status-check/919182) | |
| Version: 2.0.0 | |
| domain: automation | |
| source_url: https://gist.github.com/Nasawa/3de5049337cacb75e31296eea2e13d4b#file-3d-print-ai-health-check-yaml | |
| # -------------------------------------------------------------------------- | |
| # INPUT CONFIGURATION | |
| # -------------------------------------------------------------------------- | |
| # All user-configurable parameters are defined here | |
| # -------------------------------------------------------------------------- | |
| input: | |
| # === CORE INPUTS === | |
| # Required settings for basic operation | |
| core: | |
| name: Core inputs | |
| icon: mdi:camera | |
| collapsed: false | |
| description: Required entities and cadence. | |
| input: | |
| image_entity: | |
| name: Image or Camera Entity | |
| description: > | |
| Camera or image entity to monitor. Camera entities save snapshots to the configured path. | |
| Image entities are analyzed directly from their current state. | |
| selector: | |
| entity: | |
| domain: | |
| - camera | |
| - image | |
| multiple: false | |
| stage_sensor: | |
| name: State sensor | |
| description: Entity whose state controls when monitoring is active | |
| selector: | |
| entity: | |
| domain: | |
| - sensor | |
| multiple: false | |
| active_state: | |
| name: Active state value | |
| description: Case-insensitive match that enables monitoring (e.g., "Printing") | |
| default: Printing | |
| selector: | |
| text: | |
| multiline: false | |
| interval_minutes: | |
| name: Interval (minutes) | |
| default: 15 | |
| selector: | |
| number: | |
| min: 1.0 | |
| max: 120.0 | |
| step: 1.0 | |
| mode: slider | |
| test_mode: | |
| name: Test mode (run once immediately) | |
| description: > | |
| When enabled, runs a single check immediately regardless of state sensor value. | |
| Useful for testing configuration. Automation will complete after one cycle. | |
| default: false | |
| selector: | |
| boolean: {} | |
| # === AI SETTINGS === | |
| # Configure AI behavior, prompts, and image handling | |
| ai_settings: | |
| name: AI settings | |
| icon: mdi:robot | |
| collapsed: false | |
| description: Configure AI analysis behavior, task entity, and image attachment method. | |
| input: | |
| ai_task_entity: | |
| name: AI Task entity (optional) | |
| description: If set, this AI Task will be used instead of the system default. | |
| default: '' | |
| selector: | |
| entity: | |
| domain: | |
| - ai_task | |
| multiple: false | |
| ai_task_name: | |
| name: AI Task Name | |
| description: Name for the AI task (useful for organizing multiple automations) | |
| default: 'Image Health Check' | |
| selector: | |
| text: | |
| multiline: false | |
| ai_instructions: | |
| name: AI Instructions | |
| description: Custom prompt for the AI to analyze the image | |
| default: > | |
| You are inspecting a 3D printer's camera frame. Determine if the print is going wrong. | |
| Common failures include spaghetti (stringy filament), poor first-layer adhesion, | |
| severe warping/corner lift, nozzle clog with under-extrusion, layer shift, | |
| knocked-over part, filament blob near the hotend, or the part detaching from the bed. | |
| If there is meaningful risk of failure without intervention, set has_problem to true | |
| and provide brief, actionable advice. | |
| selector: | |
| text: | |
| multiline: true | |
| ai_schema: | |
| name: AI Response Schema (JSON) | |
| description: Define the structure of the AI response. Must be valid JSON. | |
| default: | | |
| { | |
| "has_problem": {"selector": {"boolean": {}}, "required": true}, | |
| "problem_type": {"selector": {"text": {}}}, | |
| "confidence": {"selector": {"number": {}}, "description": "0-100 (fraction like 0.85 will be interpreted as 85%)."}, | |
| "advice": {"selector": {"text": {}}} | |
| } | |
| selector: | |
| text: | |
| multiline: true | |
| use_local_model: | |
| name: Use local AI (attach camera directly) | |
| description: > | |
| ON: attach the camera live frame via media-source (good for local backends like Ollama). | |
| OFF: attach a snapshot file via Local Media (recommended for remote providers). | |
| Note: Image entities always use snapshot mode. | |
| default: true | |
| selector: | |
| boolean: {} | |
| snapshot_path_base: | |
| name: Local Media filesystem path | |
| description: > | |
| Filesystem path for your Local Media folder (NOT a URL). Commonly **/media**, | |
| sometimes **/config/media**. Must be listed under **homeassistant.allowlist_external_dirs**. | |
| default: /media | |
| selector: | |
| text: | |
| multiline: false | |
| snapshot_subfolder: | |
| name: Subfolder (under Local Media) | |
| description: > | |
| Optional subfolder name. If blank, defaults to `ai_health/<entity_object_id>` to avoid collisions. | |
| Ensure this folder exists before running the automation. | |
| default: '' | |
| selector: | |
| text: | |
| multiline: false | |
| image_format: | |
| name: Image Format | |
| description: Image format for saved snapshots | |
| default: jpg | |
| selector: | |
| select: | |
| options: | |
| - label: JPEG | |
| value: jpg | |
| - label: PNG | |
| value: png | |
| - label: WebP | |
| value: webp | |
| # === THRESHOLDS === | |
| # Optional confidence-based filtering | |
| thresholds: | |
| name: Thresholds (optional) | |
| icon: mdi:tune-variant | |
| collapsed: true | |
| description: > | |
| Gate the built-in text alert by minimum AI confidence. Your result script still receives all results. | |
| input: | |
| confidence_threshold: | |
| name: Confidence threshold (fixed) | |
| description: Notify only when normalized confidence ≥ this value (0-100). Leave blank to disable. | |
| selector: | |
| number: | |
| min: 0.0 | |
| max: 100.0 | |
| step: 1.0 | |
| mode: slider | |
| confidence_threshold_entity: | |
| name: Confidence threshold entity (number or sensor) | |
| description: If set, this entity's numeric value (0-100) takes precedence over the fixed value. | |
| default: '' | |
| selector: | |
| entity: | |
| domain: | |
| - number | |
| - sensor | |
| multiple: false | |
| # === SCRIPTS === | |
| # Optional custom script for handling results | |
| scripts: | |
| name: Script (optional) | |
| icon: mdi:script-text-outline | |
| collapsed: true | |
| description: | | |
| Provide a script to handle platform-specific notifications or archival. | |
| The same script is called for success **and** failure; on failure, an `error` object is included. | |
| <details> | |
| <summary><strong>script_on_result payload (nested objects)</strong></summary> | |
| ```yaml | |
| # On success: | |
| ai_result: | |
| has_problem: bool | |
| problem_type: string|null | |
| advice: string|null | |
| confidence: | |
| raw: number|null # as returned by the model (0.85 or 85) | |
| normalized: number|null # 0-100; null if model omitted confidence | |
| snapshot: | |
| abs_path: string # absolute file path | |
| rel_path: string # relative under Local Media (e.g., subfolder/filename.jpg) | |
| provider_mode: string # "local" or "remote" | |
| script_context: | |
| image_entity: string | |
| ai_task_entity: string|null | |
| ts: string # ISO8601 timestamp | |
| cycle_number: number # iteration count for this automation run | |
| threshold: | |
| active: boolean | |
| value: number|null | |
| notified: boolean # whether built-in alert fired this cycle | |
| error: | |
| message: string # not provided if there is no error | |
| ``` | |
| </details> | |
| input: | |
| script_on_result: | |
| name: Script on result (success or failure) | |
| description: Optional script called each cycle; receives nested objects as above. | |
| default: '' | |
| selector: | |
| entity: | |
| domain: | |
| - script | |
| multiple: false | |
| # === HOUSEKEEPING === | |
| # File management options | |
| housekeeping: | |
| name: Housekeeping (optional) | |
| icon: mdi:broom | |
| collapsed: true | |
| description: > | |
| Control snapshot file naming. This blueprint does not delete files. | |
| If you choose timestamped files, you'll need to handle cleanup yourself. | |
| input: | |
| overwrite_snapshot: | |
| name: Overwrite single file (latest) | |
| description: > | |
| If ON, writes `<subfolder>/latest.<ext>` each run. | |
| OFF writes timestamped files (no auto-cleanup). | |
| default: true | |
| selector: | |
| boolean: {} | |
| # === NOTIFICATIONS === | |
| # Built-in notification configuration | |
| notifications: | |
| name: Built-in notifications (optional) | |
| icon: mdi:message-outline | |
| collapsed: true | |
| description: > | |
| Text-only status messages from the blueprint itself. | |
| Custom notify actions have access to all variables including ai, normalized_confidence, | |
| snapshot paths, has_problem, etc. | |
| input: | |
| notify_action: | |
| name: Custom notify action (optional) | |
| description: > | |
| An action (or sequence) to run when the blueprint emits a text notification. | |
| If provided, it will be used instead of the simple notify service below. | |
| Available variables: notification_title, notification_message, notification_type, | |
| ai (full response), normalized_confidence, raw_confidence, has_problem, | |
| problem_type, advice, snapshot_abs_path, snapshot_rel_path, cycle_number, and more. | |
| default: [] | |
| selector: | |
| action: {} | |
| notify_service: | |
| name: Notify service for text pings (fallback) | |
| description: > | |
| e.g., `notify.mobile_app_<device>` or `notify.persistent_notification`. | |
| Leave blank to disable if not using a custom action. | |
| default: '' | |
| selector: | |
| text: | |
| multiline: false | |
| # === DEBUGGING === | |
| # Debug mode configuration | |
| debugging: | |
| name: Debug (optional) | |
| icon: mdi:bug | |
| collapsed: true | |
| description: Extra telemetry each cycle. | |
| input: | |
| debug_mode: | |
| name: Debug mode | |
| default: false | |
| selector: | |
| boolean: {} | |
| # ---------------------------------------------------------------------------- | |
| # TRIGGER CONFIGURATION | |
| # ---------------------------------------------------------------------------- | |
| # Automation triggers on state sensor changes or Home Assistant start | |
| # ---------------------------------------------------------------------------- | |
| triggers: | |
| # Trigger when the stage sensor changes state | |
| - trigger: state | |
| entity_id: !input stage_sensor | |
| # Trigger when Home Assistant starts (to resume monitoring) | |
| - trigger: homeassistant | |
| event: start | |
| # Restart mode: if triggered again, cancel current run and start fresh | |
| mode: restart | |
| # ---------------------------------------------------------------------------- | |
| # VARIABLE DEFINITIONS | |
| # ---------------------------------------------------------------------------- | |
| # All variables are computed once at automation start and remain constant | |
| # throughout the automation run (except cycle_count which increments) | |
| # ---------------------------------------------------------------------------- | |
| variables: | |
| # ===== INPUT REFERENCES ===== | |
| # Store input values in variables for easier access | |
| image_entity: !input image_entity | |
| stage_sensor: !input stage_sensor | |
| interval_min: !input interval_minutes | |
| active_state: !input active_state | |
| test_mode_enabled: !input test_mode | |
| ai_entity: !input ai_task_entity | |
| ai_task_name: !input ai_task_name | |
| ai_instructions: !input ai_instructions | |
| ai_schema_raw: !input ai_schema | |
| use_local: !input use_local_model | |
| snapshot_base: !input snapshot_path_base | |
| snapshot_subfolder: !input snapshot_subfolder | |
| image_format: !input image_format | |
| threshold_fixed: !input confidence_threshold | |
| threshold_entity: !input confidence_threshold_entity | |
| script_result: !input script_on_result | |
| overwrite_snapshot: !input overwrite_snapshot | |
| notify_action: !input notify_action | |
| notify_service: !input notify_service | |
| debug_enabled: !input debug_mode | |
| # ===== CONFIGURATION FLAGS ===== | |
| # Boolean flags to check if optional features are configured | |
| # Check if custom notify action is configured (array has items) | |
| has_notify_action: '{{ (notify_action | default([])) | length > 0 }}' | |
| # Check if notify service is configured (non-empty string) | |
| has_notify_service: '{{ (notify_service | string | trim) | length > 0 }}' | |
| # Check if custom AI task entity is specified | |
| has_ai_entity: '{{ (ai_entity | string | trim) | length > 0 }}' | |
| # Check if result script is configured | |
| has_script: '{{ (script_result | string | trim) | length > 0 }}' | |
| # ===== JSON SCHEMA VALIDATION ===== | |
| # Validate the AI schema is valid JSON before attempting to use it | |
| # Test if schema can be parsed as JSON (returns none if invalid) | |
| ai_schema_valid: > | |
| {% set test_parse = ai_schema_raw | from_json(none) %} | |
| {{ test_parse is not none }} | |
| # Parse schema if valid, otherwise set to none (will trigger error later) | |
| ai_schema: > | |
| {% if ai_schema_valid %} | |
| {{ ai_schema_raw | from_json }} | |
| {% else %} | |
| {{ none }} | |
| {% endif %} | |
| # ===== THRESHOLD CALCULATION ===== | |
| # Determine which threshold to use (entity takes precedence over fixed) | |
| # Check if threshold entity is provided | |
| threshold_entity_provided: '{{ (threshold_entity | string | trim) | length > 0 }}' | |
| # Get threshold value from entity (if provided) | |
| threshold_from_entity: '{{ states(threshold_entity) | float(''nan'') if threshold_entity_provided else float(''nan'') }}' | |
| # Get threshold value from fixed input (if provided) | |
| threshold_from_fixed: '{{ threshold_fixed | float(''nan'') if (threshold_fixed is not none) else float(''nan'') }}' | |
| # Final threshold value: entity takes precedence over fixed | |
| confidence_threshold: > | |
| {% if threshold_entity_provided %} | |
| {{ threshold_from_entity }} | |
| {% else %} | |
| {{ threshold_from_fixed }} | |
| {% endif %} | |
| # Check if threshold is active (value == value means not NaN) | |
| threshold_active: '{{ confidence_threshold == confidence_threshold }}' | |
| # Check if both threshold methods are configured (for warning) | |
| both_thresholds_set: '{{ threshold_entity_provided and (threshold_fixed is not none) }}' | |
| # ===== ENTITY INFORMATION ===== | |
| # Extract and compute entity-related information | |
| # Get entity object ID (e.g., "camera.printer" -> "printer") | |
| entity_obj_id: '{{ (image_entity | string).split(''.'')[-1] }}' | |
| # Get entity domain (e.g., "camera.printer" -> "camera") | |
| entity_domain: '{{ (image_entity | string).split(''.'')[0] }}' | |
| # Check if entity is a camera | |
| is_camera: '{{ entity_domain == ''camera'' }}' | |
| # Check if entity is an image entity | |
| is_image: '{{ entity_domain == ''image'' }}' | |
| # ===== IMAGE FORMAT MAPPING ===== | |
| # Map user-selected format to file extension and MIME type | |
| # File extension based on selected format | |
| format_extension: > | |
| {% if image_format == 'jpg' %}jpg | |
| {% elif image_format == 'png' %}png | |
| {% elif image_format == 'webp' %}webp | |
| {% else %}jpg{% endif %} | |
| # MIME type for AI task attachment | |
| format_mime_type: > | |
| {% if image_format == 'jpg' %}image/jpeg | |
| {% elif image_format == 'png' %}image/png | |
| {% elif image_format == 'webp' %}image/webp | |
| {% else %}image/jpeg{% endif %} | |
| # ===== SNAPSHOT PATH CONSTRUCTION ===== | |
| # Build file paths for saving snapshots | |
| # Default subfolder if user doesn't specify one | |
| default_subfolder: 'ai_health/{{ entity_obj_id }}' | |
| # Final subfolder (use user-specified or default) | |
| folder: > | |
| {% set sf = (snapshot_subfolder | string | trim) %} | |
| {% if sf | length > 0 %} | |
| {{ sf.strip('/') }} | |
| {% else %} | |
| {{ default_subfolder }} | |
| {% endif %} | |
| # Absolute path to the snapshot folder | |
| folder_abs_path: '{{ snapshot_base.rstrip(''/'') }}/{{ folder }}' | |
| # ============================================================================ | |
| # ACTION SEQUENCE | |
| # ============================================================================ | |
| # The main automation logic - runs in order from top to bottom | |
| # ============================================================================ | |
| actions: | |
| # -------------------------------------------------------------------------- | |
| # STEP 1: VALIDATE CONFIGURATION | |
| # -------------------------------------------------------------------------- | |
| # Ensure all configuration is valid before proceeding | |
| # -------------------------------------------------------------------------- | |
| # === VALIDATE JSON SCHEMA === | |
| # If the AI schema is invalid JSON, notify user and stop | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ not ai_schema_valid }}' | |
| sequence: | |
| # Set notification variables for the error | |
| - variables: | |
| notification_title: 'AI Health Check - Configuration Error' | |
| notification_message: > | |
| The AI Response Schema is not valid JSON. Please fix the schema in the blueprint configuration. | |
| Automation cannot continue. | |
| notification_type: 'error' | |
| # Send notification using configured method | |
| - choose: | |
| # Use custom notify action if configured | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_action }}' | |
| sequence: !input notify_action | |
| # Fallback to notify service if configured | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_service }}' | |
| sequence: | |
| - action: !input notify_service | |
| data: | |
| title: '{{ notification_title }}' | |
| message: '{{ notification_message }}' | |
| # Stop the automation - cannot proceed with invalid schema | |
| - stop: 'Invalid AI schema - cannot continue' | |
| # === WARN ABOUT THRESHOLD CONFLICT === | |
| # If both fixed and entity thresholds are set, warn the user | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ both_thresholds_set }}' | |
| sequence: | |
| - action: persistent_notification.create | |
| data: | |
| title: AI Health Check - Threshold Conflict | |
| message: > | |
| Both a fixed confidence threshold and a threshold entity were provided. | |
| The entity value will be used. Clear one of them to remove this message. | |
| # === SNAPSHOT DIRECTORY === | |
| # Note: The snapshot directory should exist before running this automation. | |
| # Most camera.snapshot and downloader services will create parent directories | |
| # automatically, but if you encounter errors, manually create the folder first. | |
| # -------------------------------------------------------------------------- | |
| # STEP 2: CHECK RUN CONDITIONS | |
| # -------------------------------------------------------------------------- | |
| # Only proceed if test mode is enabled OR stage sensor matches active state | |
| # -------------------------------------------------------------------------- | |
| - condition: template | |
| value_template: '{{ test_mode_enabled or ((states(stage_sensor) | lower) == (active_state | lower)) }}' | |
| # === RANDOM DELAY === | |
| # Add 0-5 second random delay to avoid simultaneous triggers | |
| # (skipped in test mode for immediate execution) | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ not test_mode_enabled }}' | |
| sequence: | |
| - delay: | |
| seconds: '{{ range(0, 5) | random }}' | |
| # -------------------------------------------------------------------------- | |
| # STEP 3: INITIALIZE CYCLE TRACKING | |
| # -------------------------------------------------------------------------- | |
| # Set up cycle counter for tracking iterations | |
| # -------------------------------------------------------------------------- | |
| - variables: | |
| cycle_count: 0 | |
| # -------------------------------------------------------------------------- | |
| # STEP 4: MAIN MONITORING LOOP | |
| # -------------------------------------------------------------------------- | |
| # Repeatedly check the image until conditions change | |
| # In test mode, runs exactly once | |
| # -------------------------------------------------------------------------- | |
| - repeat: | |
| # Exit condition: test mode OR stage sensor no longer matches active state | |
| until: | |
| - condition: template | |
| value_template: '{{ test_mode_enabled or ((states(stage_sensor) | lower) != (active_state | lower)) }}' | |
| sequence: | |
| # ====================================================================== | |
| # CYCLE START - INCREMENT COUNTER AND GENERATE PATHS | |
| # ====================================================================== | |
| # === INCREMENT CYCLE COUNTER === | |
| # Track which iteration we're on | |
| - variables: | |
| cycle_count: '{{ cycle_count + 1 }}' | |
| # Generate timestamp for this cycle (format: YYYYMMDD-HHMMSS) | |
| cycle_ts: '{{ now().strftime(''%Y%m%d-%H%M%S'') }}' | |
| # Determine filename: either "latest.ext" or "timestamp.ext" | |
| filename: '{% if overwrite_snapshot %}latest.{{ format_extension }}{% else %}{{ cycle_ts }}.{{ format_extension }}{% endif %}' | |
| # Build relative path (under Local Media) | |
| snapshot_rel_path: '{{ folder }}/{{ filename }}' | |
| # Build absolute filesystem path | |
| snapshot_abs_path: '{{ folder_abs_path }}/{{ filename }}' | |
| # Build media source ID for Home Assistant media browser | |
| media_source_id: 'media-source://media_source/local/{{ snapshot_rel_path }}' | |
| # ====================================================================== | |
| # IMAGE CAPTURE - CAMERA SNAPSHOT | |
| # ====================================================================== | |
| # Capture snapshot for camera entities | |
| # Image entities are analyzed directly from entity_picture URL (no snapshot needed) | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ is_camera }}' | |
| sequence: | |
| - action: camera.snapshot | |
| continue_on_error: true | |
| target: | |
| entity_id: !input image_entity | |
| data: | |
| filename: '{{ snapshot_abs_path }}' | |
| # Short delay to ensure snapshot is fully written | |
| - delay: 00:00:02 | |
| # ====================================================================== | |
| # AI TASK PREPARATION - BUILD REQUEST DATA | |
| # ====================================================================== | |
| # Determine how to attach the image to the AI task | |
| - variables: | |
| # Use direct camera feed if: local mode + camera entity | |
| use_direct_camera: '{{ use_local and is_camera }}' | |
| # Build attachment URL based on entity type and mode | |
| ai_attachment: > | |
| {% if use_direct_camera %} | |
| media-source://camera/{{ image_entity }} | |
| {% elif is_image %} | |
| {{ state_attr(image_entity, 'entity_picture') }} | |
| {% else %} | |
| {{ media_source_id }} | |
| {% endif %} | |
| # Build full task name with mode indicator | |
| ai_task_name_full: '{{ ai_task_name }} ({{ ''live'' if use_direct_camera else ''snapshot'' }})' | |
| # ====================================================================== | |
| # AI TASK EXECUTION - CALL AI TO ANALYZE IMAGE | |
| # ====================================================================== | |
| # Call either custom AI task entity or system default | |
| - choose: | |
| # === CUSTOM AI TASK ENTITY === | |
| # User specified a particular AI task to use | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_ai_entity }}' | |
| sequence: | |
| - action: ai_task.generate_data | |
| continue_on_error: true # Don't stop automation if AI fails | |
| data: | |
| entity_id: !input ai_task_entity | |
| task_name: '{{ ai_task_name_full }}' | |
| instructions: '{{ ai_instructions }}' | |
| structure: '{{ ai_schema }}' | |
| attachments: | |
| media_content_id: '{{ ai_attachment }}' | |
| media_content_type: '{{ format_mime_type }}' | |
| response_variable: ai # Store response in 'ai' variable | |
| # === DEFAULT AI TASK === | |
| # Use Home Assistant's default/system AI task | |
| default: | |
| - action: ai_task.generate_data | |
| continue_on_error: true | |
| data: | |
| task_name: '{{ ai_task_name_full }}' | |
| instructions: '{{ ai_instructions }}' | |
| structure: '{{ ai_schema }}' | |
| attachments: | |
| media_content_id: '{{ ai_attachment }}' | |
| media_content_type: '{{ format_mime_type }}' | |
| response_variable: ai | |
| # ====================================================================== | |
| # ERROR HANDLING - HANDLE AI TASK FAILURES | |
| # ====================================================================== | |
| # Check if AI task failed (response variable not defined) | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ ai is not defined }}' | |
| sequence: | |
| # === CALL USER SCRIPT WITH ERROR === | |
| # Let user's script know the AI task failed | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_script }}' | |
| sequence: | |
| - action: !input script_on_result | |
| data: | |
| error: | |
| message: no assistant content returned | |
| snapshot: | |
| abs_path: '{{ snapshot_abs_path }}' | |
| rel_path: '{{ snapshot_rel_path }}' | |
| provider_mode: '{{ ''local'' if use_direct_camera else ''remote'' }}' | |
| script_context: | |
| image_entity: '{{ image_entity }}' | |
| ai_task_entity: '{{ ai_entity if has_ai_entity else none }}' | |
| ts: '{{ now().isoformat() }}' | |
| cycle_number: '{{ cycle_count }}' | |
| # === SEND FAILURE NOTIFICATION === | |
| # Notify user about the AI task failure | |
| - variables: | |
| notification_title: 'AI Health Check - Processing Failed' | |
| notification_message: > | |
| The AI task did not return a valid assistant message this cycle. | |
| Try a different instruct/JSON-friendly model. (Monitoring continues.) | |
| Cycle: {{ cycle_count }} | |
| notification_type: 'error' | |
| # Expose variables for custom notify action | |
| snapshot_abs_path: '{{ snapshot_abs_path }}' | |
| snapshot_rel_path: '{{ snapshot_rel_path }}' | |
| cycle_number: '{{ cycle_count }}' | |
| # Send notification via configured method | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_action }}' | |
| sequence: !input notify_action | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_service }}' | |
| sequence: | |
| - action: !input notify_service | |
| data: | |
| title: '{{ notification_title }}' | |
| message: '{{ notification_message }}' | |
| # ====================================================================== | |
| # RESULT PROCESSING - EXTRACT AND NORMALIZE AI RESULTS | |
| # ====================================================================== | |
| # Extract and normalize confidence, problem status, etc. | |
| - variables: | |
| # === EXTRACT RAW CONFIDENCE === | |
| # AI may return confidence in different formats | |
| raw_confidence: > | |
| {% if ai is defined and (ai.data.confidence is defined or ai.data.get('confidence') is not none) %} | |
| {{ ai.data.confidence if (ai.data.confidence is defined) else ai.data.get('confidence') }} | |
| {% else %} | |
| {{ none }} | |
| {% endif %} | |
| # === NORMALIZE CONFIDENCE === | |
| # Convert to 0-100 scale (AI might return 0.85 or 85) | |
| normalized_confidence: > | |
| {% if raw_confidence is not none %} | |
| {% set c = raw_confidence | float(0) %} | |
| {{ (c * 100) if c <= 1 else c }} | |
| {% else %} | |
| {{ none }} | |
| {% endif %} | |
| # === EXTRACT PROBLEM STATUS === | |
| # Check if AI detected a problem | |
| has_problem: '{{ ai is defined and (ai.data.has_problem | default(false)) }}' | |
| # === EXTRACT PROBLEM DETAILS === | |
| # Get problem type and advice if available | |
| problem_type: '{{ ai.data.problem_type | default(none) if ai is defined else none }}' | |
| advice: '{{ ai.data.advice | default(none) if ai is defined else none }}' | |
| # === CHECK THRESHOLD === | |
| # Determine if confidence meets threshold (if active) | |
| meets_threshold: > | |
| {% if not threshold_active %} | |
| true | |
| {% else %} | |
| {% if normalized_confidence is none %} | |
| false | |
| {% else %} | |
| {{ (normalized_confidence | float(0)) >= (confidence_threshold | float(0)) }} | |
| {% endif %} | |
| {% endif %} | |
| # === DETERMINE IF WE SHOULD NOTIFY === | |
| # Only notify if: problem detected AND threshold met | |
| should_notify: '{{ has_problem and meets_threshold }}' | |
| # === EXPOSE PATHS FOR NOTIFICATIONS === | |
| # Make snapshot paths available to custom notify actions | |
| snapshot_abs_path: '{{ snapshot_abs_path }}' | |
| snapshot_rel_path: '{{ snapshot_rel_path }}' | |
| cycle_number: '{{ cycle_count }}' | |
| # ====================================================================== | |
| # PROBLEM NOTIFICATION - ALERT IF ISSUE DETECTED | |
| # ====================================================================== | |
| # Send alert if problem detected and threshold met | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ should_notify }}' | |
| sequence: | |
| # Build notification message with problem details | |
| - variables: | |
| notification_title: '⚠️ Image Issue Detected' | |
| notification_message: > | |
| Type: {{ problem_type | default('unknown') }} | |
| Confidence: {{ (normalized_confidence | float(0)) | round(1) if (normalized_confidence is not none) else '—' }}% | |
| Advice: {{ advice | default('Check the print ASAP.') }} | |
| Cycle: {{ cycle_count }} | |
| (via AI Task) | |
| notification_type: 'problem' | |
| # Send via configured notification method | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_action }}' | |
| sequence: !input notify_action | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_service }}' | |
| sequence: | |
| - action: !input notify_service | |
| data: | |
| title: '{{ notification_title }}' | |
| message: '{{ notification_message }}' | |
| # ====================================================================== | |
| # USER SCRIPT CALLBACK - PASS RESULTS TO CUSTOM SCRIPT | |
| # ====================================================================== | |
| # Call user's result script with full AI results | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_script and ai is defined }}' | |
| sequence: | |
| - action: !input script_on_result | |
| data: | |
| # AI analysis results | |
| ai_result: | |
| has_problem: '{{ has_problem }}' | |
| problem_type: '{{ problem_type }}' | |
| advice: '{{ advice }}' | |
| confidence: | |
| raw: '{{ raw_confidence if (raw_confidence is not none) else none }}' | |
| normalized: '{{ normalized_confidence if (normalized_confidence is not none) else none }}' | |
| # Snapshot information | |
| snapshot: | |
| abs_path: '{{ snapshot_abs_path }}' | |
| rel_path: '{{ snapshot_rel_path }}' | |
| provider_mode: '{{ ''local'' if use_direct_camera else ''remote'' }}' | |
| # Context and metadata | |
| script_context: | |
| image_entity: '{{ image_entity }}' | |
| ai_task_entity: '{{ ai_entity if has_ai_entity else none }}' | |
| ts: '{{ now().isoformat() }}' | |
| cycle_number: '{{ cycle_count }}' | |
| threshold: | |
| active: '{{ threshold_active }}' | |
| value: '{{ confidence_threshold if threshold_active else none }}' | |
| notified: '{{ should_notify }}' | |
| # ====================================================================== | |
| # DEBUG OUTPUT - DETAILED TELEMETRY IF ENABLED | |
| # ====================================================================== | |
| # Send detailed debug information if debug mode is on | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ debug_enabled }}' | |
| sequence: | |
| # Build comprehensive debug message | |
| - variables: | |
| notification_title: 'AI Health Check - Debug' | |
| notification_message: > | |
| {% if ai is not defined %} | |
| Cycle {{ cycle_count }}: AI failed this cycle (no assistant content). | |
| {% else %} | |
| Cycle {{ cycle_count }}: | |
| Has Problem={{ has_problem }}, | |
| Type={{ problem_type | default('—') }}, | |
| Confidence (raw)={{ raw_confidence | default('—') }}, | |
| Confidence (normalized)={{ (normalized_confidence | float(0)) | round(2) if (normalized_confidence is not none) else '—' }}, | |
| Threshold Active={{ threshold_active }}, | |
| Threshold={{ (confidence_threshold | float(0)) | round(1) if threshold_active else '—' }}, | |
| Notified={{ should_notify }}, | |
| Snapshot={{ snapshot_abs_path }} | |
| {% endif %} | |
| notification_type: 'debug' | |
| # Send debug notification | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_action }}' | |
| sequence: !input notify_action | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ has_notify_service }}' | |
| sequence: | |
| - action: !input notify_service | |
| data: | |
| title: '{{ notification_title }}' | |
| message: '{{ notification_message }}' | |
| # ====================================================================== | |
| # CYCLE END - WAIT FOR NEXT INTERVAL | |
| # ====================================================================== | |
| # Wait before next check (skipped in test mode for single run) | |
| - choose: | |
| - conditions: | |
| - condition: template | |
| value_template: '{{ not test_mode_enabled }}' | |
| sequence: | |
| - delay: | |
| minutes: '{{ interval_min | int }}' | |
| # ============================================================================ | |
| # END OF BLUEPRINT | |
| # ============================================================================ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment