From d8bcf05bf6a5873fe838dd8d4f316c42873b9f9e Mon Sep 17 00:00:00 2001 From: zorruno Date: Mon, 20 Jul 2026 19:24:35 +1200 Subject: [PATCH] various packages --- .ha_run.lock | 2 +- .prettierrc.yaml | 43 + AGENTS.md | 807 +++++ esphome/.device-builder.json | 2646 +++++++++++++---- esphome/esp-astroclockstepper1 copy.old | 1657 ----------- esphome/esp-ledmatrix1 copy.yaml.V1 | 916 ------ esphome/esp-ledmatrix1.yaml.V2 | 1397 --------- .../appliance_monitor_processor.yaml | 209 +- .../appliances_status_monitoring.yaml | 492 ++- .../Job_Chore_Tracking/jobs_tracker_jobs.yaml | 1141 ++++--- .../jobs_tracker_processor.yaml | 372 ++- packages/appliance_status_mqttfeed.yaml | 4 + packages/bedtime_mode_actions.yaml | 6 + packages/open_meteo_rain_chance.yaml | 277 ++ packages/view_road_announcement_router.yaml | 84 +- 15 files changed, 4743 insertions(+), 5310 deletions(-) create mode 100644 .prettierrc.yaml create mode 100644 AGENTS.md delete mode 100644 esphome/esp-astroclockstepper1 copy.old delete mode 100644 esphome/esp-ledmatrix1 copy.yaml.V1 delete mode 100644 esphome/esp-ledmatrix1.yaml.V2 create mode 100644 packages/open_meteo_rain_chance.yaml diff --git a/.ha_run.lock b/.ha_run.lock index 1ea9d4f..7c21484 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783321380.267702} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.7.2", "start_ts": 1784512870.0398877} \ No newline at end of file diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..bba3950 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,43 @@ +# ============================================================================= +# Prettier Configuration for Home Assistant YAML +# Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/ +# ============================================================================= +# +# Prettier auto-formats YAML files written by OpenCode. This config aligns +# with the official Home Assistant YAML Style Guide where possible. +# +# WHAT PRETTIER ENFORCES: +# - 2-space indentation (no tabs) +# - Double quotes for strings (HA convention) +# - Trailing newline at end of file +# - Consistent spacing and formatting +# - Preserves HA custom tags (!include, !secret, !input, !env_var) +# - Preserves block style sequences and mappings +# - Preserves multiline strings (literal | and folded > styles) +# - Preserves comments +# +# WHAT PRETTIER DOES NOT ENFORCE (must be handled manually or by the AI): +# - Converting flow style [1, 2, 3] to block style sequences +# - Converting flow style { key: val } to block style mappings +# - Normalizing null/~ to implicit null (just "key:" with no value) +# - Normalizing truthy booleans (Yes/On/TRUE) to lowercase true/false +# - Wrapping long lines (templates, etc.) +# +# Users can customize this file. See: https://prettier.io/docs/en/options +# ============================================================================= + +# HA YAML Style Guide: "An indentation of 2 spaces must be used" +tabWidth: 2 +useTabs: false + +# HA YAML Style Guide: "Strings are preferably quoted with double quotes" +singleQuote: false + +# Keep printWidth generous to avoid prettier breaking long YAML keys into +# explicit key notation (?). HA templates can be long but should be manually +# split using literal (|) or folded (>) block scalars per the style guide. +printWidth: 120 + +# Preserve multiline string formatting (literal |, folded >, etc.) +# HA YAML Style Guide: "make use of the literal style and folded style strings" +proseWrap: "preserve" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..55482c3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,807 @@ +# Home Assistant OpenCode Rules + +You are working directly within a Home Assistant installation. Your working directory is `/homeassistant`, which is the live Home Assistant configuration directory. + +## CRITICAL: User Consent and Scope Rules + +You MUST follow these rules strictly: + +1. **Never exceed the user's request** - Do exactly what the user asks, nothing more. Do not "improve" or "enhance" beyond the stated scope. + +2. **Never make changes without explicit approval** - Before modifying ANY file: + - Show the user exactly what you plan to change + - Wait for their explicit confirmation ("yes", "go ahead", "do it", etc.) + - If they haven't approved, DO NOT proceed + +3. **Ask, don't assume** - If the user's request is ambiguous: + - Ask clarifying questions first + - Present options and let them choose + - Never guess at their intent + +4. **Read-only by default** - When investigating or troubleshooting: + - Only read files and gather information + - Present findings and recommendations + - Wait for user instruction before making any changes + +5. **One change at a time** - When making approved changes: + - Make the minimum change needed + - Show what was changed + - Let the user verify before proceeding to any next step + +6. **No unsolicited modifications** - Never: + - "Clean up" code the user didn't ask about + - Add features they didn't request + - Refactor working configurations + - Fix issues they haven't mentioned + +7. **Respect "no"** - If a user declines a suggestion, do not: + - Repeat the suggestion + - Make the change anyway + - Try to convince them otherwise + +## Environment Context + +- You are running inside the OpenCode app +- The current directory (`/homeassistant`) contains the live Home Assistant configuration +- Changes to YAML files here directly affect the Home Assistant instance +- If add-on folder access is enabled, `/addons` and `/addon_configs` are available for Home Assistant add-on development. Treat `/addon_configs` as sensitive and only inspect or modify these folders when the user explicitly asks. +- You may have access to MCP tools for interacting with Home Assistant (check with the user) + +## Home Assistant Interaction Model + +There are three primary, safe ways to interact with Home Assistant: + +### 1. Configuration Files (YAML) +The standard way to define and customize Home Assistant behavior: +- Automations, scripts, scenes, and blueprints +- Integration and sensor configurations +- Templates, packages, and customizations +- Dashboard (Lovelace) definitions + +These files are designed for user editing and are the source of truth for your Home Assistant setup. + +### 2. MCP Tools (Runtime API) +Real-time interaction with the running Home Assistant instance: +- Query current entity states and history +- Control devices and call services +- Validate configurations +- Diagnose issues and detect anomalies +- Report OpenCode/HA agent capability status with `get_agent_capabilities` + +### Native Home Assistant LLM Platform +Home Assistant is developing a native `llm` integration where Core integrations and custom integrations can expose curated tools through `/llm.py` and registered LLM APIs. New Home Assistant builds may also expose those APIs over native MCP endpoints such as `/api/mcp/`; the built-in Assist API uses `/api/mcp/assist`. This is complementary to OpenCode MCP, not a replacement. + +- If the optional `homeassistant_native` MCP server is available, prefer it for requests that fit the configured native Home Assistant LLM API because those tools are curated by Home Assistant. +- Use OpenCode MCP for configuration editing, safe writes, validation, admin/dev workflows, screenshots, updates, ESPHome, `hab`, Zigbee tasks, add-on development, and Home Assistant documentation lookup. +- Use `get_agent_capabilities` or `ha://agent/capabilities` to check whether the running HA instance reports the native `llm` component and native MCP endpoints. +- Use `get_home_context` for compact area/domain/entity understanding before broad state dumps. +- Use `get_ha_llm_development_guide` when helping develop or review a custom integration's native `/llm.py` provider. +- Do not assume this add-on can register tools directly with HA's native `llm` platform; native tool registration is internal to HA integrations/custom integrations. The add-on can consume configured native LLM APIs through native MCP when Home Assistant exposes them. + +### 3. hab CLI (Home Assistant Builder) +A CLI tool designed for AI agents to manage Home Assistant. Run `hab` commands via the terminal: +- **Entity management**: `hab entity list`, `hab entity get light.living_room`, `hab entity logbook sensor.power --start 2h` +- **Service calls**: `hab action call light.turn_on --entity light.living_room --data '{"brightness": 200}'` +- **Automation CRUD**: `hab automation list`, `hab automation create`, `hab automation delete` +- **Dashboard management**: `hab dashboard list`, `hab dashboard view create` +- **Area/floor/zone/label**: `hab area list`, `hab area create "Kitchen"` +- **Helpers**: `hab helper list`, `hab helper create` +- **Scripts**: `hab script list`, `hab script create` +- **Scenes**: `hab scene list`, `hab scene create`, `hab scene activate "Movie Time"` +- **Blueprints**: `hab blueprint list` +- **Backups**: `hab backup list`, `hab backup create` +- **System**: `hab system info`, `hab system health`, `hab overview` +- **Devices**: `hab device list` +- **People**: `hab person list`, `hab person create`, `hab person update` +- **Categories**: `hab category list`, `hab category assign --entity light.kitchen` +- **To-do lists**: `hab todo list`, `hab todo item list todo.shopping`, `hab todo item add todo.shopping "Milk"` +- **Notifications**: `hab notification list`, `hab notification create --message "Hello" --title "Alert"` +- **Integrations**: `hab integration list`, `hab integration reload hue`, `hab integration disable mqtt` +- **Repairs**: `hab repairs list`, `hab repairs ignore ` +- **Events**: `hab event list`, `hab event fire my_custom_event --data '{"key": "value"}'` +- **Templates**: `hab template render --expression "{{ states('sensor.temperature') }}"` +- **Search**: `hab search related` + +`hab` outputs human-readable text by default. Use `--json` for structured JSON output (ideal for parsing). +`hab` is pre-authenticated via the Supervisor token - no login required. +Run `hab --help` or `hab --help` for full usage details. + + +``` +Home Assistant Builder (hab) is a CLI utility designed for LLMs +to build and manage Home Assistant configurations. + +Interactive sessions default to human-readable text. Non-interactive sessions default to JSON. + +Start with 'hab guide' for workflow-level guidance optimized for LLM and agent usage. + +Usage: + hab [command] + +Getting Started: + auth Manage authentication + capability Inspect runtime capabilities + guide Display built-in usage guides + overview Show an overview of the Home Assistant instance + schema Show machine-readable command schema + +Registry: + area Manage areas + device Manage devices + entity Manage entities + floor Manage floors + label Manage labels + person Manage persons + search Search for items and relationships + zone Manage zones + +Automation: + action Call actions (services) + automation Manage automations + blueprint Manage blueprints + category Manage categories + helper Manage groups, templates, and other helpers + scene Manage scenes + script Manage scripts + +Dashboard: + dashboard Manage dashboards + +Other: + backup Manage backups + calendar Manage calendar events + diagnostics Manage diagnostics handlers + energy Manage energy dashboard settings + esphome Manage ESPHome devices + event Manage Home Assistant events + integration Manage integrations + network Manage network settings + notification Manage persistent notifications + repairs Manage Home Assistant repairs + system Manage system + template Work with Home Assistant templates + thread Manage Thread credentials + todo Manage to-do list items + update Update hab to the latest version + version Show version information + +Additional Commands: + help Help about any command + +Flags: + --config string Path to config directory (default: ~/.config/home-assistant-builder) + -h, --help help for hab + --json Use JSON output instead of human-readable text + --skip-update-check Skip automatic update check on startup + --text Use human-readable text output + --verbose Show verbose output + +Use "hab [command] --help" for more information about a command. +``` + + +**Use configuration files when:** defining behavior, creating automations, setting up integrations +**Use MCP tools when:** checking current state, safe config writing, anomaly detection, entity diagnostics +**Use hab CLI when:** managing dashboards, areas, helpers, backups, blueprints, and bulk admin operations + +### 4. zigporter CLI (Zigbee Toolkit) +A CLI for Zigbee device management in Home Assistant. Handles cascade renames (updating entity IDs across automations, scripts, scenes, and all Lovelace dashboards atomically), device inspection, stale device cleanup, and Zigbee mesh visualization. + +**Cascade rename** — zigporter's unique value: when you rename an entity or device, it automatically patches every reference in automations, scripts, scenes, and Lovelace dashboards. `hab` can rename a single entity/device but does NOT cascade to references. + +Key commands: +- **Cascade rename**: `zigporter rename-entity light.old_id light.new_id --apply`, `zigporter rename-device "Old Name" "New Name" --apply` +- **Device inventory**: `zigporter list-devices --json`, `zigporter list-z2m --json` (requires Z2M config) +- **Device inspection**: `zigporter inspect "Device Name" --json`, `zigporter inspect sensor.entity_id --json` +- **Stale device management**: `zigporter stale "Device" --action remove`, `zigporter stale "Device" --action ignore` +- **Post-migration cleanup**: `zigporter fix-device "Device" --apply` +- **Connectivity check**: `zigporter check` +- **ZHA export**: `zigporter export --output devices.json` +- **Mesh visualization**: `zigporter network-map --format table` (terminal), `zigporter network-map --output mesh.svg` (SVG file) + +**Output format**: Use `--json` on listing/inspect commands for structured output (ideal for AI parsing). Rename commands output diffs and confirmation text. + +zigporter is pre-authenticated via the Supervisor token. Z2M commands (`list-z2m`, `network-map --backend z2m`) require Z2M URL configuration in the add-on settings. + +**Important limitations**: +- `rename-entity` / `rename-device` do NOT patch Jinja2 template expressions (e.g. `{{ states('old.id') }}`). A warning is printed listing affected files — inform the user these need manual review after renaming. +- The `migrate` command is inherently interactive (requires physical device actions) and must NOT be used by AI agents. +- Dry-run is the default for renames — always preview before using `--apply`. + + +``` + + Usage: zigporter [OPTIONS] COMMAND [ARGS]... + + Migrate Zigbee devices between ZHA and Zigbee2MQTT. Supports both ZHA → Z2M + (default) and Z2M → ZHA (--direction z2m-to-zha). + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +│ --version -v Show version and exit. │ +│ --install-completion Install completion for the current shell. │ +│ --show-completion Show completion for the current shell, to │ +│ copy it or customize the installation. │ +│ --help -h Show this message and exit. │ +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────╮ +│ setup Create or update the configuration file in the zigporter │ +│ config directory. │ +│ check Verify that all requirements are in place before migrating. │ +│ export Export current ZHA devices, entities, areas, and automation │ +│ references to JSON. │ +│ export-z2m Export current Z2M devices, entities, areas, and automation │ +│ references to JSON. │ +│ list-z2m List all devices currently paired with Zigbee2MQTT. │ +│ list-devices List all Home Assistant devices across all integrations. │ +│ migrate Interactive wizard to migrate devices between ZHA and │ +│ Zigbee2MQTT. │ +│ inspect Show all automations, scripts, scenes, and dashboard cards │ +│ that depend on a device. │ +│ rename-entity Rename an entity ID and update all references in automations, │ +│ scripts, scenes, and dashboards. │ +│ rename-device Rename a device and cascade the change to all its entities, │ +│ automations, scripts, scenes, and dashboards. │ +│ stale Identify and manage offline/stale devices across all │ +│ integrations. │ +│ fix-device Remove stale ZHA device entries left behind after migration │ +│ to Zigbee2MQTT. │ +│ network-map Show Zigbee mesh topology with signal strength (LQI) for each │ +│ device. │ +╰──────────────────────────────────────────────────────────────────────────────╯ +``` + + +**Use zigporter CLI when:** renaming entities/devices with cascade updates, inspecting Zigbee devices across integrations, cleaning up stale or post-migration devices, visualizing the Zigbee mesh + +### 5. Internal Directories (OFF-LIMITS) +Home Assistant manages internal state in directories like `.storage/`. These are: +- Not designed for direct access +- Subject to change without notice +- Potentially dangerous to modify + +**Never access internal directories directly - use configuration files or MCP tools instead.** + +## RESTRICTED: Internal Home Assistant Directories + +**NEVER read, modify, or directly interact with these internal directories:** + +| Directory | Contains | Use Instead | +|-----------|----------|-------------| +| `.storage/` | Entity/device/area registries, auth, system state | MCP: `get_devices`, `get_areas`, `get_entity_details` | +| `.cloud/` | Home Assistant Cloud state | N/A - managed by HA Cloud | +| `deps/` | Python dependency cache | N/A - managed by HA Core | +| `tts/` | Text-to-speech cache | N/A - managed by TTS integration | +| `home-assistant_v2.db` | History SQLite database | MCP: `get_history`, `get_logbook` | +| `home-assistant.log` | Raw system logs | MCP: `get_error_log` | + +These contain internal Home Assistant state that: +1. Is managed exclusively by Home Assistant core +2. Can corrupt your installation if modified incorrectly +3. May be overwritten by Home Assistant at any time +4. Has no stable schema or format guarantees + +**For information that seems to require internal access, there is always a proper alternative:** +- Need entity details? -> Read configuration files OR use `get_entity_details` +- Need device info? -> Use `get_devices` MCP tool +- Need to check history? -> Use `get_history` MCP tool +- Need to see errors? -> Use `get_error_log` MCP tool + +## File Structure Knowledge + +### Configuration Files (Primary Interface - Read/Write with User Approval) +These are the user-facing configuration files - the primary way to define Home Assistant behavior: + +- `configuration.yaml` - Main configuration file +- `automations.yaml` - Automation definitions (if using UI or split config) +- `scripts.yaml` - Script definitions +- `scenes.yaml` - Scene definitions +- `secrets.yaml` - Sensitive values (NEVER commit or expose) +- `customize.yaml` - Entity customizations +- `groups.yaml` - Group definitions +- `packages/` - Package-based configuration splits +- `blueprints/` - Automation and script blueprints +- `custom_components/` - Custom integrations (HACS or manual) +- `www/` - Static files served at /local/ +- `themes/` - Custom themes +- `*.yaml` in root - Any user-created YAML configuration + +**These files are designed for editing** and are equally valid as MCP tools for research and changes. + +### Internal Directories (OFF-LIMITS - Never Access Directly) +- `.storage/` - Internal registries and state (use MCP tools) +- `.cloud/` - Cloud authentication (managed by HA) +- `deps/` - Python dependencies (managed by HA) +- `tts/` - TTS cache (managed by HA) +- `__pycache__/` - Python bytecode (managed by Python) +- `home-assistant_v2.db` - History database (use MCP `get_history`) +- `home-assistant.log` - Logs (use MCP `get_error_log`) + +## Working with YAML on the Command Line (`yq`) + +To read, query, or convert YAML from the shell, use **`yq`** (the mikefarah/Go tool, pre-installed on `PATH`). It is the correct tool because it tolerates Home Assistant's custom tags — `!include`, `!secret`, `!env_var`, `!input`, and the `!include_dir_*` family. + +**Do NOT reach for `python3 -c "import yaml"` (PyYAML) or Ruby's YAML for HA config** — both crash with a constructor error on the very first `!include`/`!secret`, because those tags are Home Assistant extensions, not standard YAML. `yq` parses them with no setup. + +### Read / query (always safe — never errors on HA tags) + +``` +yq '.homeassistant.latitude' configuration.yaml # Print a nested value +yq '.automation | tag' configuration.yaml # Inspect the tag itself -> !include +yq 'keys' configuration.yaml # List top-level keys +yq -o=json '.' configuration.yaml | jq '.sensor' # Convert to JSON to pipe into jq +``` + +Note: output and JSON conversion strip the tag — `!secret home_latitude` prints as `home_latitude`. Use `| tag` when you need to see the tag. Never round-trip a file *through* JSON and back; that permanently loses every `!include`/`!secret`. + +### Writing / editing + +Prefer the sanctioned write path: **`write_config_safe`** (MCP — validates, backs up, and blocks accidental content loss) or read the full file and use the editor. Reserve `yq -i` for quick, low-risk edits, and only with these two caveats in mind: + +1. **A custom tag sticks to the value you overwrite.** `yq -i '.homeassistant.latitude = 52.37'` on a `!secret`-tagged node produces the corrupt `latitude: !secret 52.37`. When replacing a tagged value, reset the tag in the *same* expression: + ``` + yq -i '(.homeassistant.latitude tag = "") | .homeassistant.latitude = 52.37' configuration.yaml + ``` + To *add* a secret reference, set the tag explicitly: `yq -i '.http.api_key = "my_api_key" | .http.api_key tag = "!secret"' configuration.yaml` +2. **`yq -i` strips blank separator lines** (and collapses inline-comment spacing) across the whole file. No data is lost, but diffs are noisier. When a clean, minimal diff matters, use the editor instead. + +### Validation is not a syntax check + +`yq` only confirms YAML *parses*. To validate a Home Assistant *configuration* (resolving `!include`/`!secret` and checking integration schemas), use `check_config_syntax` / `write_config_safe` (MCP), not `yq`. + +## YAML Style Guide (MANDATORY) + +All YAML written or modified MUST follow the official Home Assistant YAML Style Guide. +Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/ + +A Prettier formatter is configured for this environment and will auto-format files on save. +However, Prettier only enforces a subset of the rules below. You are responsible for following +ALL rules, especially those Prettier cannot enforce (marked with *). + +### Indentation + +2 spaces. Tabs are forbidden. + +```yaml +# Good +example: + one: 1 + +# Bad +example: + bad: 2 +``` + +### Booleans * + +Only `true` and `false` in lowercase. Never use `Yes`, `No`, `On`, `Off`, `TRUE`, etc. + +```yaml +# Good +one: true +two: false + +# Bad +one: True +two: on +three: yes +``` + +### Strings + +Double quotes for strings. Single quotes are not allowed. + +```yaml +# Good +example: "Hi there!" + +# Bad +example: 'Hi there!' +``` + +**Exceptions** (no quotes needed): entity IDs, area IDs, device IDs, platform types, +trigger types, condition types, action names, device classes, event names, attribute names, +and values from a fixed set of options (e.g., `mode`). + +```yaml +# Good +actions: + - action: light.turn_on + target: + entity_id: light.living_room + area_id: living_room + data: + message: "Hello!" + transition: 10 + +# Bad - don't quote entity IDs and action names +actions: + - action: "light.turn_on" + target: + entity_id: "light.living_room" +``` + +### Sequences (Lists) * + +Use block style. Flow style `[1, 2, 3]` must not be used. +Block sequences must be indented under their key. + +```yaml +# Good +options: + - 1 + - 2 + - 3 + +# Bad +options: [1, 2, 3] + +# Bad - not indented under key +options: +- 1 +- 2 +``` + +### Mappings * + +Block style only. Flow style `{ key: val }` must not be used. + +```yaml +# Good +example: + one: 1 + two: 2 + +# Bad +example: { one: 1, two: 2 } +``` + +### Null Values * + +Use implicit null (just `key:` with no value). Never use `null` or `~`. + +```yaml +# Good +initial: + +# Bad +initial: null +initial: ~ +``` + +### Comments + +Capitalized, with a space after `#`, indented to match current level. + +```yaml +# Good +example: + # This is a comment + one: true + +# Bad +example: +# Comment at wrong indent + #Missing space + #lowercase start + one: true +``` + +### Multiline Strings + +Use literal `|` (preserves newlines) or folded `>` (joins lines) block scalars. +Avoid `\n` in strings. Prefer no-chomp (`|`, `>`) unless you need strip (`|-`, `>-`). + +```yaml +# Good +message: | + Hello! + This is a multiline + notification message. + +# Good - folded +description: > + This is a long description that + will be joined into a single line. + +# Bad +message: "Hello!\nThis is a multiline\nnotification message.\n" +``` + +### Templates + +Double quotes outside, single quotes inside. Use `states()` and `state_attr()` +helpers, not direct state object access. Split long templates across multiple lines. + +```yaml +# Good +value_template: "{{ states('sensor.temperature') }}" +attribute_template: "{{ state_attr('climate.living_room', 'temperature') }}" + +# Good - long template split with folded style +value_template: >- + {{ + is_state('sensor.bedroom_co_status', 'Ok') + and is_state('sensor.kitchen_co_status', 'Ok') + }} + +# Bad - single quotes outside +value_template: '{{ "some_value" == other_value }}' + +# Bad - direct state object access +value_template: "{{ states.sensor.temperature.state }}" +``` + +### Service Action Targets * + +Always use `target:` for entity/device/area targeting. Do not put `entity_id` at +the action level or inside `data:`. + +```yaml +# Good +actions: + - action: light.turn_on + target: + entity_id: light.living_room + +# Bad +actions: + - action: light.turn_on + entity_id: light.living_room + +# Bad +actions: + - action: light.turn_on + data: + entity_id: light.living_room +``` + +### Scalar vs List * + +If a property accepts both, use a scalar for single values. Do not wrap a single +value in a list. Do not use comma-separated strings. + +```yaml +# Good +entity_id: light.living_room +entity_id: + - light.living_room + - light.office + +# Bad - single value in a list +entity_id: + - light.living_room + +# Bad - comma separated +entity_id: "light.living_room, light.office" +``` + +### List of Mappings * + +When a property accepts a mapping or list of mappings (e.g., `actions`, `conditions`), +always use a list even for a single item. + +```yaml +# Good +actions: + - action: light.turn_on + target: + entity_id: light.living_room + +# Bad +actions: + action: light.turn_on + target: + entity_id: light.living_room +``` + +## Core Competencies + +### YAML Configuration +- Follow the YAML Style Guide above for ALL configuration changes +- Use anchors (`&name`) and aliases (`*name`) for DRY configurations +- Understand `!include`, `!include_dir_named`, `!include_dir_list`, `!include_dir_merge_named`, `!include_dir_merge_list` +- To read/query these files from the shell use `yq` (tag-tolerant); PyYAML/Ruby crash on HA tags — see "Working with YAML on the Command Line" +- Know when to use packages for organized configuration + +### Automations +- Write automations using both YAML and understand the UI format +- Understand triggers: state, time, event, webhook, mqtt, template, zone, device, etc. +- Understand conditions: state, numeric_state, time, template, zone, and, or, not +- Understand actions: service calls, delays, wait_template, choose, repeat, if/then/else +- Use trigger variables and automation context effectively +- Implement proper error handling with `continue_on_error` + +### Templates (Jinja2) +- Write efficient Jinja2 templates for Home Assistant +- Use filters: `float`, `int`, `round`, `timestamp_custom`, `regex_match`, etc. +- Use functions: `states()`, `state_attr()`, `is_state()`, `is_state_attr()`, `has_value()` +- Access trigger data: `trigger.to_state`, `trigger.from_state`, `trigger.entity_id` +- Handle unavailable/unknown states gracefully + +### Integrations +- Know common integrations and their configuration patterns +- Understand MQTT, REST, and template-based integrations +- Configure input_* helpers: input_boolean, input_number, input_select, input_text, input_datetime +- Set up utility_meter, statistics, and history_stats sensors + +### Lovelace Dashboards +- Write Lovelace YAML configurations +- Know standard cards and their options +- Understand conditional cards, custom cards, and card-mod +- Configure views, themes, and resources + +## Best Practices + +1. **Always validate** - Remind users to check configuration before restarting +2. **Use secrets** - Never hardcode sensitive data; use `!secret` references +3. **Backup first** - Suggest backups before major changes +4. **Incremental changes** - Make small, testable changes +5. **Comments** - Add YAML comments explaining complex logic +6. **Naming conventions** - Use consistent entity_id naming (e.g., `sensor.room_type_name`) + +## Safety Guidelines + +- NEVER expose or display contents of `secrets.yaml` +- NEVER include API keys, tokens, or passwords in responses +- NEVER make changes without explicit user approval +- NEVER access `.storage/`, `.cloud/`, or other internal directories +- NEVER attempt to modify Home Assistant's internal databases or registries +- NEVER parse internal JSON files for entity/device/area information +- ALWAYS prefer MCP tools for querying runtime state over internal file access +- ALWAYS use `call_service` through MCP rather than modifying state files +- WARN users before changes that require restart vs reload +- SUGGEST backing up files before major modifications +- CHECK configuration validity when possible +- ALWAYS confirm with user before writing, editing, or deleting any file + +## MCP Tools and Configuration Files + +You have two complementary interfaces for working with Home Assistant: + +### Configuration Files +Read and modify YAML files to understand and change Home Assistant's defined behavior: +- Review `automations.yaml` to understand existing automations +- Edit `configuration.yaml` to add new integrations +- Create new files in `packages/` for organized configuration +- Examine `custom_components/` for custom integration code + +### MCP Tools (When Available) +Query and interact with the running Home Assistant instance: +- `get_states`, `search_entities`, `get_home_context` - Current entity states and compact area/domain/entity context +- `call_service` - Control devices (with confirmation) +- `get_history`, `get_logbook` - Historical data +- `get_devices`, `get_areas` - Device and area registry info +- `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup** +- `validate_config` - Check configuration validity +- `get_error_log` - System errors and warnings +- `diagnose_entity` - Comprehensive entity troubleshooting +- `get_agent_capabilities` - OpenCode MCP capabilities and native HA `llm` / MCP readiness +- `get_ha_llm_development_guide` - Upstream references and starter template for native `/llm.py` providers +- `watch_firmware_update` - **Real-time firmware update monitoring** (ESPHome, WLED, Zigbee, etc.) +- `get_available_updates`, `update_component` - System update management +- `screenshot_url` - **Visual verification** of dashboards and UI pages (requires `screenshot_enabled` option) + +### Choosing the Right Approach + +| Task | Configuration Files | MCP Tools | hab CLI | zigporter CLI | +|------|---------------------|-----------|---------|---------------| +| Create/edit automations | Primary | **Write with `write_config_safe`** | `hab automation create` | N/A | +| Understand automation logic | Read YAML | Check state with `get_states` | `hab automation get` | N/A | +| Check current device state | Reference only | Primary (`get_home_context` for focused context) | `hab entity get` | N/A | +| Control devices | N/A | `call_service` | `hab action call` | N/A | +| Add new integrations | Primary | N/A | N/A | N/A | +| Troubleshoot issues | Review configs | `diagnose_entity`, `get_error_log` | `hab system health` | N/A | +| Check agent/LLM readiness | N/A | `get_agent_capabilities` | N/A | N/A | +| Develop native HA LLM tools | `custom_components/*/llm.py` | `get_ha_llm_development_guide` | N/A | N/A | +| Find entities | Grep YAML files | `search_entities` | `hab entity list --domain` | N/A | +| View history | N/A | `get_history` | N/A | N/A | +| **Manage dashboards** | Edit YAML | N/A | **`hab dashboard` (primary)** | N/A | +| **Verify UI changes** | N/A | **`screenshot_url`** | N/A | N/A | +| **Manage areas/floors** | N/A | `get_areas` (read-only) | **`hab area/floor` (CRUD)** | N/A | +| **Manage helpers** | N/A | N/A | **`hab helper` (primary)** | N/A | +| **Backups** | N/A | N/A | **`hab backup` (primary)** | N/A | +| **Blueprints** | N/A | N/A | **`hab blueprint` (primary)** | N/A | +| **Update firmware** | N/A | **`watch_firmware_update`** | N/A | N/A | +| **Check for updates** | N/A | `get_available_updates` | N/A | N/A | +| **Update HA Core/OS** | N/A | `update_component` | N/A | N/A | +| **Rename entity with cascade** | N/A | N/A | `hab entity update` (no cascade) | **`zigporter rename-entity` (primary)** | +| **Rename device with cascade** | N/A | N/A | `hab device update` (no cascade) | **`zigporter rename-device` (primary)** | +| **Inspect Zigbee device** | N/A | `get_entity_details` | `hab device list` | **`zigporter inspect --json` (cross-ref ZHA+Z2M+HA)** | +| **List Z2M devices** | N/A | N/A | N/A | **`zigporter list-z2m --json`** | +| **Clean up stale devices** | N/A | N/A | N/A | **`zigporter stale --action`** | +| **Fix post-migration entities** | N/A | N/A | N/A | **`zigporter fix-device --apply`** | +| **Zigbee mesh topology** | N/A | N/A | N/A | **`zigporter network-map`** | + +### Update Management (IMPORTANT) + +**For device firmware updates (ESPHome, WLED, Zigbee, etc.):** +Always use `watch_firmware_update` - it provides real-time visual progress: +``` +watch_firmware_update(entity_id="update.device_firmware", start_update=true) +``` +This single tool handles: starting the update, monitoring progress, and reporting results. + +**For system updates (Core, OS, Supervisor, Apps):** +``` +1. get_available_updates() -> Check what needs updating +2. update_component(component="core") -> Start update (returns job_id) +3. get_update_progress(job_id="...") -> Monitor progress +``` + +**Both approaches are valid and complementary.** Use configuration files for defining behavior and MCP tools for runtime interaction. + +## Documentation Currency + +Home Assistant releases monthly updates with new features, deprecations, and breaking changes. Your training data may be outdated. **Always verify configuration syntax against current documentation.** + +### Before Writing or Modifying Configuration + +**ALWAYS use these MCP tools before suggesting configuration changes:** + +1. **Check the installed version**: Use `get_config` to see what HA version is running +2. **Fetch current integration docs**: Use `get_integration_docs` to get current YAML syntax +3. **Check for breaking changes**: Use `get_breaking_changes` to see recent syntax changes +4. **Write config safely**: Use `write_config_safe` with `dry_run=true` to validate before presenting to user + +### Documentation Tools (MCP) + +| Tool | When to Use | +|------|-------------| +| `get_integration_docs` | Before writing ANY integration configuration | +| `get_breaking_changes` | When user reports config stopped working after update | +| `write_config_safe` | **ALWAYS use to write config files** — validates, blocks accidental content loss, and auto-restores on failure | +| `check_config_syntax` | Quick ad-hoc deprecation check (write_config_safe includes this automatically) | + +### Workflow Example + +When a user asks "Help me set up a template sensor": + +``` +1. get_config() -> Check HA version (e.g., 2024.12.1) +2. get_integration_docs("template") -> Get current syntax and examples +3. read_file(path) -> Read the EXISTING file content first +4. Draft configuration: include ALL existing content + new changes +5. write_config_safe(path, yaml, dry_run=true) -> Pre-validate everything +6. If errors: fix and repeat step 5 +7. Present validated config to user and get approval +8. write_config_safe(path, yaml) -> Write for real (auto backup + validation) +``` + +### Common Deprecation Patterns + +Be especially careful with these frequently-changed areas: +- **Template sensors/binary_sensors**: `platform: template` under `sensor:` is deprecated; use top-level `template:` +- **Entity configurations**: Many moved from YAML to UI-based config +- **Trigger-based templates**: Newer syntax preferred over legacy template sensors +- **Device triggers**: Syntax evolves with new device types +- **MQTT platform syntax**: `platform: mqtt` under domain keys is deprecated; use top-level `mqtt:` key +- **Direct state access**: `states.sensor.x.state` is fragile; use `states('sensor.x')` helper +- **entity_id in data**: Deprecated; use `target:` for service call targeting + +**When in doubt, fetch the docs. Never rely solely on training data for configuration syntax.** + +## Common Tasks + +### Creating an Automation +1. **Read the existing `automations.yaml` first** — you must include ALL existing automations in the final write +2. Understand the goal and identify trigger conditions +3. Determine required entities (search if MCP available) +4. Draft the automation YAML with clear comments +5. **Show the draft to the user and wait for approval** — the draft must contain all existing automations plus the new one +6. Only write the file after explicit user confirmation +7. Suggest testing approach + +> **WARNING:** Never write partial content to ANY config file. Always read the existing file first and include ALL existing content in your write. `write_config_safe` will block writes that would reduce list entries, remove top-level keys, or significantly shrink the file — but you should verify this yourself before presenting the draft to the user. + +### Troubleshooting +1. Check entity states and history (via MCP if available) +2. Review relevant configuration files +3. Check Home Assistant logs for errors +4. Identify common issues (unavailable entities, template errors, timing issues) +5. **Present findings and wait for user to request specific fixes** + +### Optimizing Configuration +1. Identify redundant or inefficient patterns +2. **Present recommendations to user** +3. Wait for user to approve specific changes +4. Implement only the changes the user explicitly approves diff --git a/esphome/.device-builder.json b/esphome/.device-builder.json index 7be9f2a..afe78fb 100644 --- a/esphome/.device-builder.json +++ b/esphome/.device-builder.json @@ -308,6 +308,657 @@ "mac_address": "88:13:BF:68:39:2C" }, "_firmware_jobs": [ + { + "job_id": "73f5241b3ffb", + "configuration": "esp-reterminal-e1001.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-20T05:17:58.469210+00:00", + "started_at": "2026-07-20T05:22:52.972291+00:00", + "completed_at": "2026-07-20T05:23:13.584838+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "8f5082f38895", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "8f5082f38895", + "configuration": "esp-reterminal-e1001.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-20T05:17:58.469169+00:00", + "started_at": "2026-07-20T05:17:58.470685+00:00", + "completed_at": "2026-07-20T05:22:52.968819+00:00", + "compile_started_at": "2026-07-20T05:18:19.282003+00:00", + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "ff7a76d6b100", + "configuration": "esp-bedside-panel.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-20T03:06:05.620406+00:00", + "started_at": "2026-07-20T03:07:18.093660+00:00", + "completed_at": "2026-07-20T03:07:31.446744+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "3d96967ebb2c", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "3d96967ebb2c", + "configuration": "esp-bedside-panel.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-20T03:06:05.620327+00:00", + "started_at": "2026-07-20T03:06:05.621988+00:00", + "completed_at": "2026-07-20T03:07:18.091028+00:00", + "compile_started_at": "2026-07-20T03:06:16.178933+00:00", + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": 79, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "c511d184709d", + "configuration": "esp-mainpowermonitor.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-12T01:37:15.723354+00:00", + "started_at": "2026-07-12T01:44:49.518560+00:00", + "completed_at": "2026-07-12T01:45:00.730111+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "c542f1a73d03", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "c542f1a73d03", + "configuration": "esp-mainpowermonitor.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-12T01:37:15.723308+00:00", + "started_at": "2026-07-12T01:37:15.727338+00:00", + "completed_at": "2026-07-12T01:44:49.514375+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "205aa20d27d9", + "configuration": "esp-3dprinterpow.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-12T01:21:26.661949+00:00", + "started_at": "2026-07-12T01:21:39.985214+00:00", + "completed_at": "2026-07-12T01:21:53.943583+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "c0f53cc7399f", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "c0f53cc7399f", + "configuration": "esp-3dprinterpow.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-12T01:21:26.661909+00:00", + "started_at": "2026-07-12T01:21:26.667812+00:00", + "completed_at": "2026-07-12T01:21:39.983557+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "bf25101412c9", + "configuration": "esp-laundrywashpow.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-12T01:17:16.232561+00:00", + "started_at": "2026-07-12T01:17:23.672495+00:00", + "completed_at": "2026-07-12T01:17:33.383403+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "8d23d2a51bab", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "8d23d2a51bab", + "configuration": "esp-laundrywashpow.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-12T01:17:16.232506+00:00", + "started_at": "2026-07-12T01:17:16.236207+00:00", + "completed_at": "2026-07-12T01:17:23.670787+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "3bb1df48a95f", + "configuration": "esp-laundrydrypow.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-12T01:00:14.372294+00:00", + "started_at": "2026-07-12T01:00:22.046535+00:00", + "completed_at": "2026-07-12T01:00:33.176688+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "27509d78b7fb", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "27509d78b7fb", + "configuration": "esp-laundrydrypow.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-12T01:00:14.372255+00:00", + "started_at": "2026-07-12T01:00:14.380689+00:00", + "completed_at": "2026-07-12T01:00:22.000411+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "c84bad09993e", + "configuration": "esp-espnow_test_transmitter1.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-11T06:19:41.016760+00:00", + "started_at": "2026-07-11T06:19:41.017132+00:00", + "completed_at": "2026-07-11T06:20:31.981498+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": 62, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "f8cba5f09ec4", + "configuration": "esp-espnow_test_receiver.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-11T06:05:52.755479+00:00", + "started_at": "2026-07-11T06:05:52.763102+00:00", + "completed_at": "2026-07-11T06:06:06.109766+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "e9d0ff65188c", + "configuration": "esp-espnow_test_receiver.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-11T06:04:14.625792+00:00", + "started_at": "2026-07-11T06:04:14.626184+00:00", + "completed_at": "2026-07-11T06:04:19.497213+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": 51, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "615b2a4df44a", + "configuration": "esp-bedrm2ceilingfan.yaml", + "job_type": "upload", + "status": "cancelled", + "created_at": "2026-07-10T11:51:50.763552+00:00", + "started_at": "2026-07-10T12:03:02.609338+00:00", + "completed_at": "2026-07-10T12:03:16.289469+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": null, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "433549183343", + "dependency_released": true, + "progress": 33, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "433549183343", + "configuration": "esp-bedrm2ceilingfan.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-10T11:51:50.763493+00:00", + "started_at": "2026-07-10T11:59:18.043534+00:00", + "completed_at": "2026-07-10T12:03:02.607226+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "1b037ff88296", + "configuration": "esp-bedrm2lights.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-10T11:51:39.897038+00:00", + "started_at": "2026-07-10T11:59:18.038715+00:00", + "completed_at": "2026-07-10T11:59:52.273519+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "1a57054d0890", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "1a57054d0890", + "configuration": "esp-bedrm2lights.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-10T11:51:39.897001+00:00", + "started_at": "2026-07-10T11:56:00.279013+00:00", + "completed_at": "2026-07-10T11:59:18.033814+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "b1e3fe59581f", + "configuration": "esp-bedrm1airfilter.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-07-10T11:51:31.367610+00:00", + "started_at": "2026-07-10T11:56:00.268461+00:00", + "completed_at": "2026-07-10T11:56:13.704304+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "19df32bad437", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "19df32bad437", + "configuration": "esp-bedrm1airfilter.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-10T11:51:31.367180+00:00", + "started_at": "2026-07-10T11:51:31.369196+00:00", + "completed_at": "2026-07-10T11:56:00.261931+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, { "job_id": "c14ec51954f2", "configuration": "esp-garagedbcontrols.yaml", @@ -316,9 +967,13 @@ "created_at": "2026-07-02T10:41:18.607560+00:00", "started_at": "2026-07-02T10:44:44.316653+00:00", "completed_at": "2026-07-02T10:45:51.002235+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "2dc9e67ec779", "dependency_released": true, @@ -331,7 +986,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "2dc9e67ec779", @@ -341,9 +998,13 @@ "created_at": "2026-07-02T10:41:18.607237+00:00", "started_at": "2026-07-02T10:41:18.608381+00:00", "completed_at": "2026-07-02T10:44:44.314950+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -356,7 +1017,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "c65ad7541adf", @@ -366,9 +1029,13 @@ "created_at": "2026-06-28T07:53:08.518532+00:00", "started_at": "2026-06-28T07:54:20.548241+00:00", "completed_at": "2026-06-28T07:54:38.040905+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "03cd448e0565", "dependency_released": true, @@ -381,7 +1048,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "03cd448e0565", @@ -391,9 +1060,13 @@ "created_at": "2026-06-28T07:53:08.518487+00:00", "started_at": "2026-06-28T07:53:08.519665+00:00", "completed_at": "2026-06-28T07:54:20.545513+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -406,7 +1079,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0297e2802d32", @@ -416,9 +1091,13 @@ "created_at": "2026-06-27T12:02:38.678051+00:00", "started_at": "2026-06-27T12:03:55.875104+00:00", "completed_at": "2026-06-27T12:04:05.294274+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "0c2f0bf9745b", "dependency_released": true, @@ -431,7 +1110,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0c2f0bf9745b", @@ -441,9 +1122,13 @@ "created_at": "2026-06-27T12:02:38.678007+00:00", "started_at": "2026-06-27T12:02:38.679168+00:00", "completed_at": "2026-06-27T12:03:55.867462+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -456,7 +1141,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8cba24a8323f", @@ -466,9 +1153,13 @@ "created_at": "2026-06-24T11:51:24.590439+00:00", "started_at": "2026-06-24T14:49:45.277115+00:00", "completed_at": "2026-06-24T14:49:58.460469+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "aaee722ad6dd", "dependency_released": true, @@ -481,7 +1172,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "aaee722ad6dd", @@ -491,9 +1184,13 @@ "created_at": "2026-06-24T11:51:24.590401+00:00", "started_at": "2026-06-24T14:46:33.264759+00:00", "completed_at": "2026-06-24T14:49:45.276214+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -506,7 +1203,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "53a002d67db6", @@ -516,9 +1215,13 @@ "created_at": "2026-06-24T11:51:19.266724+00:00", "started_at": "2026-06-24T14:46:33.233708+00:00", "completed_at": "2026-06-24T14:46:48.419507+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "60839421416c", "dependency_released": true, @@ -531,7 +1234,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "60839421416c", @@ -541,9 +1246,13 @@ "created_at": "2026-06-24T11:51:19.266687+00:00", "started_at": "2026-06-24T14:40:49.127363+00:00", "completed_at": "2026-06-24T14:46:33.232807+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -556,7 +1265,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8ed48b7ad6c3", @@ -566,9 +1277,13 @@ "created_at": "2026-06-24T11:51:12.884959+00:00", "started_at": "2026-06-24T14:40:49.123043+00:00", "completed_at": "2026-06-24T14:40:57.481911+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "4c65e42587fe", "dependency_released": true, @@ -581,7 +1296,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4c65e42587fe", @@ -591,9 +1308,13 @@ "created_at": "2026-06-24T11:51:12.884924+00:00", "started_at": "2026-06-24T14:37:27.302125+00:00", "completed_at": "2026-06-24T14:40:49.117805+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -606,7 +1327,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "68cd84ba5aad", @@ -616,9 +1339,13 @@ "created_at": "2026-06-24T11:51:00.766640+00:00", "started_at": "2026-06-24T14:37:27.294652+00:00", "completed_at": "2026-06-24T14:37:37.962712+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "212c5833f0b9", "dependency_released": true, @@ -631,7 +1358,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "212c5833f0b9", @@ -641,9 +1370,13 @@ "created_at": "2026-06-24T11:51:00.766603+00:00", "started_at": "2026-06-24T14:33:58.389533+00:00", "completed_at": "2026-06-24T14:37:27.293779+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -656,7 +1389,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "90f67f1bc3e5", @@ -666,9 +1401,13 @@ "created_at": "2026-06-24T11:50:53.964771+00:00", "started_at": "2026-06-24T14:33:58.383465+00:00", "completed_at": "2026-06-24T14:34:41.625168+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "6df38fbee969", "dependency_released": true, @@ -681,7 +1420,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6df38fbee969", @@ -691,9 +1432,13 @@ "created_at": "2026-06-24T11:50:53.964735+00:00", "started_at": "2026-06-24T14:27:15.391114+00:00", "completed_at": "2026-06-24T14:33:58.382588+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -706,7 +1451,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b757911776b0", @@ -716,9 +1463,13 @@ "created_at": "2026-06-24T11:50:42.761134+00:00", "started_at": "2026-06-24T14:27:15.374105+00:00", "completed_at": "2026-06-24T14:30:00.077877+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "3434cba94e0a", "dependency_released": true, @@ -731,7 +1482,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3434cba94e0a", @@ -741,9 +1494,13 @@ "created_at": "2026-06-24T11:50:42.761100+00:00", "started_at": "2026-06-24T14:21:34.999025+00:00", "completed_at": "2026-06-24T14:27:15.372011+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -756,7 +1513,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ff244f220584", @@ -766,9 +1525,13 @@ "created_at": "2026-06-24T11:50:26.904336+00:00", "started_at": "2026-06-24T14:21:34.990710+00:00", "completed_at": "2026-06-24T14:21:47.199113+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "7e346e30ea89", "dependency_released": true, @@ -781,7 +1544,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "7e346e30ea89", @@ -791,9 +1556,13 @@ "created_at": "2026-06-24T11:50:26.904301+00:00", "started_at": "2026-06-24T14:18:28.824836+00:00", "completed_at": "2026-06-24T14:21:34.989852+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -806,7 +1575,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "51cb8f902478", @@ -816,9 +1587,13 @@ "created_at": "2026-06-24T11:50:22.247734+00:00", "started_at": "2026-06-24T14:18:28.811710+00:00", "completed_at": "2026-06-24T14:18:41.791746+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "fa72d887a3cf", "dependency_released": true, @@ -831,7 +1606,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fa72d887a3cf", @@ -841,9 +1618,13 @@ "created_at": "2026-06-24T11:50:22.247700+00:00", "started_at": "2026-06-24T14:12:11.783583+00:00", "completed_at": "2026-06-24T14:18:28.810708+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -856,7 +1637,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "638a53e6dd3f", @@ -866,9 +1649,13 @@ "created_at": "2026-06-24T11:50:18.631210+00:00", "started_at": "2026-06-24T14:12:11.776002+00:00", "completed_at": "2026-06-24T14:12:19.828397+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "669b205142e0", "dependency_released": true, @@ -881,7 +1668,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "669b205142e0", @@ -891,9 +1680,13 @@ "created_at": "2026-06-24T11:50:18.631172+00:00", "started_at": "2026-06-24T14:09:02.649916+00:00", "completed_at": "2026-06-24T14:12:11.775011+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -906,7 +1699,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6e123487e7fc", @@ -916,9 +1711,13 @@ "created_at": "2026-06-24T11:50:13.994641+00:00", "started_at": "2026-06-24T14:09:02.642760+00:00", "completed_at": "2026-06-24T14:09:15.750170+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "5c6b973887d0", "dependency_released": true, @@ -931,7 +1730,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "5c6b973887d0", @@ -941,9 +1742,13 @@ "created_at": "2026-06-24T11:50:13.994509+00:00", "started_at": "2026-06-24T14:05:54.253995+00:00", "completed_at": "2026-06-24T14:09:02.640657+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -956,7 +1761,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "642ccb45d63b", @@ -966,9 +1773,13 @@ "created_at": "2026-06-24T11:50:07.540401+00:00", "started_at": "2026-06-24T14:05:54.233854+00:00", "completed_at": "2026-06-24T14:06:08.921514+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "5e627eee72b0", "dependency_released": true, @@ -981,7 +1792,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "5e627eee72b0", @@ -991,9 +1804,13 @@ "created_at": "2026-06-24T11:50:07.540367+00:00", "started_at": "2026-06-24T14:02:46.121117+00:00", "completed_at": "2026-06-24T14:05:54.232948+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1006,7 +1823,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "daa922805adb", @@ -1016,9 +1835,13 @@ "created_at": "2026-06-24T11:50:01.282558+00:00", "started_at": "2026-06-24T14:02:46.113929+00:00", "completed_at": "2026-06-24T14:02:57.254223+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "fa27bcc02d8c", "dependency_released": true, @@ -1031,7 +1854,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fa27bcc02d8c", @@ -1041,9 +1866,13 @@ "created_at": "2026-06-24T11:50:01.282519+00:00", "started_at": "2026-06-24T13:59:33.072305+00:00", "completed_at": "2026-06-24T14:02:46.113099+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1056,7 +1885,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "886301c4b21b", @@ -1066,9 +1897,13 @@ "created_at": "2026-06-24T11:49:37.943961+00:00", "started_at": "2026-06-24T13:59:33.020120+00:00", "completed_at": "2026-06-24T13:59:51.252411+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "a49c3e42cb3c", "dependency_released": true, @@ -1081,7 +1916,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "a49c3e42cb3c", @@ -1091,9 +1928,13 @@ "created_at": "2026-06-24T11:49:37.943926+00:00", "started_at": "2026-06-24T13:56:14.372922+00:00", "completed_at": "2026-06-24T13:59:33.019311+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1106,7 +1947,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "666f0ddea83d", @@ -1116,9 +1959,13 @@ "created_at": "2026-06-24T11:49:31.568783+00:00", "started_at": "2026-06-24T13:56:14.334876+00:00", "completed_at": "2026-06-24T13:56:23.433489+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "cf2d048802a7", "dependency_released": true, @@ -1131,7 +1978,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "cf2d048802a7", @@ -1141,9 +1990,13 @@ "created_at": "2026-06-24T11:49:31.568748+00:00", "started_at": "2026-06-24T13:53:08.883287+00:00", "completed_at": "2026-06-24T13:56:14.334094+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1156,57 +2009,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "3a39ea77d86c", - "configuration": "esp-mainpowermonitor.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-06-24T11:49:23.606288+00:00", - "started_at": "2026-06-24T13:53:08.865618+00:00", - "completed_at": "2026-06-24T13:53:19.995693+00:00", - "exit_code": 0, - "error": null, - "port": "OTA", - "new_name": "", - "depends_on": "7c3b20707daf", - "dependency_released": true, - "progress": 100, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "7c3b20707daf", - "configuration": "esp-mainpowermonitor.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-06-24T11:49:23.606253+00:00", - "started_at": "2026-06-24T13:49:57.766858+00:00", - "completed_at": "2026-06-24T13:53:08.862536+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "afea0839995d", @@ -1216,9 +2021,13 @@ "created_at": "2026-06-24T11:49:19.961424+00:00", "started_at": "2026-06-24T13:49:57.725897+00:00", "completed_at": "2026-06-24T13:50:08.731599+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "fb2291fee6b7", "dependency_released": true, @@ -1231,7 +2040,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fb2291fee6b7", @@ -1241,9 +2052,13 @@ "created_at": "2026-06-24T11:49:19.961388+00:00", "started_at": "2026-06-24T13:46:54.513145+00:00", "completed_at": "2026-06-24T13:49:57.725139+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1256,7 +2071,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "f30bfc04fd38", @@ -1266,9 +2083,13 @@ "created_at": "2026-06-24T11:49:15.113164+00:00", "started_at": "2026-06-24T13:46:54.483217+00:00", "completed_at": "2026-06-24T13:47:18.412770+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "af9f15ce746f", "dependency_released": true, @@ -1281,7 +2102,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "af9f15ce746f", @@ -1291,9 +2114,13 @@ "created_at": "2026-06-24T11:49:15.113124+00:00", "started_at": "2026-06-24T13:43:17.344179+00:00", "completed_at": "2026-06-24T13:46:54.482437+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1306,7 +2133,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "efbe53e29d59", @@ -1316,9 +2145,13 @@ "created_at": "2026-06-24T11:49:11.513335+00:00", "started_at": "2026-06-24T13:43:17.334665+00:00", "completed_at": "2026-06-24T13:43:27.002965+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "d0f177e01e51", "dependency_released": true, @@ -1331,7 +2164,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d0f177e01e51", @@ -1341,9 +2176,13 @@ "created_at": "2026-06-24T11:49:11.513301+00:00", "started_at": "2026-06-24T13:40:10.263119+00:00", "completed_at": "2026-06-24T13:43:17.331953+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1356,7 +2195,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4c142d0c46a9", @@ -1366,9 +2207,13 @@ "created_at": "2026-06-24T11:49:06.648781+00:00", "started_at": "2026-06-24T13:40:10.250472+00:00", "completed_at": "2026-06-24T13:40:20.003846+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "f6206629651f", "dependency_released": true, @@ -1381,7 +2226,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "f6206629651f", @@ -1391,9 +2238,13 @@ "created_at": "2026-06-24T11:49:06.648746+00:00", "started_at": "2026-06-24T13:37:05.295209+00:00", "completed_at": "2026-06-24T13:40:10.246833+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1406,7 +2257,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "e541b63a4c67", @@ -1416,9 +2269,13 @@ "created_at": "2026-06-24T11:49:01.951396+00:00", "started_at": "2026-06-24T13:37:05.288376+00:00", "completed_at": "2026-06-24T13:37:48.424049+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "4b02591939f6", "dependency_released": true, @@ -1431,7 +2288,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4b02591939f6", @@ -1441,9 +2300,13 @@ "created_at": "2026-06-24T11:49:01.951356+00:00", "started_at": "2026-06-24T13:30:29.664317+00:00", "completed_at": "2026-06-24T13:37:05.284076+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1456,7 +2319,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4b66b89852da", @@ -1466,9 +2331,13 @@ "created_at": "2026-06-24T11:48:58.347725+00:00", "started_at": "2026-06-24T13:30:29.652561+00:00", "completed_at": "2026-06-24T13:30:53.462265+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "88f8d1f0da59", "dependency_released": true, @@ -1481,7 +2350,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "88f8d1f0da59", @@ -1491,9 +2362,13 @@ "created_at": "2026-06-24T11:48:58.347690+00:00", "started_at": "2026-06-24T13:26:55.976678+00:00", "completed_at": "2026-06-24T13:30:29.650902+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1506,7 +2381,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4913c365f088", @@ -1516,9 +2393,13 @@ "created_at": "2026-06-24T11:48:54.824960+00:00", "started_at": "2026-06-24T13:26:55.864353+00:00", "completed_at": "2026-06-24T13:27:08.551827+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "6f92a48d7013", "dependency_released": true, @@ -1531,7 +2412,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6f92a48d7013", @@ -1541,9 +2424,13 @@ "created_at": "2026-06-24T11:48:54.824925+00:00", "started_at": "2026-06-24T13:23:52.117690+00:00", "completed_at": "2026-06-24T13:26:55.863523+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1556,7 +2443,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "9a607d0c2960", @@ -1566,9 +2455,13 @@ "created_at": "2026-06-24T11:48:48.151596+00:00", "started_at": "2026-06-24T13:23:52.085362+00:00", "completed_at": "2026-06-24T13:24:01.860495+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "fbf9a2d0d0d7", "dependency_released": true, @@ -1581,7 +2474,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fbf9a2d0d0d7", @@ -1591,9 +2486,13 @@ "created_at": "2026-06-24T11:48:48.151558+00:00", "started_at": "2026-06-24T13:20:46.872413+00:00", "completed_at": "2026-06-24T13:23:52.084235+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1606,7 +2505,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "922373607271", @@ -1616,9 +2517,13 @@ "created_at": "2026-06-24T11:48:44.080586+00:00", "started_at": "2026-06-24T13:20:46.865921+00:00", "completed_at": "2026-06-24T13:20:51.152578+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "d5db2045db77", "dependency_released": true, @@ -1631,7 +2536,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d5db2045db77", @@ -1641,9 +2548,13 @@ "created_at": "2026-06-24T11:48:44.080539+00:00", "started_at": "2026-06-24T13:17:41.934502+00:00", "completed_at": "2026-06-24T13:20:46.865208+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1656,7 +2567,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "282c7cdf96ca", @@ -1666,9 +2579,13 @@ "created_at": "2026-06-24T11:48:34.182203+00:00", "started_at": "2026-06-24T13:17:41.924878+00:00", "completed_at": "2026-06-24T13:17:50.823657+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "2a5226ea5609", "dependency_released": true, @@ -1681,7 +2598,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "2a5226ea5609", @@ -1691,9 +2610,13 @@ "created_at": "2026-06-24T11:48:34.182167+00:00", "started_at": "2026-06-24T13:14:33.437417+00:00", "completed_at": "2026-06-24T13:17:41.923902+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1706,7 +2629,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8762e1f19bb2", @@ -1716,9 +2641,13 @@ "created_at": "2026-06-24T11:48:29.562797+00:00", "started_at": "2026-06-24T13:14:33.432878+00:00", "completed_at": "2026-06-24T13:14:39.783888+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "fda47155414b", "dependency_released": true, @@ -1731,7 +2660,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fda47155414b", @@ -1741,9 +2672,13 @@ "created_at": "2026-06-24T11:48:29.562756+00:00", "started_at": "2026-06-24T13:12:00.119914+00:00", "completed_at": "2026-06-24T13:14:33.429361+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1756,7 +2691,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d47a37beda78", @@ -1766,9 +2703,13 @@ "created_at": "2026-06-24T11:48:26.017079+00:00", "started_at": "2026-06-24T13:12:00.108490+00:00", "completed_at": "2026-06-24T13:12:16.563464+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "6b1dadb7e32f", "dependency_released": true, @@ -1781,7 +2722,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6b1dadb7e32f", @@ -1791,9 +2734,13 @@ "created_at": "2026-06-24T11:48:26.017047+00:00", "started_at": "2026-06-24T13:06:26.503951+00:00", "completed_at": "2026-06-24T13:12:00.107474+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1806,7 +2753,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "762e61133aa3", @@ -1816,9 +2765,13 @@ "created_at": "2026-06-24T11:48:21.565814+00:00", "started_at": "2026-06-24T13:06:26.486919+00:00", "completed_at": "2026-06-24T13:06:36.118156+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "e5c661348557", "dependency_released": true, @@ -1831,7 +2784,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "e5c661348557", @@ -1841,9 +2796,13 @@ "created_at": "2026-06-24T11:48:21.565778+00:00", "started_at": "2026-06-24T13:03:14.390671+00:00", "completed_at": "2026-06-24T13:06:26.486073+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1856,7 +2815,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "aecda9b80725", @@ -1866,9 +2827,13 @@ "created_at": "2026-06-24T11:48:16.902742+00:00", "started_at": "2026-06-24T13:03:14.379936+00:00", "completed_at": "2026-06-24T13:03:32.613265+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "12b579662735", "dependency_released": true, @@ -1881,7 +2846,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "12b579662735", @@ -1891,9 +2858,13 @@ "created_at": "2026-06-24T11:48:16.902703+00:00", "started_at": "2026-06-24T12:57:43.789960+00:00", "completed_at": "2026-06-24T13:03:14.377278+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1906,7 +2877,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d2d3b330e2d6", @@ -1916,9 +2889,13 @@ "created_at": "2026-06-24T11:48:06.677575+00:00", "started_at": "2026-06-24T12:57:43.781432+00:00", "completed_at": "2026-06-24T12:57:52.144522+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "9bf3f1ea0c1b", "dependency_released": true, @@ -1931,7 +2908,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "9bf3f1ea0c1b", @@ -1941,9 +2920,13 @@ "created_at": "2026-06-24T11:48:06.677539+00:00", "started_at": "2026-06-24T12:54:25.676460+00:00", "completed_at": "2026-06-24T12:57:43.777736+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -1956,7 +2939,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ecb7fbae380d", @@ -1966,9 +2951,13 @@ "created_at": "2026-06-24T11:48:01.942230+00:00", "started_at": "2026-06-24T12:54:25.659385+00:00", "completed_at": "2026-06-24T12:54:36.106800+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "139efa28f4cf", "dependency_released": true, @@ -1981,7 +2970,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "139efa28f4cf", @@ -1991,9 +2982,13 @@ "created_at": "2026-06-24T11:48:01.942189+00:00", "started_at": "2026-06-24T12:50:56.804405+00:00", "completed_at": "2026-06-24T12:54:25.658460+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2006,7 +3001,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6b7ad893bea2", @@ -2016,9 +3013,13 @@ "created_at": "2026-06-24T11:47:58.041742+00:00", "started_at": "2026-06-24T12:50:56.795757+00:00", "completed_at": "2026-06-24T12:51:19.380365+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "1c6f7a8eac07", "dependency_released": true, @@ -2031,7 +3032,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "1c6f7a8eac07", @@ -2041,9 +3044,13 @@ "created_at": "2026-06-24T11:47:58.041707+00:00", "started_at": "2026-06-24T12:44:35.893753+00:00", "completed_at": "2026-06-24T12:50:56.794943+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2056,57 +3063,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "85284f418303", - "configuration": "esp-laundrywashpow.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-06-24T11:47:54.264858+00:00", - "started_at": "2026-06-24T12:44:35.885070+00:00", - "completed_at": "2026-06-24T12:44:46.649477+00:00", - "exit_code": 0, - "error": null, - "port": "OTA", - "new_name": "", - "depends_on": "79f39244030a", - "dependency_released": true, - "progress": 100, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "79f39244030a", - "configuration": "esp-laundrywashpow.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-06-24T11:47:54.264817+00:00", - "started_at": "2026-06-24T12:41:06.052163+00:00", - "completed_at": "2026-06-24T12:44:35.884284+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b91948d49474", @@ -2116,9 +3075,13 @@ "created_at": "2026-06-24T11:47:50.317968+00:00", "started_at": "2026-06-24T12:41:06.045398+00:00", "completed_at": "2026-06-24T12:41:18.031668+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "d07f330e4582", "dependency_released": true, @@ -2131,7 +3094,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d07f330e4582", @@ -2141,9 +3106,13 @@ "created_at": "2026-06-24T11:47:50.317934+00:00", "started_at": "2026-06-24T12:37:59.664843+00:00", "completed_at": "2026-06-24T12:41:06.044558+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2156,7 +3125,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "a0f78a0df422", @@ -2166,9 +3137,13 @@ "created_at": "2026-06-24T11:47:43.409097+00:00", "started_at": "2026-06-24T12:37:59.646931+00:00", "completed_at": "2026-06-24T12:38:09.577783+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "065cad4276c7", "dependency_released": true, @@ -2181,7 +3156,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "065cad4276c7", @@ -2191,9 +3168,13 @@ "created_at": "2026-06-24T11:47:43.409061+00:00", "started_at": "2026-06-24T12:34:56.082119+00:00", "completed_at": "2026-06-24T12:37:59.645508+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2206,57 +3187,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "440172dc7468", - "configuration": "esp-laundrydrypow.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-06-24T11:47:39.573369+00:00", - "started_at": "2026-06-24T12:34:56.070866+00:00", - "completed_at": "2026-06-24T12:35:07.236118+00:00", - "exit_code": 0, - "error": null, - "port": "OTA", - "new_name": "", - "depends_on": "ac668456639c", - "dependency_released": true, - "progress": 100, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "ac668456639c", - "configuration": "esp-laundrydrypow.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-06-24T11:47:39.573323+00:00", - "started_at": "2026-06-24T12:31:29.677109+00:00", - "completed_at": "2026-06-24T12:34:56.066961+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "33798c73cf2c", @@ -2266,9 +3199,13 @@ "created_at": "2026-06-24T11:47:36.179864+00:00", "started_at": "2026-06-24T12:31:29.669766+00:00", "completed_at": "2026-06-24T12:31:39.780686+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "4125f3fcdb4d", "dependency_released": true, @@ -2281,7 +3218,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4125f3fcdb4d", @@ -2291,9 +3230,13 @@ "created_at": "2026-06-24T11:47:36.179828+00:00", "started_at": "2026-06-24T12:27:53.334522+00:00", "completed_at": "2026-06-24T12:31:29.668823+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2306,7 +3249,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "78da318f0029", @@ -2316,9 +3261,13 @@ "created_at": "2026-06-24T11:47:31.918721+00:00", "started_at": "2026-06-24T12:27:53.327805+00:00", "completed_at": "2026-06-24T12:28:05.606224+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "48eb5fb2e8b8", "dependency_released": true, @@ -2331,7 +3280,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "48eb5fb2e8b8", @@ -2341,9 +3292,13 @@ "created_at": "2026-06-24T11:47:31.918683+00:00", "started_at": "2026-06-24T12:24:46.286660+00:00", "completed_at": "2026-06-24T12:27:53.325167+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2356,7 +3311,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "29fc5aee6a81", @@ -2366,9 +3323,13 @@ "created_at": "2026-06-24T11:47:28.042637+00:00", "started_at": "2026-06-24T12:24:46.281700+00:00", "completed_at": "2026-06-24T12:24:57.954485+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "d8fe298d0c40", "dependency_released": true, @@ -2381,7 +3342,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d8fe298d0c40", @@ -2391,9 +3354,13 @@ "created_at": "2026-06-24T11:47:28.042604+00:00", "started_at": "2026-06-24T12:21:33.286542+00:00", "completed_at": "2026-06-24T12:24:46.272915+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2406,7 +3373,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "38f95f64be26", @@ -2416,9 +3385,13 @@ "created_at": "2026-06-24T11:47:20.272166+00:00", "started_at": "2026-06-24T12:21:33.280890+00:00", "completed_at": "2026-06-24T12:21:51.738028+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "4ec02a1f56e1", "dependency_released": true, @@ -2431,7 +3404,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4ec02a1f56e1", @@ -2441,9 +3416,13 @@ "created_at": "2026-06-24T11:47:20.272127+00:00", "started_at": "2026-06-24T12:17:50.130204+00:00", "completed_at": "2026-06-24T12:21:33.275224+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2456,7 +3435,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "e6e516c0fbba", @@ -2466,9 +3447,13 @@ "created_at": "2026-06-24T11:47:14.725551+00:00", "started_at": "2026-06-24T12:17:50.111942+00:00", "completed_at": "2026-06-24T12:18:05.471774+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "df1dde74008a", "dependency_released": true, @@ -2481,7 +3466,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "df1dde74008a", @@ -2491,9 +3478,13 @@ "created_at": "2026-06-24T11:47:14.725515+00:00", "started_at": "2026-06-24T12:11:23.272923+00:00", "completed_at": "2026-06-24T12:17:50.110871+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2506,7 +3497,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "04d599695edf", @@ -2516,9 +3509,13 @@ "created_at": "2026-06-24T11:47:02.335395+00:00", "started_at": "2026-06-24T12:07:59.215593+00:00", "completed_at": "2026-06-24T12:08:09.865560+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "6f51f54884ef", "dependency_released": true, @@ -2531,7 +3528,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6f51f54884ef", @@ -2541,9 +3540,13 @@ "created_at": "2026-06-24T11:47:02.335352+00:00", "started_at": "2026-06-24T12:04:15.409100+00:00", "completed_at": "2026-06-24T12:07:59.205973+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2556,7 +3559,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0943b35ac141", @@ -2566,9 +3571,13 @@ "created_at": "2026-06-24T11:46:56.500037+00:00", "started_at": "2026-06-24T12:04:15.404021+00:00", "completed_at": "2026-06-24T12:04:27.154469+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "7e6b0407152d", "dependency_released": true, @@ -2581,7 +3590,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "7e6b0407152d", @@ -2591,9 +3602,13 @@ "created_at": "2026-06-24T11:46:56.500001+00:00", "started_at": "2026-06-24T12:00:27.313340+00:00", "completed_at": "2026-06-24T12:04:15.400840+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2606,7 +3621,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "05fe953ff669", @@ -2616,9 +3633,13 @@ "created_at": "2026-06-24T11:46:36.866914+00:00", "started_at": "2026-06-24T12:00:27.305534+00:00", "completed_at": "2026-06-24T12:00:34.502981+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "76b3825cd83a", "dependency_released": true, @@ -2631,7 +3652,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "76b3825cd83a", @@ -2641,9 +3664,13 @@ "created_at": "2026-06-24T11:46:36.866877+00:00", "started_at": "2026-06-24T11:56:49.211138+00:00", "completed_at": "2026-06-24T12:00:27.303623+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2656,7 +3683,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "313b79efb9ae", @@ -2666,9 +3695,13 @@ "created_at": "2026-06-24T11:46:31.032791+00:00", "started_at": "2026-06-24T11:56:49.180324+00:00", "completed_at": "2026-06-24T11:56:59.211826+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "1553d32caf13", "dependency_released": true, @@ -2681,7 +3714,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "1553d32caf13", @@ -2691,9 +3726,13 @@ "created_at": "2026-06-24T11:46:31.032752+00:00", "started_at": "2026-06-24T11:53:26.950917+00:00", "completed_at": "2026-06-24T11:56:49.178172+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2706,7 +3745,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "23a2ac0ba238", @@ -2716,9 +3757,13 @@ "created_at": "2026-06-24T11:46:25.636327+00:00", "started_at": "2026-06-24T11:53:26.942313+00:00", "completed_at": "2026-06-24T11:53:35.541508+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "5fa63cb048e0", "dependency_released": true, @@ -2731,7 +3776,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "5fa63cb048e0", @@ -2741,9 +3788,13 @@ "created_at": "2026-06-24T11:46:25.636291+00:00", "started_at": "2026-06-24T11:49:58.515206+00:00", "completed_at": "2026-06-24T11:53:26.938680+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2756,7 +3807,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "c0441f500aba", @@ -2766,9 +3819,13 @@ "created_at": "2026-06-24T11:46:20.681040+00:00", "started_at": "2026-06-24T11:49:58.512648+00:00", "completed_at": "2026-06-24T11:50:09.188769+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "3187da75fea4", "dependency_released": true, @@ -2781,7 +3838,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3187da75fea4", @@ -2791,9 +3850,13 @@ "created_at": "2026-06-24T11:46:20.680999+00:00", "started_at": "2026-06-24T11:46:20.681902+00:00", "completed_at": "2026-06-24T11:49:58.505113+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2806,7 +3869,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "639845a1773f", @@ -2816,9 +3881,13 @@ "created_at": "2026-06-24T11:13:49.710559+00:00", "started_at": "2026-06-24T11:38:36.426784+00:00", "completed_at": "2026-06-24T11:38:44.418253+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "8a7b6cef6f2b", "dependency_released": true, @@ -2831,7 +3900,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8a7b6cef6f2b", @@ -2841,9 +3912,13 @@ "created_at": "2026-06-24T11:13:49.710521+00:00", "started_at": "2026-06-24T11:35:05.238325+00:00", "completed_at": "2026-06-24T11:38:36.424723+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2856,7 +3931,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "9e5cf4a7265e", @@ -2866,9 +3943,13 @@ "created_at": "2026-06-24T11:13:45.110766+00:00", "started_at": "2026-06-24T11:35:05.237413+00:00", "completed_at": "2026-06-24T11:35:13.309250+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "faa9c0ffec61", "dependency_released": true, @@ -2881,7 +3962,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "faa9c0ffec61", @@ -2891,9 +3974,13 @@ "created_at": "2026-06-24T11:13:45.110727+00:00", "started_at": "2026-06-24T11:31:39.910651+00:00", "completed_at": "2026-06-24T11:35:05.231744+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2906,7 +3993,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3cca3877dcc7", @@ -2916,9 +4005,13 @@ "created_at": "2026-06-24T11:10:25.652877+00:00", "started_at": "2026-06-24T11:31:39.907294+00:00", "completed_at": "2026-06-24T11:31:50.903714+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "2adabd220e57", "dependency_released": true, @@ -2931,7 +4024,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "2adabd220e57", @@ -2941,9 +4036,13 @@ "created_at": "2026-06-24T11:10:25.652841+00:00", "started_at": "2026-06-24T11:28:16.222765+00:00", "completed_at": "2026-06-24T11:31:39.901914+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -2956,107 +4055,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "003f10b44b95", - "configuration": "esp-bedrm2ceilingfan.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-06-24T11:10:20.419189+00:00", - "started_at": "2026-06-24T11:28:16.167504+00:00", - "completed_at": "2026-06-24T11:28:34.562896+00:00", - "exit_code": 0, - "error": null, - "port": "OTA", - "new_name": "", - "depends_on": "cf07cfcb7b55", - "dependency_released": true, - "progress": 100, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "cf07cfcb7b55", - "configuration": "esp-bedrm2ceilingfan.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-06-24T11:10:20.419149+00:00", - "started_at": "2026-06-24T11:24:29.734739+00:00", - "completed_at": "2026-06-24T11:28:16.161732+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "f053b649b4e4", - "configuration": "esp-bedrm1airfilter.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-06-24T11:10:09.997262+00:00", - "started_at": "2026-06-24T11:24:29.726369+00:00", - "completed_at": "2026-06-24T11:24:40.162683+00:00", - "exit_code": 0, - "error": null, - "port": "OTA", - "new_name": "", - "depends_on": "855efee6affc", - "dependency_released": true, - "progress": 100, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "855efee6affc", - "configuration": "esp-bedrm1airfilter.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-06-24T11:10:09.997219+00:00", - "started_at": "2026-06-24T11:20:56.586662+00:00", - "completed_at": "2026-06-24T11:24:29.724626+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "01a7af7cadbc", @@ -3066,9 +4067,13 @@ "created_at": "2026-06-24T11:10:05.002955+00:00", "started_at": "2026-06-24T11:20:56.582567+00:00", "completed_at": "2026-06-24T11:21:08.371061+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "8303199b7ced", "dependency_released": true, @@ -3081,7 +4086,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8303199b7ced", @@ -3091,9 +4098,13 @@ "created_at": "2026-06-24T11:10:05.002897+00:00", "started_at": "2026-06-24T11:17:16.744945+00:00", "completed_at": "2026-06-24T11:20:56.578496+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3106,7 +4117,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "539048ba629b", @@ -3116,9 +4129,13 @@ "created_at": "2026-06-24T11:09:57.679920+00:00", "started_at": "2026-06-24T11:17:16.740743+00:00", "completed_at": "2026-06-24T11:17:25.826798+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "58395c3dab36", "dependency_released": true, @@ -3131,7 +4148,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "58395c3dab36", @@ -3141,9 +4160,13 @@ "created_at": "2026-06-24T11:09:57.679481+00:00", "started_at": "2026-06-24T11:13:38.968127+00:00", "completed_at": "2026-06-24T11:17:16.738402+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3156,57 +4179,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "0d8a0b2be69a", - "configuration": "esp-3dprinterpow.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-06-24T11:09:52.251760+00:00", - "started_at": "2026-06-24T11:13:38.962444+00:00", - "completed_at": "2026-06-24T11:13:53.170165+00:00", - "exit_code": 0, - "error": null, - "port": "OTA", - "new_name": "", - "depends_on": "4b8fea58e1a6", - "dependency_released": true, - "progress": 100, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "4b8fea58e1a6", - "configuration": "esp-3dprinterpow.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-06-24T11:09:52.251709+00:00", - "started_at": "2026-06-24T11:09:52.252461+00:00", - "completed_at": "2026-06-24T11:13:38.960444+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "dfabed27adf5", @@ -3216,9 +4191,13 @@ "created_at": "2026-06-12T10:19:14.926585+00:00", "started_at": "2026-06-12T10:22:25.692593+00:00", "completed_at": "2026-06-12T10:22:49.018883+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "b03c87e5954e", "dependency_released": true, @@ -3231,7 +4210,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b03c87e5954e", @@ -3241,9 +4222,13 @@ "created_at": "2026-06-12T10:19:14.926538+00:00", "started_at": "2026-06-12T10:20:08.467305+00:00", "completed_at": "2026-06-12T10:22:25.683155+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3256,7 +4241,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0dc6aca98f76", @@ -3266,9 +4253,13 @@ "created_at": "2026-06-12T10:17:56.852471+00:00", "started_at": "2026-06-12T10:20:08.462596+00:00", "completed_at": "2026-06-12T10:20:24.266523+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "b299a38f73d6", "dependency_released": true, @@ -3281,7 +4272,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b299a38f73d6", @@ -3291,9 +4284,13 @@ "created_at": "2026-06-12T10:17:56.852426+00:00", "started_at": "2026-06-12T10:17:56.853172+00:00", "completed_at": "2026-06-12T10:20:08.461368+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3306,7 +4303,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0a4602631e6e", @@ -3316,9 +4315,13 @@ "created_at": "2026-06-12T08:31:00.645286+00:00", "started_at": "2026-06-12T08:31:00.645530+00:00", "completed_at": "2026-06-12T08:31:07.627462+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": -15, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3331,7 +4334,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0b7b51e5af2a", @@ -3341,9 +4346,13 @@ "created_at": "2026-06-12T05:37:12.378996+00:00", "started_at": "2026-06-12T05:40:20.938981+00:00", "completed_at": "2026-06-12T05:40:40.151165+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "4bb434e78efe", "dependency_released": true, @@ -3356,7 +4365,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4bb434e78efe", @@ -3366,9 +4377,13 @@ "created_at": "2026-06-12T05:37:12.378923+00:00", "started_at": "2026-06-12T05:37:12.379876+00:00", "completed_at": "2026-06-12T05:40:20.934723+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3381,7 +4396,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "af97c828d58a", @@ -3391,9 +4408,13 @@ "created_at": "2026-06-05T04:25:30.974561+00:00", "started_at": "2026-06-05T04:25:44.710401+00:00", "completed_at": "2026-06-05T04:25:58.902563+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "bd7bbeb99660", "dependency_released": true, @@ -3406,7 +4427,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "bd7bbeb99660", @@ -3416,9 +4439,13 @@ "created_at": "2026-06-05T04:25:30.974521+00:00", "started_at": "2026-06-05T04:25:30.977331+00:00", "completed_at": "2026-06-05T04:25:44.702151+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3431,7 +4458,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "c7846a63cc3b", @@ -3441,9 +4470,13 @@ "created_at": "2026-06-01T12:03:34.253854+00:00", "started_at": "2026-06-01T12:03:34.254126+00:00", "completed_at": "2026-06-01T12:06:01.062909+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3456,7 +4489,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "806186443ff2", @@ -3466,9 +4501,13 @@ "created_at": "2026-06-01T11:21:42.771723+00:00", "started_at": "2026-06-01T11:21:42.772227+00:00", "completed_at": "2026-06-01T11:21:53.743481+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": -15, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3481,7 +4520,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "80ef4ab140f9", @@ -3491,9 +4532,13 @@ "created_at": "2026-06-01T04:19:41.470443+00:00", "started_at": "2026-06-01T08:24:58.246476+00:00", "completed_at": "2026-06-01T08:27:25.931721+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3506,7 +4551,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "a8b8f023de6a", @@ -3516,9 +4563,13 @@ "created_at": "2026-06-01T04:19:41.468291+00:00", "started_at": "2026-06-01T08:22:22.841716+00:00", "completed_at": "2026-06-01T08:24:58.242760+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3531,7 +4582,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "841b61799089", @@ -3541,9 +4594,13 @@ "created_at": "2026-06-01T04:19:41.466041+00:00", "started_at": "2026-06-01T08:19:50.594744+00:00", "completed_at": "2026-06-01T08:22:22.835917+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3556,7 +4613,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "26895803d863", @@ -3566,9 +4625,13 @@ "created_at": "2026-06-01T04:19:41.462994+00:00", "started_at": "2026-06-01T08:17:16.278510+00:00", "completed_at": "2026-06-01T08:19:50.591115+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3581,7 +4644,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "401e802b42fc", @@ -3591,9 +4656,13 @@ "created_at": "2026-06-01T04:19:41.459431+00:00", "started_at": "2026-06-01T08:12:57.414682+00:00", "completed_at": "2026-06-01T08:17:16.274357+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3606,7 +4675,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "9bb91caf6ea7", @@ -3616,9 +4687,13 @@ "created_at": "2026-06-01T04:19:41.456943+00:00", "started_at": "2026-06-01T08:10:33.258386+00:00", "completed_at": "2026-06-01T08:12:57.408266+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3631,7 +4706,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b7806031ac6a", @@ -3641,9 +4718,13 @@ "created_at": "2026-06-01T04:19:41.453167+00:00", "started_at": "2026-06-01T08:05:31.637954+00:00", "completed_at": "2026-06-01T08:10:33.251836+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3656,7 +4737,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "592ab6848d85", @@ -3666,9 +4749,13 @@ "created_at": "2026-06-01T04:19:41.450208+00:00", "started_at": "2026-06-01T08:03:09.126799+00:00", "completed_at": "2026-06-01T08:05:31.580525+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3681,7 +4768,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "904e327c3334", @@ -3691,9 +4780,13 @@ "created_at": "2026-06-01T04:19:41.447067+00:00", "started_at": "2026-06-01T08:00:39.486657+00:00", "completed_at": "2026-06-01T08:03:09.121293+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3706,7 +4799,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fe00864c5cf7", @@ -3716,9 +4811,13 @@ "created_at": "2026-06-01T04:19:41.440981+00:00", "started_at": "2026-06-01T07:58:13.213564+00:00", "completed_at": "2026-06-01T08:00:39.481628+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3731,7 +4830,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "7eb7c9ee1e7a", @@ -3741,9 +4842,13 @@ "created_at": "2026-06-01T04:19:41.436490+00:00", "started_at": "2026-06-01T07:55:36.673660+00:00", "completed_at": "2026-06-01T07:58:13.208090+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3756,7 +4861,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "1ee6e3d397b3", @@ -3766,9 +4873,13 @@ "created_at": "2026-06-01T04:19:41.433502+00:00", "started_at": "2026-06-01T07:53:19.409094+00:00", "completed_at": "2026-06-01T07:55:36.669241+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3781,7 +4892,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "99d5d125fe98", @@ -3791,9 +4904,13 @@ "created_at": "2026-06-01T04:19:41.430708+00:00", "started_at": "2026-06-01T07:49:10.912145+00:00", "completed_at": "2026-06-01T07:53:19.401079+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3806,7 +4923,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "717fc9bd271e", @@ -3816,9 +4935,13 @@ "created_at": "2026-06-01T04:19:41.426977+00:00", "started_at": "2026-06-01T07:46:31.727391+00:00", "completed_at": "2026-06-01T07:49:10.906782+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3831,7 +4954,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6721b096c9cc", @@ -3841,9 +4966,13 @@ "created_at": "2026-06-01T04:19:41.423999+00:00", "started_at": "2026-06-01T07:41:43.564425+00:00", "completed_at": "2026-06-01T07:46:31.721784+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3856,7 +4985,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "1fbb6614600d", @@ -3866,9 +4997,13 @@ "created_at": "2026-06-01T04:19:41.421677+00:00", "started_at": "2026-06-01T07:39:44.439769+00:00", "completed_at": "2026-06-01T07:41:43.559868+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3881,7 +5016,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3cd900181342", @@ -3891,9 +5028,13 @@ "created_at": "2026-06-01T04:19:41.418890+00:00", "started_at": "2026-06-01T07:37:14.286920+00:00", "completed_at": "2026-06-01T07:39:44.422464+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3906,7 +5047,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b6ba858a3159", @@ -3916,9 +5059,13 @@ "created_at": "2026-06-01T04:19:41.415987+00:00", "started_at": "2026-06-01T07:34:36.246523+00:00", "completed_at": "2026-06-01T07:37:14.276981+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3931,7 +5078,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4e9eb1428aaf", @@ -3941,9 +5090,13 @@ "created_at": "2026-06-01T04:19:41.413080+00:00", "started_at": "2026-06-01T07:31:40.393791+00:00", "completed_at": "2026-06-01T07:34:36.239519+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3956,7 +5109,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "deaf8d652e71", @@ -3966,9 +5121,13 @@ "created_at": "2026-06-01T04:19:41.407992+00:00", "started_at": "2026-06-01T07:29:08.025306+00:00", "completed_at": "2026-06-01T07:31:40.388086+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -3981,7 +5140,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "9848928e8aa9", @@ -3991,9 +5152,13 @@ "created_at": "2026-06-01T04:19:41.405522+00:00", "started_at": "2026-06-01T07:26:27.671710+00:00", "completed_at": "2026-06-01T07:29:08.010393+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4006,7 +5171,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d5e36440263a", @@ -4016,9 +5183,13 @@ "created_at": "2026-06-01T04:19:41.402992+00:00", "started_at": "2026-06-01T07:23:55.881866+00:00", "completed_at": "2026-06-01T07:26:27.631594+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4031,7 +5202,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ec29ee60dd79", @@ -4041,9 +5214,13 @@ "created_at": "2026-06-01T04:19:41.398433+00:00", "started_at": "2026-06-01T07:21:37.141074+00:00", "completed_at": "2026-06-01T07:23:55.875367+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4056,7 +5233,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "63daa7565ece", @@ -4066,9 +5245,13 @@ "created_at": "2026-06-01T04:19:41.395840+00:00", "started_at": "2026-06-01T07:18:54.167927+00:00", "completed_at": "2026-06-01T07:21:37.135975+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4081,7 +5264,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8710d7279d9b", @@ -4091,9 +5276,13 @@ "created_at": "2026-06-01T04:19:41.393392+00:00", "started_at": "2026-06-01T07:16:23.704106+00:00", "completed_at": "2026-06-01T07:18:54.161630+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4106,7 +5295,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "41dade37ff9e", @@ -4116,9 +5307,13 @@ "created_at": "2026-06-01T04:19:41.386769+00:00", "started_at": "2026-06-01T07:13:48.541824+00:00", "completed_at": "2026-06-01T07:16:23.699593+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4131,7 +5326,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "c5670718a679", @@ -4141,9 +5338,13 @@ "created_at": "2026-06-01T04:19:41.383986+00:00", "started_at": "2026-06-01T07:11:02.907847+00:00", "completed_at": "2026-06-01T07:13:48.507785+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4156,7 +5357,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "d966329dcc80", @@ -4166,9 +5369,13 @@ "created_at": "2026-06-01T04:19:41.375097+00:00", "started_at": "2026-06-01T07:08:23.389553+00:00", "completed_at": "2026-06-01T07:11:02.891633+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4181,7 +5388,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "a5daff23ff71", @@ -4191,9 +5400,13 @@ "created_at": "2026-06-01T04:19:41.372468+00:00", "started_at": "2026-06-01T07:05:37.057632+00:00", "completed_at": "2026-06-01T07:08:23.383298+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4206,7 +5419,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ee2810a84787", @@ -4216,9 +5431,13 @@ "created_at": "2026-06-01T04:19:41.368857+00:00", "started_at": "2026-06-01T07:03:07.909454+00:00", "completed_at": "2026-06-01T07:05:37.052138+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4231,7 +5450,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6abfa3374f55", @@ -4241,9 +5462,13 @@ "created_at": "2026-06-01T04:19:41.360994+00:00", "started_at": "2026-06-01T07:01:57.079055+00:00", "completed_at": "2026-06-01T07:03:07.905749+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 1, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4256,7 +5481,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "0eb6aa1b52c0", @@ -4266,9 +5493,13 @@ "created_at": "2026-06-01T04:19:41.355990+00:00", "started_at": "2026-06-01T06:59:33.114291+00:00", "completed_at": "2026-06-01T07:01:57.073945+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4281,7 +5512,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "cda0a2c7d6c2", @@ -4291,9 +5524,13 @@ "created_at": "2026-06-01T04:19:41.351382+00:00", "started_at": "2026-06-01T06:57:03.395869+00:00", "completed_at": "2026-06-01T06:59:33.102439+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4306,7 +5543,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "7ad86a25c5ec", @@ -4316,9 +5555,13 @@ "created_at": "2026-06-01T04:19:41.344971+00:00", "started_at": "2026-06-01T06:54:42.436787+00:00", "completed_at": "2026-06-01T06:57:03.347772+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4331,7 +5574,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ecd7a128260f", @@ -4341,9 +5586,13 @@ "created_at": "2026-06-01T04:19:41.341990+00:00", "started_at": "2026-06-01T06:51:32.428274+00:00", "completed_at": "2026-06-01T06:54:42.347776+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4356,7 +5605,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b3d9dc3860bd", @@ -4366,9 +5617,13 @@ "created_at": "2026-06-01T04:19:41.336451+00:00", "started_at": "2026-06-01T06:49:09.394408+00:00", "completed_at": "2026-06-01T06:51:32.423796+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4381,7 +5636,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "1ad119398728", @@ -4391,9 +5648,13 @@ "created_at": "2026-06-01T04:19:41.331503+00:00", "started_at": "2026-06-01T06:46:37.014368+00:00", "completed_at": "2026-06-01T06:49:09.380081+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4406,7 +5667,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3188938a371d", @@ -4416,9 +5679,13 @@ "created_at": "2026-06-01T04:19:41.323982+00:00", "started_at": "2026-06-01T06:39:28.089290+00:00", "completed_at": "2026-06-01T06:46:37.007922+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4431,7 +5698,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "e3336c69c748", @@ -4441,9 +5710,13 @@ "created_at": "2026-06-01T04:19:41.320954+00:00", "started_at": "2026-06-01T06:35:10.468057+00:00", "completed_at": "2026-06-01T06:39:28.082733+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4456,7 +5729,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "28f417ca8773", @@ -4466,9 +5741,13 @@ "created_at": "2026-06-01T04:19:41.317589+00:00", "started_at": "2026-06-01T06:32:47.871855+00:00", "completed_at": "2026-06-01T06:35:10.425499+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4481,7 +5760,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "7d6aa3700c5a", @@ -4491,9 +5772,13 @@ "created_at": "2026-06-01T04:19:41.314985+00:00", "started_at": "2026-06-01T06:30:21.382837+00:00", "completed_at": "2026-06-01T06:32:47.832193+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4506,7 +5791,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "09e51ad51881", @@ -4516,9 +5803,13 @@ "created_at": "2026-06-01T04:19:41.308913+00:00", "started_at": "2026-06-01T06:28:01.575603+00:00", "completed_at": "2026-06-01T06:30:21.375996+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4531,7 +5822,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3b3640dc6cfc", @@ -4541,9 +5834,13 @@ "created_at": "2026-06-01T04:19:41.303567+00:00", "started_at": "2026-06-01T06:25:26.577307+00:00", "completed_at": "2026-06-01T06:28:01.468360+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4556,7 +5853,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "99a17e9ab129", @@ -4566,9 +5865,13 @@ "created_at": "2026-06-01T04:19:41.300486+00:00", "started_at": "2026-06-01T06:20:26.930884+00:00", "completed_at": "2026-06-01T06:25:26.569055+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4581,7 +5884,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "af5e15644d36", @@ -4591,9 +5896,13 @@ "created_at": "2026-06-01T04:19:41.289971+00:00", "started_at": "2026-06-01T06:13:22.397593+00:00", "completed_at": "2026-06-01T06:15:42.312385+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4606,7 +5915,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4c41a70b1788", @@ -4616,9 +5927,13 @@ "created_at": "2026-06-01T04:19:41.287773+00:00", "started_at": "2026-06-01T06:11:03.333994+00:00", "completed_at": "2026-06-01T06:13:22.392347+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4631,7 +5946,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3eff66dc7505", @@ -4641,9 +5958,13 @@ "created_at": "2026-06-01T04:19:41.285392+00:00", "started_at": "2026-06-01T06:08:28.542523+00:00", "completed_at": "2026-06-01T06:11:03.330174+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4656,7 +5977,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "86d463083b20", @@ -4666,9 +5989,13 @@ "created_at": "2026-06-01T04:19:41.282947+00:00", "started_at": "2026-06-01T06:05:56.626658+00:00", "completed_at": "2026-06-01T06:08:28.537601+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4681,7 +6008,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "e6572e3a476a", @@ -4691,9 +6020,13 @@ "created_at": "2026-06-01T04:19:41.280497+00:00", "started_at": "2026-06-01T06:03:31.695984+00:00", "completed_at": "2026-06-01T06:05:56.620777+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4706,7 +6039,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "28fc0643b0e5", @@ -4716,9 +6051,13 @@ "created_at": "2026-06-01T04:19:41.278648+00:00", "started_at": "2026-06-01T06:01:06.374080+00:00", "completed_at": "2026-06-01T06:03:31.690656+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4731,7 +6070,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "73f7b65cdd7f", @@ -4741,9 +6082,13 @@ "created_at": "2026-06-01T04:19:41.276252+00:00", "started_at": "2026-06-01T05:58:33.131479+00:00", "completed_at": "2026-06-01T06:01:06.229215+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4756,7 +6101,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b177505bfa87", @@ -4766,9 +6113,13 @@ "created_at": "2026-06-01T04:19:41.273838+00:00", "started_at": "2026-06-01T05:55:50.543587+00:00", "completed_at": "2026-06-01T05:58:33.115040+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4781,7 +6132,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "f6cd32e7d905", @@ -4791,9 +6144,13 @@ "created_at": "2026-06-01T04:19:41.268127+00:00", "started_at": "2026-06-01T05:53:06.428517+00:00", "completed_at": "2026-06-01T05:55:50.526619+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4806,7 +6163,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "5bf3a0415eca", @@ -4816,9 +6175,13 @@ "created_at": "2026-06-01T04:19:41.265940+00:00", "started_at": "2026-06-01T05:50:47.184842+00:00", "completed_at": "2026-06-01T05:53:06.423671+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4831,7 +6194,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "339d6dfe81fd", @@ -4841,9 +6206,13 @@ "created_at": "2026-06-01T04:19:41.264201+00:00", "started_at": "2026-06-01T05:48:19.072246+00:00", "completed_at": "2026-06-01T05:50:47.180106+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4856,7 +6225,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "59eb9c1b3f6b", @@ -4866,9 +6237,13 @@ "created_at": "2026-06-01T04:19:41.262158+00:00", "started_at": "2026-06-01T05:45:58.403037+00:00", "completed_at": "2026-06-01T05:48:19.053106+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4881,7 +6256,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b3bb7b0592b6", @@ -4891,9 +6268,13 @@ "created_at": "2026-06-01T04:19:41.258987+00:00", "started_at": "2026-06-01T05:43:37.418940+00:00", "completed_at": "2026-06-01T05:45:58.349334+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4906,7 +6287,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "7cf7445bdb57", @@ -4916,9 +6299,13 @@ "created_at": "2026-06-01T04:19:41.252112+00:00", "started_at": "2026-06-01T05:41:08.452386+00:00", "completed_at": "2026-06-01T05:43:37.411438+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4931,7 +6318,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "f6a9b2b5ebdf", @@ -4941,9 +6330,13 @@ "created_at": "2026-06-01T04:19:41.248910+00:00", "started_at": "2026-06-01T05:38:34.411826+00:00", "completed_at": "2026-06-01T05:41:08.384662+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4956,7 +6349,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "71f38d5b17e2", @@ -4966,9 +6361,13 @@ "created_at": "2026-06-01T04:19:41.246083+00:00", "started_at": "2026-06-01T05:36:05.517194+00:00", "completed_at": "2026-06-01T05:38:34.371613+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -4981,7 +6380,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "678010c77357", @@ -4991,9 +6392,13 @@ "created_at": "2026-06-01T04:19:41.238598+00:00", "started_at": "2026-06-01T05:33:37.180225+00:00", "completed_at": "2026-06-01T05:36:05.507099+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5006,7 +6411,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "fa4e47ae6747", @@ -5016,9 +6423,13 @@ "created_at": "2026-06-01T04:19:41.236837+00:00", "started_at": "2026-06-01T05:31:10.108403+00:00", "completed_at": "2026-06-01T05:33:37.175681+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5031,7 +6442,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "87ea46b214cf", @@ -5041,9 +6454,13 @@ "created_at": "2026-06-01T04:19:41.234917+00:00", "started_at": "2026-06-01T05:28:42.167771+00:00", "completed_at": "2026-06-01T05:31:10.104136+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5056,7 +6473,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ed04e1a2268d", @@ -5066,9 +6485,13 @@ "created_at": "2026-06-01T04:19:41.232634+00:00", "started_at": "2026-06-01T05:26:11.202581+00:00", "completed_at": "2026-06-01T05:28:42.159922+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5081,7 +6504,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "b27a71fa6f9c", @@ -5091,9 +6516,13 @@ "created_at": "2026-06-01T04:19:41.230916+00:00", "started_at": "2026-06-01T05:23:43.347967+00:00", "completed_at": "2026-06-01T05:26:11.196582+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5106,7 +6535,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6aa869f37e2b", @@ -5116,9 +6547,13 @@ "created_at": "2026-06-01T04:19:41.227732+00:00", "started_at": "2026-06-01T05:21:03.796202+00:00", "completed_at": "2026-06-01T05:23:43.341178+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5131,7 +6566,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3098b43a1966", @@ -5141,9 +6578,13 @@ "created_at": "2026-06-01T04:19:41.226046+00:00", "started_at": "2026-06-01T05:18:34.253696+00:00", "completed_at": "2026-06-01T05:21:03.792392+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5156,7 +6597,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "3d21bce1f0d7", @@ -5166,9 +6609,13 @@ "created_at": "2026-06-01T04:19:41.224127+00:00", "started_at": "2026-06-01T05:15:56.607659+00:00", "completed_at": "2026-06-01T05:18:34.248079+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5181,7 +6628,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "ae4c700747c7", @@ -5191,9 +6640,13 @@ "created_at": "2026-06-01T04:19:41.222082+00:00", "started_at": "2026-06-01T05:13:19.349695+00:00", "completed_at": "2026-06-01T05:15:56.602795+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5206,7 +6659,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "e710c1c575ee", @@ -5216,9 +6671,13 @@ "created_at": "2026-06-01T04:19:41.220091+00:00", "started_at": "2026-06-01T05:08:29.838253+00:00", "completed_at": "2026-06-01T05:13:19.335120+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5231,7 +6690,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "bb777067bf2f", @@ -5241,9 +6702,13 @@ "created_at": "2026-06-01T04:19:41.218186+00:00", "started_at": "2026-06-01T05:05:51.689928+00:00", "completed_at": "2026-06-01T05:08:29.822644+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5256,7 +6721,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "493417088612", @@ -5266,9 +6733,13 @@ "created_at": "2026-06-01T04:19:41.215725+00:00", "started_at": "2026-06-01T05:03:11.804359+00:00", "completed_at": "2026-06-01T05:05:51.684910+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5281,7 +6752,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "8a5d39489f1e", @@ -5291,9 +6764,13 @@ "created_at": "2026-06-01T04:19:41.213955+00:00", "started_at": "2026-06-01T04:58:26.392118+00:00", "completed_at": "2026-06-01T05:03:11.798440+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5306,7 +6783,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "2bcb831b7ed2", @@ -5316,9 +6795,13 @@ "created_at": "2026-06-01T04:19:41.212251+00:00", "started_at": "2026-06-01T04:55:43.968599+00:00", "completed_at": "2026-06-01T04:58:26.386495+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5331,7 +6814,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "25274c12214e", @@ -5341,9 +6826,13 @@ "created_at": "2026-06-01T04:19:41.210457+00:00", "started_at": "2026-06-01T04:49:44.043451+00:00", "completed_at": "2026-06-01T04:55:43.957948+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5356,7 +6845,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "dd006a8cb5f8", @@ -5366,9 +6857,13 @@ "created_at": "2026-06-01T04:19:41.208532+00:00", "started_at": "2026-06-01T04:46:54.066706+00:00", "completed_at": "2026-06-01T04:49:44.037947+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5381,7 +6876,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "808ee3474e43", @@ -5391,9 +6888,13 @@ "created_at": "2026-06-01T04:19:41.206272+00:00", "started_at": "2026-06-01T04:41:38.157621+00:00", "completed_at": "2026-06-01T04:46:54.057075+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5406,7 +6907,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "98b518fd7deb", @@ -5416,9 +6919,13 @@ "created_at": "2026-06-01T04:19:41.202280+00:00", "started_at": "2026-06-01T04:31:50.021073+00:00", "completed_at": "2026-06-01T04:34:29.776467+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5431,7 +6938,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "4e9147b9aba3", @@ -5441,9 +6950,13 @@ "created_at": "2026-06-01T04:19:41.197618+00:00", "started_at": "2026-06-01T04:23:09.183099+00:00", "completed_at": "2026-06-01T04:31:50.013969+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5456,7 +6969,9 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { "job_id": "6e3f6f6d11e2", @@ -5466,9 +6981,13 @@ "created_at": "2026-06-01T04:19:41.194888+00:00", "started_at": "2026-06-01T04:19:41.195147+00:00", "completed_at": "2026-06-01T04:23:09.177841+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5481,19 +7000,25 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { - "job_id": "0d9882f767e4", - "configuration": "esp-ledmatrix1.yaml", + "job_id": "387b3ff39a73", + "configuration": "esp-laundrydrypow.yaml", "job_type": "clean", "status": "completed", - "created_at": "2026-06-01T11:27:18.782204+00:00", - "started_at": "2026-06-01T11:27:18.782662+00:00", - "completed_at": "2026-06-01T11:27:20.136530+00:00", + "created_at": "2026-07-12T00:12:57.211435+00:00", + "started_at": "2026-07-12T00:12:57.211913+00:00", + "completed_at": "2026-07-12T00:12:58.709223+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5506,44 +7031,25 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { - "job_id": "3be5eba32760", - "configuration": "esp-entbtproxy.yaml", - "job_type": "clean", - "status": "completed", - "created_at": "2026-06-01T09:32:39.673871+00:00", - "started_at": "2026-06-01T09:32:39.674388+00:00", - "completed_at": "2026-06-01T09:32:40.967000+00:00", - "exit_code": 0, - "error": null, - "port": "", - "new_name": "", - "depends_on": "", - "dependency_released": false, - "progress": null, - "remote_peer": "", - "remote_job_id": "", - "remote_peer_label": "", - "device_name": "", - "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "" - }, - { - "job_id": "caeff2ee984b", + "job_id": "270c5f21f57b", "configuration": "", "job_type": "reset_build_env", "status": "completed", - "created_at": "2026-06-01T04:18:48.122127+00:00", - "started_at": "2026-06-01T04:18:48.122426+00:00", - "completed_at": "2026-06-01T04:19:11.229921+00:00", + "created_at": "2026-07-12T00:04:24.718162+00:00", + "started_at": "2026-07-12T00:04:24.718741+00:00", + "completed_at": "2026-07-12T00:04:37.784714+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5556,19 +7062,25 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { - "job_id": "89273e97de03", - "configuration": "esp-officelights.yaml", + "job_id": "4be6b792154f", + "configuration": "esp-laundrydrypow.yaml", "job_type": "clean", "status": "completed", - "created_at": "2026-05-31T21:27:58.149553+00:00", - "started_at": "2026-05-31T21:27:58.149802+00:00", - "completed_at": "2026-05-31T21:27:59.353763+00:00", + "created_at": "2026-07-12T00:01:04.660322+00:00", + "started_at": "2026-07-12T00:01:04.662322+00:00", + "completed_at": "2026-07-12T00:01:06.475785+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5581,19 +7093,25 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" }, { - "job_id": "56d376896863", - "configuration": "esp-occupancyoffice.yaml", + "job_id": "03f2f58c6f4c", + "configuration": "esp-laundrydrypow.yaml", "job_type": "clean", "status": "completed", - "created_at": "2026-05-31T21:18:20.951861+00:00", - "started_at": "2026-05-31T21:18:20.952106+00:00", - "completed_at": "2026-05-31T21:18:22.290042+00:00", + "created_at": "2026-07-11T23:56:56.604637+00:00", + "started_at": "2026-07-11T23:56:56.605003+00:00", + "completed_at": "2026-07-11T23:56:58.902045+00:00", + "compile_started_at": null, + "compile_ended_at": null, "exit_code": 0, "error": null, "port": "", + "flash_bootloader": false, + "is_deferred_install": false, "new_name": "", "depends_on": "", "dependency_released": false, @@ -5606,7 +7124,40 @@ "source": "local", "source_pin_sha256": "", "source_label": "", - "source_esphome_version": "" + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "b9bcfbd246f0", + "configuration": "esp-3dprinterpow.yaml", + "job_type": "clean", + "status": "completed", + "created_at": "2026-07-11T11:23:22.671682+00:00", + "started_at": "2026-07-11T11:23:22.672050+00:00", + "completed_at": "2026-07-11T11:23:24.298828+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "", + "target_esphome_version": "", + "failure_reason": "" } ], "esp-nspanel2lounge copy.yaml": { @@ -5640,5 +7191,14 @@ }, "esp-astroclockstepper1.yaml": { "mac_address": "24:D7:EB:D0:16:68" + }, + "esp-espnow_test_receiver.yaml": { + "mac_address": "A8:42:E3:A9:E9:80" + }, + "esp-bedside-panel.yaml": { + "mac_address": "80:F1:B2:D3:65:90" + }, + "esp-reterminal-e1001.yaml": { + "mac_address": "44:1B:F6:85:AB:A4" } } \ No newline at end of file diff --git a/esphome/esp-astroclockstepper1 copy.old b/esphome/esp-astroclockstepper1 copy.old deleted file mode 100644 index b481505..0000000 --- a/esphome/esp-astroclockstepper1 copy.old +++ /dev/null @@ -1,1657 +0,0 @@ -#:########################################################################################:# -# TITLE: ASTRONOMICAL CLOCK STEPPER -# zorruno.com layout v1.1 2026 -#:########################################################################################:# -# REPO: -# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-astroclockstepper.yaml -#:########################################################################################:# -# VERSIONS: -# V1.7 2026-06-22 Rebuilt direction model so clockwise always equals forward and position count increase -# V1.6 2026-06-22 Fixed auto-position cycle calculation to use rotation_days / solar-year cycle -# V1.5 2026-06-22 Changed Motor Stop to OFF on boot and added shortest-path auto-positioning -# V1.4 2026-06-22 Added Motor Stop switch, swapped reverse buttons 3/4, and separated auto-position motor direction -# V1.3 2026-06-22 Added SNTP year auto-positioning button and restored boot auto-position switch -# V1.2 2026-06-22 Added DHCP IPv4 address flash sequence on onboard LED after WiFi connects -# V1.1 2026-06-22 Added onboard D1 Mini LED flash on each motor step -# V1.0 2026-06-22 Initial Version -#:########################################################################################:# -# HARDWARE: -# ESP8266 D1 Mini Clone -# 5V 28BYJ-48 Stepper Motor -# Keyestudio / ULN2003 style 5V stepper driver board -# -# Stepper Driver: -# - IN1: GPIO14 / D5 -# - IN2: GPIO12 / D6 -# - IN3: GPIO13 / D7 -# - IN4: GPIO15 / D8 -# - VCC: 5V motor supply -# - GND: Common GND with ESP8266 -# -# Physical Buttons: -# - Fast Reverse / Anti-clockwise: GPIO5 / D1 / Button 1 -# - Slow Reverse / Anti-clockwise: GPIO4 / D2 / Button 2 -# - Slow Forward / Clockwise: GPIO0 / D3 / Button 3 -# - Fast Forward / Clockwise: GPIO16 / D0 / Button 4 -# -# Button board: -# - Uses built-in 103 pull-up resistors, 10k ohm -# - Buttons should pull the GPIO pin to GND when pressed -# - Pull-ups must go to 3.3V, not 5V -# - Fast modes are on the two end buttons -# -# Onboard Step LED: -# - GPIO2 / D4 -# - Active-low on most D1 Mini boards -#:########################################################################################:# -# OPERATION NOTES: -# - Normal rotation starts automatically after boot because Motor Stop defaults OFF. -# - Motor Stop is OFF by default after every reboot and is not restored. -# - Motor Stop ON turns off all motor coils and pauses physical position counting. -# - Default rotation is one full revolution clockwise every 365.2422 days. -# - Default step count is 4096 steps per full revolution. -# - Normal movement is extremely slow, about one step every 2.14 hours. -# - Clockwise physical movement is forward. -# - Forward movement increases Position Step and Position Days. -# - Anti-clockwise physical movement is reverse. -# - Reverse movement decreases Position Step and Position Days. -# - Motor coils are powered only while stepping, then released. -# - No holding torque is used. -# - Web template switches mimic held physical buttons. -# - Manual movement only applies while a physical button or web switch is held/on. -# - Position counter wraps back to zero after one full rotation. -# - Zero Position Counter sets the current physical pointer location as Jan-zero. -# - Zero Position Counter does not reset the stepper coil phase. -# - Onboard D1 Mini LED flashes briefly every time a motor step is issued. -# - After WiFi connects, the onboard LED flashes the DHCP IPv4 address. -# - IPv4 flash sequence is decimal digit based. -# - A digit of 0 is represented as 10 flashes. -# - SNTP is used for current year-position calculation. -# - Auto Position Now calculates the target using the configured 365.2422-day rotation cycle. -# - Auto Position Now moves to the current calculated year position by the shortest path. -# - Auto Position can move clockwise or anti-clockwise, whichever is quicker. -# - Auto Position On Boot is off by default, restored on power loss, and runs after boot/IP flash if Motor Stop is OFF. -#:########################################################################################:# -# OFFLINE NOTES: -# a) Home Assistant OFFLINE, but Network ONLINE -# - Device remains controllable via local web portal -# - HA entities unavailable until HA returns -# b) WiFi/Network OFFLINE -# - No HA API or web control available -# - Physical buttons continue to work if Motor Stop is OFF -# - Fallback AP / captive portal should become available from network_common_dhcp.yaml -# c) SNTP NOT SYNCED -# - Normal motor timing continues if Motor Stop is OFF -# - Manual controls continue if Motor Stop is OFF -# - Auto Position Now will not run until SNTP time is valid -# d) Device power loss / reboot -# - Current position counter is restored from flash -# - Auto Position On Boot setting is restored from flash -# - Motor Stop returns to OFF after every reboot -# - Physical position is assumed valid because the motor housing/dial is fixed -#:########################################################################################:# - - -#:########################################################################################:# -# SUBSTITUTIONS: Specific device variable substitutions -# If NOT using a secrets file, just replace these with the passwords etc (in quotes) -#:########################################################################################:# -substitutions: - # Device Naming - device_name: "esp-astroclockstepper1" - friendly_name: "Astronomical Clock Stepper" - description_comment: "Astronomical Clock Stepper :: ESP8266 D1 Mini with 28BYJ-48 Stepper Motor (Layout V1.1)" - device_area: "Standalone" - - # Project Naming - project_name: "zorruno.Astronomical Clock Stepper" - project_version: "v1.7" - - # Passwords & Secrets - api_key: !secret esp-api_key - ota_pass: !secret esp-ota_pass - - # Device Settings - log_level: "INFO" - sensor_update_interval: "1s" - calculated_sensor_update_interval: "60s" - - # Time Settings - timezone_name: "Pacific/Auckland" - - # Astronomical Clock Settings - rotation_days: "365.2422" - steps_per_rotation: "4096" - - # Physical Direction Calibration - # This is the motor phase direction that makes the needle move clockwise. - # Your testing showed the old reverse direction moved clockwise, so default is -1. - # If clockwise/anti-clockwise is still backwards, change this to 1. - clockwise_motor_direction_multiplier: "-1" - - # Manual Movement Speeds - manual_fast_rotation_seconds: "30" - manual_slow_rotation_seconds: "300" - - # Auto Position Path - # true = move clockwise or anti-clockwise, whichever is quicker - # false = always move clockwise / forward through the year - auto_position_use_shortest_path: "true" - - # Auto Position Speed - # Default is the same as Slow Forward: one full rotation every 300 seconds. - auto_position_rotation_seconds: "300" - - # Coil Release - coil_release_ms: "500" - - # Step LED - onboard_step_led_pin: GPIO2 - step_led_flash_ms: "25" - - # DHCP IP Address LED Flash Settings - ip_flash_enabled: "true" - ip_flash_repeat_count: "5" - ip_flash_zero_digit_blinks: "10" - ip_flash_loop_interval: "25ms" - - # Suggested human-countable timing - ip_flash_on_ms: "200" - ip_flash_between_flash_gap_ms: "450" - ip_flash_digit_gap_ms: "1500" - ip_flash_octet_gap_ms: "3500" - ip_flash_repeat_gap_ms: "8000" - - # Stepper Output Pins - stepper_in1_pin: GPIO14 - stepper_in2_pin: GPIO12 - stepper_in3_pin: GPIO13 - stepper_in4_pin: GPIO15 - - # Physical Button Input Pins - # Button 1 and 2 physically move anti-clockwise, so they are reverse. - # Button 3 and 4 physically move clockwise, so they are forward. - button_fast_reverse_pin: GPIO5 - button_slow_reverse_pin: GPIO4 - button_slow_forward_pin: GPIO0 - button_fast_forward_pin: GPIO16 - - # Entity Naming - motor_stop_name: "Motor Stop" - - web_fast_forward_name: "Web Fast Forward" - web_slow_forward_name: "Web Slow Forward" - web_fast_reverse_name: "Web Fast Reverse" - web_slow_reverse_name: "Web Slow Reverse" - - physical_fast_forward_name: "Physical Fast Forward" - physical_slow_forward_name: "Physical Slow Forward" - physical_fast_reverse_name: "Physical Fast Reverse" - physical_slow_reverse_name: "Physical Slow Reverse" - - zero_position_name: "Zero Position Counter" - replay_ip_flash_name: "Replay IP Address Flash" - - auto_position_now_name: "Auto Position Now" - auto_position_on_boot_name: "Auto Position On Boot" - auto_position_active_name: "Auto Position Active" - sntp_time_valid_name: "SNTP Time Valid" - - -#:########################################################################################:# -# PACKAGES: Included Common Packages -# https://esphome.io/components/packages.html -#:########################################################################################:# -packages: - #### WIFI, Network (DHCP/IPV6 etc), Fallback AP, Safemode #### - common_wifi: !include - file: common/network_common_dhcp.yaml - vars: - local_device_name: "${device_name}" - local_ota_pass: "${ota_pass}" - - #### HOME ASSISTANT API (choose encryption or no encryption options) #### - common_api: !include - file: common/api_common.yaml - #file: common/api_common_noencryption.yaml - vars: - local_api_key: "${api_key}" - - #### MQTT #### - #common_mqtt: !include - # file: common/mqtt_common.yaml - # vars: - # local_device_name: "${device_name}" - - #### WEB PORTAL #### - #common_webportal: !include common/webportal_common.yaml - common_webportal: !include common/webportal_common_nopasswd.yaml - - #### SNTP (Only use if you want/need accurate timeclocks) #### - #common_sntp: !include common/sntp_common.yaml - - #### DIAGNOSTICS Sensors #### - diag_basic: !include common/include_basic_diag_sensors.yaml - diag_more: !include common/include_more_diag_sensors_nomqtt.yaml - #diag_debug: !include common/include_debug_diag_sensors.yaml - #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml - - -#:########################################################################################:# -# ESPHOME: -# https://esphome.io/components/esphome.html -#:########################################################################################:# -esphome: - name: "${device_name}" - friendly_name: "${friendly_name}" - comment: "${description_comment}" - area: "${device_area}" - project: - name: "${project_name}" - version: "${project_version}" - - on_boot: - priority: -100 - then: - - output.turn_off: onboard_step_led - - # Motor Stop should always be OFF on boot so normal movement can run. - - switch.turn_off: motor_stop_switch - - - logger.log: - level: INFO - format: "Astronomical Clock Stepper started. Normal rotation is %s days per full revolution." - args: - - '"${rotation_days}"' - - # Wait for WiFi/DHCP, then flash the assigned IPv4 address. - # This does not stop the main stepper scheduler from running. - - wait_until: - condition: - wifi.connected: - - - delay: 3s - - - script.execute: start_ip_flash_script - - # Give the IP flash state machine time to start. - - delay: 100ms - - # Wait for the boot IP flash sequence to finish before optional auto-positioning. - - wait_until: - condition: - lambda: |- - return !id(ip_flash_active); - - - if: - condition: - switch.is_on: auto_position_on_boot_switch - then: - - globals.set: - id: auto_position_on_boot_pending - value: "true" - - - if: - condition: - lambda: |- - return id(sntp_time).now().is_valid(); - then: - - globals.set: - id: auto_position_on_boot_pending - value: "false" - - script.execute: start_auto_position_script - - -#:########################################################################################:# -# ESP PLATFORM AND FRAMEWORK: -# https://esphome.io/components/esp8266.html -#:########################################################################################:# -esp8266: - board: d1_mini - restore_from_flash: true - -preferences: - flash_write_interval: 1min - -mdns: - disabled: false - - -#:########################################################################################:# -# LOGGING: -# https://esphome.io/components/logger.html -#:########################################################################################:# -logger: - level: "${log_level}" - baud_rate: 0 - - -#:########################################################################################:# -# TIME: -# SNTP time source for year-position calculation -# https://esphome.io/components/time/sntp.html -#:########################################################################################:# -time: - - platform: sntp - id: sntp_time - timezone: "${timezone_name}" - on_time_sync: - then: - - if: - condition: - lambda: |- - return id(auto_position_on_boot_pending) && id(auto_position_on_boot_switch).state; - then: - - globals.set: - id: auto_position_on_boot_pending - value: "false" - - script.execute: start_auto_position_script - - -#:########################################################################################:# -# GLOBALS: -# Internal position, timing, mode, held-button state, IP flash state, and auto-position state -# https://esphome.io/components/globals.html -#:########################################################################################:# -globals: - - id: current_position_step - type: int - restore_value: yes - initial_value: "0" - - # Electrical stepper phase. - # This should not be reset when setting the calendar zero point. - - id: current_phase_index - type: int - restore_value: yes - initial_value: "0" - - - id: last_loop_ms - type: uint32_t - restore_value: no - initial_value: "0" - - - id: last_step_ms - type: uint32_t - restore_value: no - initial_value: "0" - - - id: step_accumulator_ms - type: double - restore_value: no - initial_value: "0.0" - - - id: coils_are_on - type: bool - restore_value: no - initial_value: "false" - - - id: step_led_is_on - type: bool - restore_value: no - initial_value: "false" - - - id: step_led_started_ms - type: uint32_t - restore_value: no - initial_value: "0" - - # 0 = Normal - # 1 = Fast Forward - # 2 = Slow Forward - # 3 = Fast Reverse - # 4 = Slow Reverse - # 5 = Auto Position - # 6 = Motor Stopped - - id: current_motion_mode - type: int - restore_value: no - initial_value: "0" - - # Physical Button States - - id: physical_fast_forward_active - type: bool - restore_value: no - initial_value: "false" - - - id: physical_slow_forward_active - type: bool - restore_value: no - initial_value: "false" - - - id: physical_fast_reverse_active - type: bool - restore_value: no - initial_value: "false" - - - id: physical_slow_reverse_active - type: bool - restore_value: no - initial_value: "false" - - # Web Button States - - id: web_fast_forward_active - type: bool - restore_value: no - initial_value: "false" - - - id: web_slow_forward_active - type: bool - restore_value: no - initial_value: "false" - - - id: web_fast_reverse_active - type: bool - restore_value: no - initial_value: "false" - - - id: web_slow_reverse_active - type: bool - restore_value: no - initial_value: "false" - - # IP Address Flash State - - id: ip_flash_active - type: bool - restore_value: no - initial_value: "false" - - - id: ip_flash_led_is_on - type: bool - restore_value: no - initial_value: "false" - - - id: ip_flash_next_action_ms - type: uint32_t - restore_value: no - initial_value: "0" - - - id: ip_flash_repeats_completed - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_octet_index - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_digit_index - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_blinks_completed - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_octet_0 - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_octet_1 - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_octet_2 - type: int - restore_value: no - initial_value: "0" - - - id: ip_flash_octet_3 - type: int - restore_value: no - initial_value: "0" - - # Auto Position State - - id: auto_position_active - type: bool - restore_value: no - initial_value: "false" - - - id: auto_position_on_boot_pending - type: bool - restore_value: no - initial_value: "false" - - - id: auto_position_target_step - type: int - restore_value: no - initial_value: "0" - - - id: auto_position_steps_remaining - type: int - restore_value: no - initial_value: "0" - - # 1 = clockwise / forward / increasing position - # -1 = anti-clockwise / reverse / decreasing position - - id: auto_position_logical_direction - type: int - restore_value: no - initial_value: "1" - - - id: auto_position_cycle_seconds - type: double - restore_value: no - initial_value: "0.0" - - - id: auto_position_forward_steps - type: int - restore_value: no - initial_value: "0" - - - id: auto_position_reverse_steps - type: int - restore_value: no - initial_value: "0" - - -#:########################################################################################:# -# OUTPUT: -# Stepper driver GPIO outputs and onboard step LED -# https://esphome.io/components/output/gpio.html -#:########################################################################################:# -output: - - platform: gpio - id: stepper_in1 - pin: ${stepper_in1_pin} - - - platform: gpio - id: stepper_in2 - pin: ${stepper_in2_pin} - - - platform: gpio - id: stepper_in3 - pin: ${stepper_in3_pin} - - - platform: gpio - id: stepper_in4 - pin: ${stepper_in4_pin} - - # D1 Mini onboard LED GPIO2 / D4 and active-low - - platform: gpio - id: onboard_step_led - pin: - number: ${onboard_step_led_pin} - inverted: true - - -#:########################################################################################:# -# SCRIPT: -# Utility scripts -# https://esphome.io/components/script.html -#:########################################################################################:# -script: - - id: start_ip_flash_script - mode: restart - then: - - lambda: |- - const bool IP_FLASH_ENABLED = ${ip_flash_enabled}; - - if (!IP_FLASH_ENABLED) { - ESP_LOGI("ip_flash", "IP flash is disabled."); - return; - } - - if (WiFi.status() != WL_CONNECTED) { - ESP_LOGW("ip_flash", "WiFi is not connected. Cannot flash IP address."); - return; - } - - IPAddress ip = WiFi.localIP(); - - if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0) { - ESP_LOGW("ip_flash", "Invalid IPv4 address 0.0.0.0. Cannot flash IP address."); - return; - } - - id(ip_flash_octet_0) = ip[0]; - id(ip_flash_octet_1) = ip[1]; - id(ip_flash_octet_2) = ip[2]; - id(ip_flash_octet_3) = ip[3]; - - id(ip_flash_repeats_completed) = 0; - id(ip_flash_octet_index) = 0; - id(ip_flash_digit_index) = 0; - id(ip_flash_blinks_completed) = 0; - id(ip_flash_led_is_on) = false; - id(ip_flash_next_action_ms) = millis(); - id(ip_flash_active) = true; - - // Keep the IP flash sequence visually clean. - id(step_led_is_on) = false; - id(onboard_step_led).turn_off(); - - ESP_LOGI( - "ip_flash", - "Flashing IPv4 address %u.%u.%u.%u on onboard LED. Repeats: %d", - ip[0], - ip[1], - ip[2], - ip[3], - ${ip_flash_repeat_count} - ); - - - id: start_auto_position_script - mode: restart - then: - - lambda: |- - const int STEPS_PER_ROTATION = ${steps_per_rotation}; - const bool AUTO_POSITION_USE_SHORTEST_PATH = ${auto_position_use_shortest_path}; - - const double ROTATION_SECONDS = - ${rotation_days} * 24.0 * 60.0 * 60.0; - - auto now = id(sntp_time).now(); - - if (!now.is_valid()) { - ESP_LOGW("auto_position", "SNTP time is not valid yet. Auto position cancelled."); - return; - } - - if (id(motor_stop_switch).state) { - ESP_LOGW("auto_position", "Motor Stop is ON. Auto position cancelled."); - return; - } - - const int year = now.year; - const int month = now.month; - const int day_of_month = now.day_of_month; - - const bool is_leap_year = - ((year % 4 == 0) && (year % 100 != 0)) || - (year % 400 == 0); - - const int days_before_month_common[12] = { - 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 - }; - - const int days_before_month_leap[12] = { - 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 - }; - - int safe_month = month; - - if (safe_month < 1) { - safe_month = 1; - } - - if (safe_month > 12) { - safe_month = 12; - } - - int safe_day_of_month = day_of_month; - - if (safe_day_of_month < 1) { - safe_day_of_month = 1; - } - - if (safe_day_of_month > 31) { - safe_day_of_month = 31; - } - - const int day_of_year = - (is_leap_year ? days_before_month_leap[safe_month - 1] : days_before_month_common[safe_month - 1]) + - safe_day_of_month; - - const uint32_t seconds_elapsed_this_calendar_year = - ((uint32_t) (day_of_year - 1) * 86400UL) + - ((uint32_t) now.hour * 3600UL) + - ((uint32_t) now.minute * 60UL) + - ((uint32_t) now.second); - - // The display zero mark is Jan 1 local midnight. - // The dial cycle is the configured solar-year length, not 365 or 366 days. - double cycle_seconds = (double) seconds_elapsed_this_calendar_year; - - while (cycle_seconds >= ROTATION_SECONDS) { - cycle_seconds -= ROTATION_SECONDS; - } - - while (cycle_seconds < 0.0) { - cycle_seconds += ROTATION_SECONDS; - } - - int target_step = - (int) ((cycle_seconds / ROTATION_SECONDS) * (double) STEPS_PER_ROTATION); - - target_step = target_step % STEPS_PER_ROTATION; - - int current_step = id(current_position_step) % STEPS_PER_ROTATION; - - if (current_step < 0) { - current_step += STEPS_PER_ROTATION; - } - - const int forward_steps = - (target_step - current_step + STEPS_PER_ROTATION) % STEPS_PER_ROTATION; - - const int reverse_steps = - (current_step - target_step + STEPS_PER_ROTATION) % STEPS_PER_ROTATION; - - int chosen_steps = forward_steps; - int chosen_logical_direction = 1; - - if (AUTO_POSITION_USE_SHORTEST_PATH && reverse_steps < forward_steps) { - chosen_steps = reverse_steps; - chosen_logical_direction = -1; - } - - id(auto_position_target_step) = target_step; - id(auto_position_steps_remaining) = chosen_steps; - id(auto_position_logical_direction) = chosen_logical_direction; - id(auto_position_cycle_seconds) = cycle_seconds; - id(auto_position_forward_steps) = forward_steps; - id(auto_position_reverse_steps) = reverse_steps; - - if (chosen_steps == 0) { - id(auto_position_active) = false; - ESP_LOGI( - "auto_position", - "Already at correct solar-year position. Year: %d, target step: %d.", - year, - target_step - ); - return; - } - - id(auto_position_active) = true; - - ESP_LOGI( - "auto_position", - "Auto positioning started. Year: %d, day_of_year: %d, cycle_seconds: %.2f / %.2f, target step: %d, current step: %d, forward steps: %d, reverse steps: %d, chosen direction: %s, moving %d steps.", - year, - day_of_year, - cycle_seconds, - ROTATION_SECONDS, - target_step, - current_step, - forward_steps, - reverse_steps, - chosen_logical_direction >= 0 ? "clockwise/forward" : "anti-clockwise/reverse", - chosen_steps - ); - - -#:########################################################################################:# -# SWITCH: -# Motor stop, web held-button controls, and restored auto-position boot option -# https://esphome.io/components/switch/template.html -#:########################################################################################:# -switch: - - platform: template - name: "${web_fast_forward_name}" - id: web_fast_forward_switch - icon: mdi:fast-forward - optimistic: true - restore_mode: ALWAYS_OFF - turn_on_action: - - switch.turn_off: web_slow_forward_switch - - switch.turn_off: web_fast_reverse_switch - - switch.turn_off: web_slow_reverse_switch - - globals.set: - id: web_fast_forward_active - value: "true" - turn_off_action: - - globals.set: - id: web_fast_forward_active - value: "false" - - - platform: template - name: "${web_slow_forward_name}" - id: web_slow_forward_switch - icon: mdi:play-speed - optimistic: true - restore_mode: ALWAYS_OFF - turn_on_action: - - switch.turn_off: web_fast_forward_switch - - switch.turn_off: web_fast_reverse_switch - - switch.turn_off: web_slow_reverse_switch - - globals.set: - id: web_slow_forward_active - value: "true" - turn_off_action: - - globals.set: - id: web_slow_forward_active - value: "false" - - - platform: template - name: "${web_fast_reverse_name}" - id: web_fast_reverse_switch - icon: mdi:rewind - optimistic: true - restore_mode: ALWAYS_OFF - turn_on_action: - - switch.turn_off: web_fast_forward_switch - - switch.turn_off: web_slow_forward_switch - - switch.turn_off: web_slow_reverse_switch - - globals.set: - id: web_fast_reverse_active - value: "true" - turn_off_action: - - globals.set: - id: web_fast_reverse_active - value: "false" - - - platform: template - name: "${web_slow_reverse_name}" - id: web_slow_reverse_switch - icon: mdi:rewind-outline - optimistic: true - restore_mode: ALWAYS_OFF - turn_on_action: - - switch.turn_off: web_fast_forward_switch - - switch.turn_off: web_slow_forward_switch - - switch.turn_off: web_fast_reverse_switch - - globals.set: - id: web_slow_reverse_active - value: "true" - turn_off_action: - - globals.set: - id: web_slow_reverse_active - value: "false" - - - platform: template - name: "${auto_position_on_boot_name}" - id: auto_position_on_boot_switch - icon: mdi:calendar-sync - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - - - platform: template - name: "${motor_stop_name}" - id: motor_stop_switch - icon: mdi:motor-off - optimistic: true - - # OFF by default after every boot. State is not restored. - # OFF means the motor is allowed to run. - # ON means motor outputs are disabled. - restore_mode: ALWAYS_OFF - - turn_on_action: - - switch.turn_off: web_fast_forward_switch - - switch.turn_off: web_slow_forward_switch - - switch.turn_off: web_fast_reverse_switch - - switch.turn_off: web_slow_reverse_switch - - lambda: |- - id(web_fast_forward_active) = false; - id(web_slow_forward_active) = false; - id(web_fast_reverse_active) = false; - id(web_slow_reverse_active) = false; - - id(stepper_in1).turn_off(); - id(stepper_in2).turn_off(); - id(stepper_in3).turn_off(); - id(stepper_in4).turn_off(); - id(coils_are_on) = false; - - if (id(auto_position_active)) { - id(auto_position_active) = false; - id(auto_position_steps_remaining) = 0; - ESP_LOGW("motor_stop", "Motor Stop enabled. Auto positioning cancelled."); - } - - id(step_accumulator_ms) = 0.0; - id(current_motion_mode) = 6; - - turn_off_action: - - lambda: |- - id(step_accumulator_ms) = 0.0; - id(current_motion_mode) = 0; - ESP_LOGI("motor_stop", "Motor Stop disabled. Motor outputs enabled."); - - -#:########################################################################################:# -# BINARY SENSORS: -# Physical button inputs using external 103 / 10k pull-ups -# https://esphome.io/components/binary_sensor/gpio.html -#:########################################################################################:# -binary_sensor: - - platform: gpio - name: "${physical_fast_reverse_name}" - id: physical_fast_reverse_button - pin: - number: ${button_fast_reverse_pin} - mode: INPUT - inverted: true - filters: - - delayed_on: 20ms - - delayed_off: 20ms - on_press: - - globals.set: - id: physical_fast_reverse_active - value: "true" - on_release: - - globals.set: - id: physical_fast_reverse_active - value: "false" - - - platform: gpio - name: "${physical_slow_reverse_name}" - id: physical_slow_reverse_button - pin: - number: ${button_slow_reverse_pin} - mode: INPUT - inverted: true - filters: - - delayed_on: 20ms - - delayed_off: 20ms - on_press: - - globals.set: - id: physical_slow_reverse_active - value: "true" - on_release: - - globals.set: - id: physical_slow_reverse_active - value: "false" - - - platform: gpio - name: "${physical_slow_forward_name}" - id: physical_slow_forward_button - pin: - number: ${button_slow_forward_pin} - mode: INPUT - inverted: true - filters: - - delayed_on: 20ms - - delayed_off: 20ms - on_press: - - globals.set: - id: physical_slow_forward_active - value: "true" - on_release: - - globals.set: - id: physical_slow_forward_active - value: "false" - - - platform: gpio - name: "${physical_fast_forward_name}" - id: physical_fast_forward_button - pin: - number: ${button_fast_forward_pin} - mode: INPUT - inverted: true - filters: - - delayed_on: 20ms - - delayed_off: 20ms - on_press: - - globals.set: - id: physical_fast_forward_active - value: "true" - on_release: - - globals.set: - id: physical_fast_forward_active - value: "false" - - - platform: template - name: "${auto_position_active_name}" - id: auto_position_active_sensor - icon: mdi:calendar-sync - lambda: |- - return id(auto_position_active); - - - platform: template - name: "${sntp_time_valid_name}" - id: sntp_time_valid_sensor - icon: mdi:clock-check-outline - lambda: |- - return id(sntp_time).now().is_valid(); - - -#:########################################################################################:# -# BUTTON: -# Utility buttons -# https://esphome.io/components/button/template.html -#:########################################################################################:# -button: - - platform: template - name: "${zero_position_name}" - id: zero_position_counter_button - icon: mdi:counter - on_press: - - globals.set: - id: current_position_step - value: "0" - - globals.set: - id: step_accumulator_ms - value: "0.0" - - globals.set: - id: auto_position_active - value: "false" - - globals.set: - id: auto_position_steps_remaining - value: "0" - - globals.set: - id: auto_position_logical_direction - value: "1" - - globals.set: - id: auto_position_cycle_seconds - value: "0.0" - - globals.set: - id: auto_position_forward_steps - value: "0" - - globals.set: - id: auto_position_reverse_steps - value: "0" - - logger.log: - level: INFO - format: "Position counter zeroed. Current physical pointer location is now Jan-zero." - - - platform: template - name: "${replay_ip_flash_name}" - id: replay_ip_flash_button - icon: mdi:ip-network - on_press: - - script.execute: start_ip_flash_script - - - platform: template - name: "${auto_position_now_name}" - id: auto_position_now_button - icon: mdi:calendar-clock - on_press: - - script.execute: start_auto_position_script - - -#:########################################################################################:# -# SENSOR: -# Position and calculated movement sensors -# https://esphome.io/components/sensor/template.html -#:########################################################################################:# -sensor: - - platform: template - name: "Position Days" - id: position_days_sensor - icon: mdi:calendar-clock - unit_of_measurement: "days" - accuracy_decimals: 4 - update_interval: "${sensor_update_interval}" - lambda: |- - return ((double) id(current_position_step) * ${rotation_days}) / ${steps_per_rotation}; - - - platform: template - name: "Position Step" - id: position_step_sensor - icon: mdi:counter - accuracy_decimals: 0 - update_interval: "${sensor_update_interval}" - lambda: |- - return id(current_position_step); - - - platform: template - name: "Days Per Step" - id: days_per_step_sensor - icon: mdi:calendar-arrow-right - unit_of_measurement: "days/step" - accuracy_decimals: 8 - update_interval: "${calculated_sensor_update_interval}" - lambda: |- - return ${rotation_days} / ${steps_per_rotation}; - - - platform: template - name: "Hours Per Step" - id: hours_per_step_sensor - icon: mdi:timer-outline - unit_of_measurement: "h/step" - accuracy_decimals: 8 - update_interval: "${calculated_sensor_update_interval}" - lambda: |- - return (${rotation_days} * 24.0) / ${steps_per_rotation}; - - - platform: template - name: "Normal Step Interval" - id: normal_step_interval_sensor - icon: mdi:timer-cog-outline - unit_of_measurement: "s/step" - accuracy_decimals: 4 - update_interval: "${calculated_sensor_update_interval}" - lambda: |- - return (${rotation_days} * 24.0 * 60.0 * 60.0) / ${steps_per_rotation}; - - - platform: template - name: "Auto Position Target Step" - id: auto_position_target_step_sensor - icon: mdi:target - accuracy_decimals: 0 - update_interval: "${sensor_update_interval}" - lambda: |- - return id(auto_position_target_step); - - - platform: template - name: "Auto Position Target Days" - id: auto_position_target_days_sensor - icon: mdi:calendar-clock - unit_of_measurement: "days" - accuracy_decimals: 4 - update_interval: "${sensor_update_interval}" - lambda: |- - return ((double) id(auto_position_target_step) * ${rotation_days}) / ${steps_per_rotation}; - - - platform: template - name: "Auto Position Steps Remaining" - id: auto_position_steps_remaining_sensor - icon: mdi:counter - accuracy_decimals: 0 - update_interval: "${sensor_update_interval}" - lambda: |- - return id(auto_position_steps_remaining); - - - platform: template - name: "Auto Position Forward Steps" - id: auto_position_forward_steps_sensor - icon: mdi:rotate-right - accuracy_decimals: 0 - update_interval: "${sensor_update_interval}" - lambda: |- - return id(auto_position_forward_steps); - - - platform: template - name: "Auto Position Reverse Steps" - id: auto_position_reverse_steps_sensor - icon: mdi:rotate-left - accuracy_decimals: 0 - update_interval: "${sensor_update_interval}" - lambda: |- - return id(auto_position_reverse_steps); - - - platform: template - name: "Auto Position Cycle Days" - id: auto_position_cycle_days_sensor - icon: mdi:calendar-today - unit_of_measurement: "days" - accuracy_decimals: 4 - update_interval: "${sensor_update_interval}" - lambda: |- - return id(auto_position_cycle_seconds) / 86400.0; - - -#:########################################################################################:# -# TEXT SENSOR: -# Human-readable status sensors -# https://esphome.io/components/text_sensor/template.html -#:########################################################################################:# -text_sensor: - - platform: template - name: "Motion Mode" - id: motion_mode_text_sensor - icon: mdi:rotate-3d-variant - update_interval: "${sensor_update_interval}" - lambda: |- - if (id(motor_stop_switch).state) { - return {"Motor Stopped"}; - } - - switch (id(current_motion_mode)) { - case 1: - return {"Fast Forward"}; - case 2: - return {"Slow Forward"}; - case 3: - return {"Fast Reverse"}; - case 4: - return {"Slow Reverse"}; - case 5: - return {"Auto Position"}; - case 6: - return {"Motor Stopped"}; - case 0: - default: - return {"Normal"}; - } - - - platform: template - name: "Normal Direction" - id: normal_direction_text_sensor - icon: mdi:rotate-right - update_interval: "${calculated_sensor_update_interval}" - lambda: |- - return {"Clockwise / Forward"}; - - - platform: template - name: "Auto Position Direction" - id: auto_position_direction_text_sensor - icon: mdi:rotate-3d-variant - update_interval: "${sensor_update_interval}" - lambda: |- - if (!id(auto_position_active)) { - return {"Idle"}; - } - - if (id(auto_position_logical_direction) < 0) { - return {"Anti-clockwise / Reverse"}; - } - - return {"Clockwise / Forward"}; - - -#:########################################################################################:# -# INTERVAL: -# Stepper scheduler and IP flash scheduler -# https://esphome.io/components/interval.html -#:########################################################################################:# -interval: - - interval: 5ms - then: - - lambda: |- - const int STEPS_PER_ROTATION = ${steps_per_rotation}; - const int CLOCKWISE_MOTOR_DIRECTION_MULTIPLIER = ${clockwise_motor_direction_multiplier}; - - const double NORMAL_STEP_INTERVAL_MS = - (${rotation_days} * 24.0 * 60.0 * 60.0 * 1000.0) / ${steps_per_rotation}; - - const double FAST_STEP_INTERVAL_MS = - (${manual_fast_rotation_seconds} * 1000.0) / ${steps_per_rotation}; - - const double SLOW_STEP_INTERVAL_MS = - (${manual_slow_rotation_seconds} * 1000.0) / ${steps_per_rotation}; - - const double AUTO_POSITION_STEP_INTERVAL_MS = - (${auto_position_rotation_seconds} * 1000.0) / ${steps_per_rotation}; - - const uint32_t COIL_RELEASE_MS = ${coil_release_ms}; - const uint32_t STEP_LED_FLASH_MS = ${step_led_flash_ms}; - - auto all_coils_off = [&]() { - id(stepper_in1).turn_off(); - id(stepper_in2).turn_off(); - id(stepper_in3).turn_off(); - id(stepper_in4).turn_off(); - id(coils_are_on) = false; - }; - - auto flash_step_led = [&](uint32_t now_ms) { - if (!id(ip_flash_active)) { - id(onboard_step_led).turn_on(); - id(step_led_started_ms) = now_ms; - id(step_led_is_on) = true; - } - }; - - auto set_coils_for_phase = [&](int phase) { - const bool sequence[8][4] = { - {true, false, false, false}, - {true, true, false, false}, - {false, true, false, false}, - {false, true, true, false}, - {false, false, true, false}, - {false, false, true, true }, - {false, false, false, true }, - {true, false, false, true } - }; - - phase = ((phase % 8) + 8) % 8; - - if (sequence[phase][0]) { - id(stepper_in1).turn_on(); - } else { - id(stepper_in1).turn_off(); - } - - if (sequence[phase][1]) { - id(stepper_in2).turn_on(); - } else { - id(stepper_in2).turn_off(); - } - - if (sequence[phase][2]) { - id(stepper_in3).turn_on(); - } else { - id(stepper_in3).turn_off(); - } - - if (sequence[phase][3]) { - id(stepper_in4).turn_on(); - } else { - id(stepper_in4).turn_off(); - } - - id(coils_are_on) = true; - }; - - auto do_one_step = [&](int logical_direction, uint32_t now_ms) { - // logical_direction >= 0 means clockwise / forward / increase position. - // logical_direction < 0 means anti-clockwise / reverse / decrease position. - int motor_direction = - logical_direction >= 0 - ? CLOCKWISE_MOTOR_DIRECTION_MULTIPLIER - : -CLOCKWISE_MOTOR_DIRECTION_MULTIPLIER; - - if (logical_direction >= 0) { - id(current_position_step) = - (id(current_position_step) + 1) % STEPS_PER_ROTATION; - } else { - id(current_position_step) = - (id(current_position_step) + STEPS_PER_ROTATION - 1) % STEPS_PER_ROTATION; - } - - if (motor_direction >= 0) { - id(current_phase_index) = (id(current_phase_index) + 1) % 8; - } else { - id(current_phase_index) = (id(current_phase_index) + 7) % 8; - } - - set_coils_for_phase(id(current_phase_index)); - id(last_step_ms) = now_ms; - flash_step_led(now_ms); - }; - - const bool fast_forward_active = - id(physical_fast_forward_active) || id(web_fast_forward_active); - - const bool slow_forward_active = - id(physical_slow_forward_active) || id(web_slow_forward_active); - - const bool fast_reverse_active = - id(physical_fast_reverse_active) || id(web_fast_reverse_active); - - const bool slow_reverse_active = - id(physical_slow_reverse_active) || id(web_slow_reverse_active); - - const bool any_manual_control_active = - fast_forward_active || - slow_forward_active || - fast_reverse_active || - slow_reverse_active; - - const uint32_t now_ms = millis(); - - if (id(last_loop_ms) == 0) { - id(last_loop_ms) = now_ms; - id(step_accumulator_ms) = 0.0; - - if (id(motor_stop_switch).state) { - id(current_motion_mode) = 6; - } else { - id(current_motion_mode) = 0; - } - - return; - } - - const uint32_t elapsed_ms = now_ms - id(last_loop_ms); - id(last_loop_ms) = now_ms; - - if (id(motor_stop_switch).state) { - if (id(auto_position_active)) { - id(auto_position_active) = false; - id(auto_position_steps_remaining) = 0; - ESP_LOGW("motor_stop", "Motor Stop is ON. Auto positioning cancelled."); - } - - all_coils_off(); - id(step_accumulator_ms) = 0.0; - id(current_motion_mode) = 6; - return; - } - - if (any_manual_control_active && id(auto_position_active)) { - id(auto_position_active) = false; - id(auto_position_steps_remaining) = 0; - ESP_LOGW("auto_position", "Auto positioning cancelled by manual control."); - } - - int selected_mode = 0; - int selected_logical_direction = 1; - double selected_step_interval_ms = NORMAL_STEP_INTERVAL_MS; - - if (fast_forward_active) { - selected_mode = 1; - selected_logical_direction = 1; - selected_step_interval_ms = FAST_STEP_INTERVAL_MS; - } else if (slow_forward_active) { - selected_mode = 2; - selected_logical_direction = 1; - selected_step_interval_ms = SLOW_STEP_INTERVAL_MS; - } else if (fast_reverse_active) { - selected_mode = 3; - selected_logical_direction = -1; - selected_step_interval_ms = FAST_STEP_INTERVAL_MS; - } else if (slow_reverse_active) { - selected_mode = 4; - selected_logical_direction = -1; - selected_step_interval_ms = SLOW_STEP_INTERVAL_MS; - } else if (id(auto_position_active)) { - selected_mode = 5; - selected_logical_direction = id(auto_position_logical_direction); - - if (selected_logical_direction == 0) { - selected_logical_direction = 1; - } - - selected_step_interval_ms = AUTO_POSITION_STEP_INTERVAL_MS; - } else { - selected_mode = 0; - selected_logical_direction = 1; - selected_step_interval_ms = NORMAL_STEP_INTERVAL_MS; - } - - if (selected_mode != id(current_motion_mode)) { - id(current_motion_mode) = selected_mode; - id(step_accumulator_ms) = 0.0; - } - - id(step_accumulator_ms) += (double) elapsed_ms; - - if (id(step_accumulator_ms) >= selected_step_interval_ms) { - do_one_step(selected_logical_direction, now_ms); - - if (selected_mode == 5 && id(auto_position_active)) { - id(auto_position_steps_remaining)--; - - if (id(auto_position_steps_remaining) <= 0) { - id(auto_position_steps_remaining) = 0; - id(auto_position_active) = false; - id(auto_position_logical_direction) = 1; - id(current_motion_mode) = 0; - - ESP_LOGI( - "auto_position", - "Auto positioning complete. Current step: %d.", - id(current_position_step) - ); - } - } - - id(step_accumulator_ms) -= selected_step_interval_ms; - - // If the loop was badly delayed, avoid trying to catch up with a burst - // of very fast steps that the motor may not physically follow. - if (id(step_accumulator_ms) > selected_step_interval_ms) { - id(step_accumulator_ms) = 0.0; - } - } - - if ( - id(coils_are_on) && - selected_mode == 0 && - ((uint32_t) (now_ms - id(last_step_ms)) >= COIL_RELEASE_MS) - ) { - all_coils_off(); - } - - if ( - !id(ip_flash_active) && - id(step_led_is_on) && - ((uint32_t) (now_ms - id(step_led_started_ms)) >= STEP_LED_FLASH_MS) - ) { - id(onboard_step_led).turn_off(); - id(step_led_is_on) = false; - } - - - interval: ${ip_flash_loop_interval} - then: - - lambda: |- - if (!id(ip_flash_active)) { - return; - } - - const int IP_FLASH_REPEAT_COUNT = ${ip_flash_repeat_count}; - const int IP_FLASH_ZERO_DIGIT_BLINKS = ${ip_flash_zero_digit_blinks}; - - const uint32_t IP_FLASH_ON_MS = ${ip_flash_on_ms}; - const uint32_t IP_FLASH_BETWEEN_FLASH_GAP_MS = ${ip_flash_between_flash_gap_ms}; - const uint32_t IP_FLASH_DIGIT_GAP_MS = ${ip_flash_digit_gap_ms}; - const uint32_t IP_FLASH_OCTET_GAP_MS = ${ip_flash_octet_gap_ms}; - const uint32_t IP_FLASH_REPEAT_GAP_MS = ${ip_flash_repeat_gap_ms}; - - const uint32_t now_ms = millis(); - - if ((int32_t) (now_ms - id(ip_flash_next_action_ms)) < 0) { - return; - } - - auto get_octet = [&](int index) -> int { - switch (index) { - case 0: - return id(ip_flash_octet_0); - case 1: - return id(ip_flash_octet_1); - case 2: - return id(ip_flash_octet_2); - case 3: - default: - return id(ip_flash_octet_3); - } - }; - - auto get_digit_count = [&](int value) -> int { - if (value >= 100) { - return 3; - } - - if (value >= 10) { - return 2; - } - - return 1; - }; - - auto get_digit = [&](int value, int digit_index) -> int { - const int digit_count = get_digit_count(value); - - if (digit_count == 3) { - if (digit_index == 0) { - return value / 100; - } - - if (digit_index == 1) { - return (value / 10) % 10; - } - - return value % 10; - } - - if (digit_count == 2) { - if (digit_index == 0) { - return value / 10; - } - - return value % 10; - } - - return value; - }; - - const int current_octet = get_octet(id(ip_flash_octet_index)); - const int current_digit_count = get_digit_count(current_octet); - const int current_digit = get_digit(current_octet, id(ip_flash_digit_index)); - const int target_blinks = - current_digit == 0 ? IP_FLASH_ZERO_DIGIT_BLINKS : current_digit; - - if (id(ip_flash_led_is_on)) { - id(onboard_step_led).turn_off(); - id(ip_flash_led_is_on) = false; - id(ip_flash_blinks_completed)++; - - if (id(ip_flash_blinks_completed) < target_blinks) { - id(ip_flash_next_action_ms) = now_ms + IP_FLASH_BETWEEN_FLASH_GAP_MS; - return; - } - - id(ip_flash_blinks_completed) = 0; - id(ip_flash_digit_index)++; - - if (id(ip_flash_digit_index) < current_digit_count) { - id(ip_flash_next_action_ms) = now_ms + IP_FLASH_DIGIT_GAP_MS; - return; - } - - id(ip_flash_digit_index) = 0; - id(ip_flash_octet_index)++; - - if (id(ip_flash_octet_index) < 4) { - id(ip_flash_next_action_ms) = now_ms + IP_FLASH_OCTET_GAP_MS; - return; - } - - id(ip_flash_octet_index) = 0; - id(ip_flash_repeats_completed)++; - - if (id(ip_flash_repeats_completed) >= IP_FLASH_REPEAT_COUNT) { - id(ip_flash_active) = false; - id(ip_flash_led_is_on) = false; - id(onboard_step_led).turn_off(); - - ESP_LOGI("ip_flash", "Finished flashing IPv4 address."); - return; - } - - id(ip_flash_next_action_ms) = now_ms + IP_FLASH_REPEAT_GAP_MS; - return; - } - - id(onboard_step_led).turn_on(); - id(ip_flash_led_is_on) = true; - id(ip_flash_next_action_ms) = now_ms + IP_FLASH_ON_MS; - - -#:########################################################################################:# -# MQTT: -# Device-specific MQTT command triggers, in addition to common MQTT config -# https://esphome.io/components/mqtt.html -#:########################################################################################:# -#mqtt: -# on_message: -# - topic: "${mqtt_command_topic}/fast_forward/set" -# payload: "ON" -# then: -# - switch.turn_on: web_fast_forward_switch -# -# - topic: "${mqtt_command_topic}/fast_forward/set" -# payload: "OFF" -# then: -# - switch.turn_off: web_fast_forward_switch -# -# - topic: "${mqtt_command_topic}/slow_forward/set" -# payload: "ON" -# then: -# - switch.turn_on: web_slow_forward_switch -# -# - topic: "${mqtt_command_topic}/slow_forward/set" -# payload: "OFF" -# then: -# - switch.turn_off: web_slow_forward_switch -# -# - topic: "${mqtt_command_topic}/fast_reverse/set" -# payload: "ON" -# then: -# - switch.turn_on: web_fast_reverse_switch -# -# - topic: "${mqtt_command_topic}/fast_reverse/set" -# payload: "OFF" -# then: -# - switch.turn_off: web_fast_reverse_switch -# -# - topic: "${mqtt_command_topic}/slow_reverse/set" -# payload: "ON" -# then: -# - switch.turn_on: web_slow_reverse_switch -# -# - topic: "${mqtt_command_topic}/slow_reverse/set" -# payload: "OFF" -# then: -# - switch.turn_off: web_slow_reverse_switch \ No newline at end of file diff --git a/esphome/esp-ledmatrix1 copy.yaml.V1 b/esphome/esp-ledmatrix1 copy.yaml.V1 deleted file mode 100644 index c932298..0000000 --- a/esphome/esp-ledmatrix1 copy.yaml.V1 +++ /dev/null @@ -1,916 +0,0 @@ -#:########################################################################################:# -# TITLE: LED MATRIX 1 -# zorruno.com layout v1.1 2026 -# ESP32-C3 SuperMini MAX7219 LED matrix clock and scroll display. -#:########################################################################################:# -# REFERENCES: -# Original Project Reference https://github.com/RealDeco/matrixclock-esphome -# Fonts: https://github.com/RealDeco/matrixclock-esphome/tree/main/fonts -# 3D Printed Case (includes project notes in the 3mf files) -# https://github.com/RealDeco/matrixclock-esphome/tree/main/3DPrint -#:########################################################################################:# -# REPO: -# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-ledmatrix1.yaml -#:########################################################################################:# -# VERSIONS: -# V1.3 2026-05-24 Added native API action for Home Assistant direct scroll text commands. -# V1.2 2026-05-24 Added 1-15 brightness level slider and improved operation notes. -# V1.1 2026-05-23 Updated yaml to zorruno layout V1.1, cleaned comments, fixed SNTP time IDs, -# added current_mins fallback display, removed unused MQTT topic, and fixed -# blank scroll handling. -# V1.0 2026-05-23 Initial Version -#:########################################################################################:# -# HARDWARE: -# - ESP32-C3 SuperMini. -# - MAX7219 4-chip LED matrix display, typically 32 x 8 pixels. -# - SPI wiring configured below: -# - MOSI: GPIO8 -# - CS: GPIO9 -# - SCK: GPIO10 -# - Power note: 4 MAX7219 modules needs a suitable external 5 V supply. -# - Keep ESP32-C3 GND and matrix power supply GND connected together. -#:########################################################################################:# -# OPERATION NOTES: -# - Main display: -# - Shows a 32 x 8 LED matrix clock using the MAX7219 display component. -# - Uses common/sntp_common.yaml as the primary time source via id(sntp_time). -# - If SNTP is not valid, uses id(current_mins) as a fallback clock. -# - In fallback mode the colon stays solid, so it is obvious time is not fully synced. -# -# - Clock controls: -# - Clock format can be selected as 12h or 24h from Home Assistant or MQTT. -# - 12h mode draws a small A or P indicator at the far right of the display. -# -# - Brightness controls: -# - The Display light entity can turn the matrix on/off and set proportional brightness. -# - The Brightness Level number slider sets the exact MAX7219 intensity from 1 to 15. -# - MQTT brightness accepts 0 to turn the display off, or 1-15 to set exact intensity. -# - The 1-15 slider keeps the last non-zero brightness value when the display is off. -# -# - Scroll text controls: -# - Scroll Text is exposed as a Home Assistant text entity. -# - Native API action esphome.esp_ledmatrix1_scroll_text can scroll text directly from Home Assistant. -# - The text box clears itself after a valid scroll request is accepted. -# - Empty scroll requests are ignored and do not replay the previous message. -# - Scroll Delay controls the delay in milliseconds between scroll steps. -# -# - Scroll transitions: -# - Before scrolling, the clock always slides down and off the display. -# - After scrolling, the selected re-entry effect runs. -# - Available re-entry effects are Slide up, Slide down, and Pixel fall. -# - Pixel fall hides the clock until the particle effect finishes. -#:########################################################################################:# -# MQTT COMMANDS: -# - ${mqtt_command_topic}/scroll : Text payload to scroll once across the display. -# - ${mqtt_command_topic}/mode : Payload 12 or 24 to select clock format. -# - ${mqtt_command_topic}/bright : Payload 0 turns display off, 1-15 sets exact brightness. -# - ${mqtt_status_topic}/status : Publishes online/offline status. -#:########################################################################################:# -# HOME ASSISTANT API ACTIONS: -# - esphome.esp_ledmatrix1_scroll_text -# data: -# message: "Text to scroll" -#:########################################################################################:# -# OFFLINE NOTES: -# a) Home Assistant offline, but WiFi/network and MQTT online: -# - Clock display should continue using SNTP if the NTP servers are reachable. -# - MQTT scroll, brightness, and 12h/24h mode commands should still work. -# -# b) MQTT offline, but WiFi/network and Home Assistant API online: -# - Home Assistant text, light, number, and select entities should still work. -# - Clock display should continue using SNTP if the NTP servers are reachable. -# -# c) Entire WiFi/network offline: -# - SNTP will not be available after boot. -# - Clock display will use the current_mins fallback from common/sntp_common.yaml. -# - Accurate time is needed for the clock, so fallback time will drift. -# - For a rough reset, power-cycle the device at 12:00 noon. -# - Home Assistant and MQTT controls will not be available while offline. -#:########################################################################################:# - -#:########################################################################################:# -# SUBSTITUTIONS: Specific device variable substitutions -# If NOT using a secrets file, just replace these with the passwords etc in quotes. -#:########################################################################################:# -substitutions: - # Device Naming - device_name: "esp-ledmatrix1" - friendly_name: "LED Matrix Display" - description_comment: "LED Matrix Display (Layout V1.1)" - device_area: "Lounge" # Allows ESP device to be automatically linked to an Area in Home Assistant. - - # Project Naming - project_name: "Generic.ESP32 Supermini" - project_version: "v1.3" - - # Passwords & Secrets - api_key: !secret esp-api_key - ota_pass: !secret esp-ota_pass - static_ip_address: !secret esp-ledmatrix1_ip # Substitutions cannot be used inside secret names. - mqtt_command_main_topic: !secret mqtt_command_main_topic - mqtt_status_main_topic: !secret mqtt_status_main_topic - - # If changing IP addresses, update current_ip_address here, otherwise it remains static_ip_address. - # Do not forget to switch it back when the address change is complete. - current_ip_address: ${static_ip_address} - - # MQTT LOCAL Controls - mqtt_device_name: "ledmatrix1" - mqtt_command_topic: "${mqtt_command_main_topic}/${mqtt_device_name}" - mqtt_status_topic: "${mqtt_status_main_topic}/${mqtt_device_name}" - - # Device Settings - log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE - update_interval: "60s" - - # Matrix Display Settings - pin_mosi: GPIO8 - pin_cs: GPIO9 - pin_sck: GPIO10 - num_chips: "4" - scroll_delay_ms: "15" # ms between scroll steps - matrix_initial_brightness: "2" # Default MAX7219 intensity, 1-15. - y_offset: "0" - y_scroll_offset: "0" - -#:########################################################################################:# -# PACKAGES: Included Common Packages -# https://esphome.io/components/packages.html -#:########################################################################################:# -packages: - #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### - common_wifi: !include - file: common/network_common.yaml - vars: - local_device_name: "${device_name}" - local_static_ip_address: "${static_ip_address}" - local_ota_pass: "${ota_pass}" - local_current_ip_address: "${current_ip_address}" - - #### HOME ASSISTANT API (choose encryption or no encryption options) #### - common_api: !include - file: common/api_common.yaml - #file: common/api_common_noencryption.yaml - vars: - local_api_key: "${api_key}" - - #### MQTT #### - common_mqtt: !include - file: common/mqtt_common.yaml - vars: - local_device_name: "${device_name}" - - #### WEB PORTAL #### - common_webportal: !include common/webportal_common.yaml - - #### SNTP (Needed for the clock display) #### - common_sntp: !include common/sntp_common.yaml - - #### DIAGNOSTICS Sensors #### - diag_basic: !include common/include_basic_diag_sensors.yaml - #diag_more: !include common/include_more_diag_sensors.yaml - #diag_debug: !include common/include_debug_diag_sensors.yaml - #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml - -#:########################################################################################:# -# ESPHOME: -# https://esphome.io/components/esphome.html -#:########################################################################################:# -esphome: - name: "${device_name}" - friendly_name: "${friendly_name}" - comment: "${description_comment}" # Appears on the ESPHome page in Home Assistant. - area: "${device_area}" - project: - name: "${project_name}" - version: "${project_version}" - on_boot: - priority: 600 - then: - # Ensure Home Assistant shows the text box as empty rather than unknown. - - delay: 500ms - - lambda: |- - id(ha_scroll_text).publish_state(""); - - component.update: matrix - -#:########################################################################################:# -# ESP PLATFORM AND FRAMEWORK: -# https://esphome.io/components/esp32/ -#:########################################################################################:# -esp32: - variant: esp32c3 - framework: - type: esp-idf - version: recommended - -#esp8266: -# board: esp01_1m -# early_pin_init: false # Recommended false where switches are involved. Defaults to true. -# board_flash_mode: dout # Default is dout. - -#:########################################################################################:# -# LOGGER: ESPHome Logging Enable -# https://esphome.io/components/logger.html -#:########################################################################################:# -logger: - level: "${log_level}" # INFO suggested, or DEBUG for testing. - #baud_rate: 0 # Set to 0 for no UART logging if UART is used for another device. - #esp8266_store_log_strings_in_flash: false - #tx_buffer_size: 64 - -#:########################################################################################:# -# API COMPONENT: Device-specific Home Assistant native API actions -# https://esphome.io/components/api.html -#:########################################################################################:# -api: - actions: - # Home Assistant action: esphome.esp_ledmatrix1_scroll_text - # Example HA data: { message: "Hello from Home Assistant" } - - action: scroll_text - variables: - message: string - then: - - lambda: |- - std::string s = message; - - s.erase(std::remove(s.begin(), s.end(), '\r'), s.end()); - s.erase(std::remove(s.begin(), s.end(), '\n'), s.end()); - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20; }), s.end()); - - // Ignore empty API scroll commands completely. - if (s.empty()) return; - - // Keep the HA text entity visually in sync with this direct API action. - id(ha_scroll_text).publish_state(s.c_str()); - - id(scroll_text) = s; - id(scroll_x) = ${num_chips} * 8; - id(start_scroll_sequence).execute(); - id(clear_scroll_text_box).execute(); - -#:########################################################################################:# -# GLOBALS: -# https://esphome.io/components/globals.html -#:########################################################################################:# -globals: - # Text currently being scrolled across the matrix. - - id: scroll_text - type: std::string - initial_value: "" - - # Current X position of the scrolling text. - - id: scroll_x - type: int - initial_value: '0' - - # True while a scroll message is active. - - id: scrolling - type: bool - initial_value: 'false' - - # False = 24h mode, true = 12h mode. - - id: use_12h_mode - type: bool - restore_value: true - initial_value: 'false' - - # Runtime scroll delay, controlled by the Scroll Delay number entity. - - id: scroll_delay_runtime - type: int - restore_value: true - initial_value: '${scroll_delay_ms}' - - # MAX7219 intensity: 0=OFF, 1..15=ON. Driven by the HA light and brightness slider. - - id: matrix_intensity - type: int - restore_value: false - initial_value: '${matrix_initial_brightness}' - - # Last non-zero brightness level selected by HA, MQTT, or the 1-15 slider. - # This allows the exact slider to keep its value when the display is switched off. - - id: matrix_last_nonzero_intensity - type: int - restore_value: true - initial_value: '${matrix_initial_brightness}' - - # Brightness sync loop protection for HA light <-> MQTT brightness updates. - - id: suppress_mqtt_bright_sync - type: bool - restore_value: false - initial_value: 'false' - - - id: target_mqtt_bright - type: int - restore_value: false - initial_value: '-1' - - - id: last_mqtt_bright - type: int - restore_value: false - initial_value: '-1' - - # Re-entry effect after scrolling: 0=Slide up, 1=Slide down, 2=Pixel fall. - - id: transition_mode - type: int - restore_value: true - initial_value: '0' - - # Slide transition state. - - id: transition_active - type: bool - restore_value: false - initial_value: 'false' - - - id: transition_y - type: int - restore_value: false - initial_value: '0' - - # Pixel-fall transition state. - - id: pixel_fall_active - type: bool - restore_value: false - initial_value: 'false' - - - id: pixel_fall_gen - type: int - restore_value: false - initial_value: '0' - -#:########################################################################################:# -# FONT COMPONENT: -# https://esphome.io/components/font/ -#:########################################################################################:# -font: - # Clock digits. Keep glyphs limited to reduce firmware size. - - file: "fonts/Eight-Bit-Dragon.ttf" - id: digit5 - size: 8 - glyphs: ["0123456789: -"] - - # General 5x8 font for scroll text. - - file: "fonts/5x8.bdf" - id: font5x8 - size: 8 - -#:########################################################################################:# -# SPI COMPONENT: -# https://esphome.io/components/spi.html -#:########################################################################################:# -spi: - clk_pin: ${pin_sck} - mosi_pin: ${pin_mosi} - -#:########################################################################################:# -# MQTT: Device-specific MQTT command triggers -# This adds device-specific MQTT command triggers to the common MQTT configuration. -#:########################################################################################:# -mqtt: - discovery: false - birth_message: - topic: "${mqtt_status_topic}/status" - payload: "online" - qos: 0 - retain: true - will_message: - topic: "${mqtt_status_topic}/status" - payload: "offline" - qos: 0 - retain: true - on_message: - # Scroll a text payload once across the matrix. - - topic: "${mqtt_command_topic}/scroll" - then: - - lambda: |- - std::string s = x.c_str(); - s.erase(std::remove(s.begin(), s.end(), '\r'), s.end()); - s.erase(std::remove(s.begin(), s.end(), '\n'), s.end()); - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20; }), s.end()); - - // Ignore empty MQTT scroll commands completely. - if (s.empty()) return; - - id(scroll_text) = s; - id(scroll_x) = ${num_chips} * 8; - id(start_scroll_sequence).execute(); - - # Set 12h or 24h clock mode from MQTT. - - topic: "${mqtt_command_topic}/mode" - then: - - lambda: |- - if (x == "12") { - id(use_12h_mode) = true; - } else if (x == "24") { - id(use_12h_mode) = false; - } - - component.update: clock_format_select - - component.update: matrix - - # Brightness from MQTT. 0=OFF, 1..15=ON brightness level. - - topic: "${mqtt_command_topic}/bright" - then: - - lambda: |- - std::string s = x.c_str(); - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20 || c == 0x7F; }), s.end()); - s.erase(s.begin(), std::find_if(s.begin(), s.end(), - [](unsigned char ch){ return !std::isspace(ch); })); - s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch){ return !std::isspace(ch); }).base(), s.end()); - - if (s.empty() || !std::all_of(s.begin(), s.end(), - [](unsigned char c){ return std::isdigit(c); })) return; - - int v = atoi(s.c_str()); - if (v < 0) v = 0; - if (v > 15) v = 15; - - id(suppress_mqtt_bright_sync) = true; - - if (v == 0) { - id(matrix_light).turn_off().perform(); - } else { - id(matrix_last_nonzero_intensity) = v; - id(matrix_brightness_number).publish_state((float) v); - - auto call = id(matrix_light).turn_on(); - call.set_brightness((float) v / 15.0f); - call.perform(); - } - - id(last_mqtt_bright) = v; - id(suppress_mqtt_bright_sync) = false; - -#:########################################################################################:# -# TEXT COMPONENT: -# https://esphome.io/components/text/template/ -#:########################################################################################:# -text: - - platform: template - id: ha_scroll_text - name: "${friendly_name} Scroll Text" - icon: "mdi:message-text-outline" - mode: text - max_length: 80 - optimistic: false - set_action: - - lambda: |- - std::string s = x.c_str(); - - s.erase(std::remove(s.begin(), s.end(), '\r'), s.end()); - s.erase(std::remove(s.begin(), s.end(), '\n'), s.end()); - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20; }), s.end()); - - // Publish the cleaned text so the HA UI updates immediately. - id(ha_scroll_text).publish_state(s.c_str()); - - // Empty text only clears the entity. It should not start a scroll. - if (s.empty()) return; - - id(scroll_text) = s; - id(scroll_x) = ${num_chips} * 8; - id(start_scroll_sequence).execute(); - id(clear_scroll_text_box).execute(); - -#:########################################################################################:# -# NUMBER COMPONENT: -# https://esphome.io/components/number/template/ -#:########################################################################################:# -number: - - platform: template - id: scroll_delay_number - name: "${friendly_name} Scroll Delay" - icon: "mdi:timer-outline" - unit_of_measurement: "ms" - min_value: 1 - max_value: 100 - step: 1 - restore_value: true - initial_value: ${scroll_delay_ms} - optimistic: true - set_action: - - lambda: |- - id(scroll_delay_runtime) = (int) x; - - - platform: template - id: matrix_brightness_number - name: "${friendly_name} Brightness Level" - icon: "mdi:brightness-6" - min_value: 1 - max_value: 15 - step: 1 - restore_value: true - initial_value: ${matrix_initial_brightness} - optimistic: true - set_action: - - lambda: |- - int v = (int) lroundf(x); - if (v < 1) v = 1; - if (v > 15) v = 15; - - id(matrix_last_nonzero_intensity) = v; - - // Setting the exact brightness slider also turns the display on. - auto call = id(matrix_light).turn_on(); - call.set_brightness((float) v / 15.0f); - call.perform(); - -#:########################################################################################:# -# OUTPUT COMPONENT: -# https://esphome.io/components/output/ -#:########################################################################################:# -output: - # Template output lets the HA light and brightness slider control MAX7219 intensity. - - platform: template - id: matrix_brightness_out - type: float - write_action: - - lambda: |- - int pub = 0; - - if (state <= 0.001f) { - id(matrix_intensity) = 0; - id(matrix).turn_on_off(false); - pub = 0; - } else { - int v = (int) lroundf(state * 15.0f); - if (v < 1) v = 1; - if (v > 15) v = 15; - - id(matrix_intensity) = v; - id(matrix_last_nonzero_intensity) = v; - id(matrix_brightness_number).publish_state((float) v); - id(matrix).turn_on_off(true); - pub = v; - } - - id(target_mqtt_bright) = pub; - - - component.update: matrix - - # Publish brightness back to MQTT unless this update came from MQTT. - - if: - condition: - lambda: |- - return !id(suppress_mqtt_bright_sync) - && (id(target_mqtt_bright) != id(last_mqtt_bright)); - then: - - mqtt.publish: - topic: "${mqtt_command_topic}/bright" - payload: !lambda |- - char buf[6]; - snprintf(buf, sizeof(buf), "%d", id(target_mqtt_bright)); - return std::string(buf); - qos: 0 - retain: true - - lambda: |- - id(last_mqtt_bright) = id(target_mqtt_bright); - -#:########################################################################################:# -# LIGHT COMPONENT: -# https://esphome.io/components/light/monochromatic/ -#:########################################################################################:# -light: - - platform: monochromatic - id: matrix_light - name: "${friendly_name} Display" - icon: "mdi:brightness-6" - output: matrix_brightness_out - gamma_correct: 1.0 - default_transition_length: 0s - restore_mode: RESTORE_DEFAULT_ON - -#:########################################################################################:# -# SELECT COMPONENT: -# https://esphome.io/components/select/template/ -#:########################################################################################:# -select: - - platform: template - id: clock_format_select - name: "${friendly_name} Clock Format" - icon: "mdi:clock-time-three-outline" - options: - - "24" - - "12" - lambda: |- - return esphome::optional(std::string(id(use_12h_mode) ? "12" : "24")); - set_action: - - lambda: |- - id(use_12h_mode) = (x == "12"); - - component.update: clock_format_select - - component.update: matrix - - - platform: template - id: transition_select - name: "${friendly_name} Transition Effect" - icon: "mdi:transition" - options: - - "Slide up" - - "Slide down" - - "Pixel fall" - lambda: |- - if (id(transition_mode) == 2) return std::string("Pixel fall"); - if (id(transition_mode) == 1) return std::string("Slide down"); - return std::string("Slide up"); - set_action: - - lambda: |- - if (x == "Pixel fall") id(transition_mode) = 2; - else if (x == "Slide down") id(transition_mode) = 1; - else id(transition_mode) = 0; - - component.update: transition_select - -#:########################################################################################:# -# SCRIPT COMPONENT: -# https://esphome.io/components/script.html -#:########################################################################################:# -script: - # Clear the HA text box shortly after a scroll request has been accepted. - - id: clear_scroll_text_box - mode: restart - then: - - delay: 400ms - - lambda: |- - id(ha_scroll_text).publish_state(""); - - # Sequence: exit slide-down -> scroll -> selected re-entry. - - id: start_scroll_sequence - mode: restart - then: - - lambda: |- - id(scrolling) = false; - id(pixel_fall_active) = false; - - component.update: matrix - - script.execute: clock_exit_down - - script.wait: clock_exit_down - - lambda: |- - id(scrolling) = true; - - script.execute: do_scroll - - # Scroll the stored text until it has moved fully off the left side. - - id: do_scroll - mode: restart - then: - - while: - condition: - lambda: |- - return id(scrolling); - then: - - lambda: |- - id(scroll_x) -= 1; - const int end_limit = - (int(id(scroll_text).length()) * 6); - if (id(scroll_x) < end_limit) { - id(scrolling) = false; - - if (id(transition_mode) == 1) { - id(clock_slide_down_entry).execute(); - } else if (id(transition_mode) == 2) { - id(clock_pixel_fall_transition).execute(); - } else { - id(clock_slide_up_entry).execute(); - } - } - - component.update: matrix - - delay: !lambda |- - return (uint32_t) id(scroll_delay_runtime); - - # Exit: slide clock down off-screen and hold it there to prevent a one-frame flash. - - id: clock_exit_down - mode: restart - then: - - lambda: |- - id(transition_active) = true; - id(transition_y) = ${y_offset}; - - repeat: - count: 9 - then: - - lambda: |- - id(transition_y) += 1; - - component.update: matrix - - delay: 45ms - - lambda: |- - id(transition_active) = true; - id(transition_y) = 8; - - component.update: matrix - - # Re-entry: clock slides down into place from above. - - id: clock_slide_down_entry - mode: restart - then: - - lambda: |- - id(transition_active) = true; - id(transition_y) = -8; - - repeat: - count: 9 - then: - - lambda: |- - id(transition_y) += 1; - - component.update: matrix - - delay: 60ms - - lambda: |- - id(transition_active) = false; - id(transition_y) = ${y_offset}; - - component.update: matrix - - # Re-entry: clock slides up into place from below. - - id: clock_slide_up_entry - mode: restart - then: - - lambda: |- - id(transition_active) = true; - id(transition_y) = 8; - - repeat: - count: 9 - then: - - lambda: |- - id(transition_y) -= 1; - - component.update: matrix - - delay: 60ms - - lambda: |- - id(transition_active) = false; - id(transition_y) = ${y_offset}; - - component.update: matrix - - # Re-entry: falling pixels effect before the clock redraws. - - id: clock_pixel_fall_transition - mode: restart - then: - - lambda: |- - id(transition_active) = false; - id(transition_y) = ${y_offset}; - id(pixel_fall_active) = true; - id(pixel_fall_gen) += 1; - - repeat: - count: 18 - then: - - component.update: matrix - - delay: 55ms - - lambda: |- - id(pixel_fall_active) = false; - id(transition_active) = false; - id(transition_y) = ${y_offset}; - - component.update: matrix - -#:########################################################################################:# -# DISPLAY COMPONENT: -# https://esphome.io/components/display/max7219digit/ -#:########################################################################################:# -display: - - platform: max7219digit - id: matrix - cs_pin: ${pin_cs} - num_chips: ${num_chips} - intensity: 2 - update_interval: 500ms - rotate_chip: 0 - reverse_enable: false - flip_x: false - - lambda: |- - // Apply brightness only when it changes to reduce flicker. - static int last_brightness = -1; - if (id(matrix_intensity) != last_brightness) { - if (id(matrix_intensity) > 0) { - int v = id(matrix_intensity); - if (v > 15) v = 15; - it.intensity((uint8_t) v); - } - last_brightness = id(matrix_intensity); - } - - if (id(matrix_intensity) == 0) return; - - it.clear(); - - // Scroll text has priority over clock display. - if (id(scrolling)) { - it.print(id(scroll_x), ${y_scroll_offset}, id(font5x8), id(scroll_text).c_str()); - return; - } - - // Pixel fall shows only particles while active. - if (id(pixel_fall_active)) { - static int last_gen = -1; - static int px[24]; - static int py[24]; - static uint32_t rng = 1; - - auto rand_u32 = [&]() -> uint32_t { - rng = rng * 1664525u + 1013904223u; - return rng; - }; - - if (last_gen != id(pixel_fall_gen)) { - last_gen = id(pixel_fall_gen); - rng = 0xA5A5A5A5u ^ (uint32_t) id(pixel_fall_gen); - for (int i = 0; i < 24; i++) { - px[i] = (int)(rand_u32() % 32); - py[i] = - (int)(rand_u32() % 16); - } - } - - for (int i = 0; i < 24; i++) { - int spd = 1 + (int)(rand_u32() % 2); - py[i] += spd; - - if (py[i] > 10) { - px[i] = (int)(rand_u32() % 32); - py[i] = - (int)(rand_u32() % 10); - } - - for (int t = 0; t < 2; t++) { - int yy = py[i] - t; - int xx = px[i]; - if (xx >= 0 && xx <= 31 && yy >= 0 && yy <= 7) { - it.draw_pixel_at(xx, yy, Color::WHITE); - } - } - } - return; - } - - // Prefer real SNTP time. Fall back to current_mins from common/sntp_common.yaml. - auto now = id(sntp_time).now(); - bool time_valid = now.is_valid(); - - int raw_hour = 0; - int minute_to_show = 0; - int second_to_show = 0; - - if (time_valid) { - raw_hour = now.hour; - minute_to_show = now.minute; - second_to_show = now.second; - } else { - int mins = id(current_mins); - if (mins < 0) mins = 0; - if (mins >= 1440) mins = mins % 1440; - - raw_hour = mins / 60; - minute_to_show = mins % 60; - second_to_show = 0; - } - - int hour_to_show = raw_hour; - if (id(use_12h_mode)) { - hour_to_show = raw_hour % 12; - if (hour_to_show == 0) hour_to_show = 12; - } - - char hh[3], mm[3]; - snprintf(hh, sizeof(hh), "%02d", hour_to_show); - snprintf(mm, sizeof(mm), "%02d", minute_to_show); - - const int x_h1 = 3; - const int x_h2 = 9; - const int x_m1 = 17; - const int x_m2 = 23; - - const int y = (id(transition_active) ? id(transition_y) : ${y_offset}); - - char ch[2] = { hh[0], 0 }; - it.print(x_h1, y, id(digit5), ch); - ch[0] = hh[1]; - it.print(x_h2, y, id(digit5), ch); - - ch[0] = mm[0]; - it.print(x_m1, y, id(digit5), ch); - ch[0] = mm[1]; - it.print(x_m2, y, id(digit5), ch); - - // In 12h mode, draw a tiny A or P indicator at the far right. - if (id(use_12h_mode)) { - const bool is_pm = raw_hour >= 12; - - const uint8_t A_rows[4] = { 2, 5, 7, 5 }; - const uint8_t P_rows[4] = { 6, 5, 6, 4 }; - const uint8_t *L = is_pm ? P_rows : A_rows; - - const int x_ampm = 29; - const int y_ampm = y + 4; - - for (int r = 0; r < 4; r++) { - uint8_t row = L[r]; - for (int c = 0; c < 3; c++) { - if (row & (1 << (2 - c))) { - int xx = x_ampm + c; - int yy = y_ampm + r; - if (yy >= 0 && yy <= 7 && xx >= 0 && xx <= 31) { - it.draw_pixel_at(xx, yy, Color::WHITE); - } - } - } - } - } - - // Blink the colon when SNTP is valid. Keep it solid during fallback time. - const bool colon_on = time_valid ? ((second_to_show % 2) == 0) : true; - if (colon_on) { - const int x_col = (x_h2 + x_m1) / 2 + 2; - int y1 = y + 2; - int y2 = y + 5; - if (y1 >= 0 && y1 <= 7) it.draw_pixel_at(x_col, y1, Color::WHITE); - if (y2 >= 0 && y2 <= 7) it.draw_pixel_at(x_col, y2, Color::WHITE); - } \ No newline at end of file diff --git a/esphome/esp-ledmatrix1.yaml.V2 b/esphome/esp-ledmatrix1.yaml.V2 deleted file mode 100644 index c2d6ab3..0000000 --- a/esphome/esp-ledmatrix1.yaml.V2 +++ /dev/null @@ -1,1397 +0,0 @@ -#:########################################################################################:# -# TITLE: LED MATRIX 1 -# zorruno.com layout v1.1 2026 -# ESP32-C3 SuperMini MAX7219 LED matrix clock and scroll display. -#:########################################################################################:# -# REFERENCES: -# Original Project Reference https://github.com/RealDeco/matrixclock-esphome -# Fonts: https://github.com/RealDeco/matrixclock-esphome/tree/main/fonts -# 3D Printed Case (includes project notes in the 3mf files) -# https://github.com/RealDeco/matrixclock-esphome/tree/main/3DPrint -#:########################################################################################:# -# REPO: -# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-ledmatrix1.yaml -#:########################################################################################:# -# VERSIONS: -# V1.4 2026-06-01 Added queued MQTT announcement mode with cancel IDs, per-message speed, -# per-message transition mode, repeat scheduling, and optional sounder GPIO. -# V1.3 2026-05-24 Added native API action for Home Assistant direct scroll text commands. -# V1.2 2026-05-24 Added 1-15 brightness level slider and improved operation notes. -# V1.1 2026-05-23 Updated yaml to zorruno layout V1.1, cleaned comments, fixed SNTP time IDs, -# added current_mins fallback display, removed unused MQTT topic, and fixed -# blank scroll handling. -# V1.0 2026-05-23 Initial Version -#:########################################################################################:# -# HARDWARE: -# - ESP32-C3 SuperMini. -# - MAX7219 4-chip LED matrix display, typically 32 x 8 pixels. -# - SPI wiring configured below: -# - MOSI: GPIO8 -# - CS: GPIO9 -# - SCK: GPIO10 -# - Optional sounder / beeper output defaults to GPIO4. -# - Power note: 4 MAX7219 modules needs a suitable external 5 V supply. -# - Keep ESP32-C3 GND and matrix power supply GND connected together. -#:########################################################################################:# -# OPERATION NOTES: -# - Main display: -# - Shows a 32 x 8 LED matrix clock using the MAX7219 display component. -# - Uses common/sntp_common.yaml as the primary time source via id(sntp_time). -# - If SNTP is not valid, uses id(current_mins) as a fallback clock. -# - In fallback mode the colon stays solid, so it is obvious time is not fully synced. -# -# - Clock controls: -# - Clock format can be selected as 12h or 24h from Home Assistant or MQTT. -# - 12h mode draws a small A or P indicator at the far right of the display. -# -# - Brightness controls: -# - The Display light entity can turn the matrix on/off and set proportional brightness. -# - The Brightness Level number slider sets the exact MAX7219 intensity from 1 to 15. -# - MQTT brightness accepts 0 to turn the display off, or 1-15 to set exact intensity. -# - The 1-15 slider keeps the last non-zero brightness value when the display is off. -# -# - Scroll text controls: -# - Scroll Text is exposed as a Home Assistant text entity. -# - Native API action esphome.esp_ledmatrix1_scroll_text can scroll text directly from Home Assistant. -# - The text box clears itself after a valid scroll request is accepted. -# - Empty scroll requests are ignored and do not replay the previous message. -# - Scroll Delay controls the default delay in milliseconds between scroll steps. -# - MQTT /scroll uses the selected default scroll delay and selected default transition effect. -# -# - Announcement controls: -# - MQTT /announce queues repeated announcement messages with per-message parameters. -# - Announcement format is: -# Message text [identifier,transition_mode,speed,repeat_seconds,repeat_times,sounder_quantity] -# - identifier is cleaned to letters, numbers, underscore, or dash, and is limited to 8 characters. -# - transition_mode is 0, 1, or 2 for Slide up, Slide down, or Pixel fall. -# - speed is the per-message scroll delay in ms, clamped from 1 to 100. -# - repeat_seconds is the delay between completed scrolls, clamped from 0 to 99. -# - repeat_times is the total number of scroll displays, clamped from 0 to 99. -# - repeat_times 0 is treated as a one-shot announcement. -# - sounder_quantity is the number of announcement displays that should beep first. -# - sounder_quantity 0 means no sounder beep. -# - If a new announcement arrives with the same identifier, older queued copies are removed -# and the current matching announcement is cancelled after its current scroll finishes. -# - MQTT /cancel_id cancels future repeats for a queued or active announcement ID. -# - The active scroll is allowed to finish cleanly when cancelled. -# -# - Scroll transitions: -# - Before scrolling, the clock always slides down and off the display. -# - After scrolling, the selected re-entry effect runs. -# - Available re-entry effects are Slide up, Slide down, and Pixel fall. -# - Pixel fall hides the clock until the particle effect finishes. -#:########################################################################################:# -# MQTT COMMANDS: -# - ${mqtt_command_topic}/scroll : Text payload to scroll once across the display. -# - ${mqtt_command_topic}/announce : Queued announcement payload: -# Message text [id,transition,speed,repeat_seconds,repeat_times,sounder_quantity] -# - ${mqtt_command_topic}/cancel_id : Payload is the short ID to cancel future repeats. -# - ${mqtt_command_topic}/mode : Payload 12 or 24 to select clock format. -# - ${mqtt_command_topic}/bright : Payload 0 turns display off, 1-15 sets exact brightness. -# - ${mqtt_status_topic}/status : Publishes online/offline status. -# -# - Example announce payload: -# Washing machine complete [wash,1,15,10,6,1] -#:########################################################################################:# -# HOME ASSISTANT API ACTIONS: -# - esphome.esp_ledmatrix1_scroll_text -# data: -# message: "Text to scroll" -#:########################################################################################:# -# OFFLINE NOTES: -# a) Home Assistant offline, but WiFi/network and MQTT online: -# - Clock display should continue using SNTP if the NTP servers are reachable. -# - MQTT scroll, announce, cancel_id, brightness, and 12h/24h mode commands should still work. -# -# b) MQTT offline, but WiFi/network and Home Assistant API online: -# - Home Assistant text, light, number, and select entities should still work. -# - Clock display should continue using SNTP if the NTP servers are reachable. -# - MQTT announce and cancel_id commands will not be available. -# -# c) Entire WiFi/network offline: -# - SNTP will not be available after boot. -# - Clock display will use the current_mins fallback from common/sntp_common.yaml. -# - Accurate time is needed for the clock, so fallback time will drift. -# - For a rough reset, power-cycle the device at 12:00 noon. -# - Home Assistant and MQTT controls will not be available while offline. -#:########################################################################################:# - -#:########################################################################################:# -# SUBSTITUTIONS: Specific device variable substitutions -# If NOT using a secrets file, just replace these with the passwords etc in quotes. -#:########################################################################################:# -substitutions: - # Device Naming - device_name: "esp-ledmatrix1" - friendly_name: "LED Matrix Display" - description_comment: "LED Matrix Display (Layout V1.1)" - device_area: "Lounge" # Allows ESP device to be automatically linked to an Area in Home Assistant. - - # Project Naming - project_name: "Generic.ESP32 Supermini" - project_version: "v1.4" - - # Passwords & Secrets - api_key: !secret esp-api_key - ota_pass: !secret esp-ota_pass - static_ip_address: !secret esp-ledmatrix1_ip # Substitutions cannot be used inside secret names. - mqtt_command_main_topic: !secret mqtt_command_main_topic - mqtt_status_main_topic: !secret mqtt_status_main_topic - - # If changing IP addresses, update current_ip_address here, otherwise it remains static_ip_address. - # Do not forget to switch it back when the address change is complete. - current_ip_address: ${static_ip_address} - - # MQTT LOCAL Controls - mqtt_device_name: "ledmatrix1" - mqtt_command_topic: "${mqtt_command_main_topic}/${mqtt_device_name}" - mqtt_status_topic: "${mqtt_status_main_topic}/${mqtt_device_name}" - - # Device Settings - log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE - update_interval: "60s" - - # Matrix Display Settings - pin_mosi: GPIO8 - pin_cs: GPIO9 - pin_sck: GPIO10 - num_chips: "4" - scroll_delay_ms: "15" # ms between scroll steps - matrix_initial_brightness: "2" # Default MAX7219 intensity, 1-15. - y_offset: "0" - y_scroll_offset: "0" - - # Announcement / Sounder Settings - pin_sounder: GPIO4 - sounder_pulse_ms: "120" - -#:########################################################################################:# -# PACKAGES: Included Common Packages -# https://esphome.io/components/packages.html -#:########################################################################################:# -packages: - #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### - common_wifi: !include - file: common/network_common.yaml - vars: - local_device_name: "${device_name}" - local_static_ip_address: "${static_ip_address}" - local_ota_pass: "${ota_pass}" - local_current_ip_address: "${current_ip_address}" - - #### HOME ASSISTANT API (choose encryption or no encryption options) #### - common_api: !include - file: common/api_common.yaml - #file: common/api_common_noencryption.yaml - vars: - local_api_key: "${api_key}" - - #### MQTT #### - common_mqtt: !include - file: common/mqtt_common.yaml - vars: - local_device_name: "${device_name}" - - #### WEB PORTAL #### - common_webportal: !include common/webportal_common.yaml - - #### SNTP (Needed for the clock display) #### - common_sntp: !include common/sntp_common.yaml - - #### DIAGNOSTICS Sensors #### - diag_basic: !include common/include_basic_diag_sensors.yaml - #diag_more: !include common/include_more_diag_sensors.yaml - #diag_debug: !include common/include_debug_diag_sensors.yaml - #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml - -#:########################################################################################:# -# ESPHOME: -# https://esphome.io/components/esphome.html -#:########################################################################################:# -esphome: - name: "${device_name}" - friendly_name: "${friendly_name}" - comment: "${description_comment}" # Appears on the ESPHome page in Home Assistant. - area: "${device_area}" - project: - name: "${project_name}" - version: "${project_version}" - -#:########################################################################################:# -# ESP PLATFORM AND FRAMEWORK: -# https://esphome.io/components/esp32/ -#:########################################################################################:# -esp32: - variant: esp32c3 - framework: - type: esp-idf - version: recommended - -#esp8266: -# board: esp01_1m -# early_pin_init: false # Recommended false where switches are involved. Defaults to true. -# board_flash_mode: dout # Default is dout. - -#:########################################################################################:# -# LOGGER: ESPHome Logging Enable -# https://esphome.io/components/logger.html -#:########################################################################################:# -logger: - level: "${log_level}" # INFO suggested, or DEBUG for testing. - #baud_rate: 0 # Set to 0 for no UART logging if UART is used for another device. - #esp8266_store_log_strings_in_flash: false - #tx_buffer_size: 64 - -#:########################################################################################:# -# GLOBALS: -# https://esphome.io/components/globals.html -#:########################################################################################:# -globals: - # Text currently being scrolled across the matrix. - - id: scroll_text - type: std::string - initial_value: "" - - # Current X position of the scrolling text. - - id: scroll_x - type: int - initial_value: '0' - - # True while a scroll message is active. - - id: scrolling - type: bool - initial_value: 'false' - - # False = 24h mode, true = 12h mode. - - id: use_12h_mode - type: bool - restore_value: true - initial_value: 'false' - - # Runtime scroll delay, controlled by the Scroll Delay number entity. - - id: scroll_delay_runtime - type: int - restore_value: true - initial_value: '${scroll_delay_ms}' - - # Current scroll-specific settings. - # Normal /scroll commands copy the current default settings into these. - # /announce commands set these per message. - - id: scroll_effective_transition_mode - type: int - restore_value: false - initial_value: '-1' - - - id: scroll_effective_delay_ms - type: int - restore_value: false - initial_value: '-1' - - # MAX7219 intensity: 0=OFF, 1..15=ON. Driven by the HA light and brightness slider. - - id: matrix_intensity - type: int - restore_value: false - initial_value: '${matrix_initial_brightness}' - - # Last non-zero brightness level selected by HA, MQTT, or the 1-15 slider. - # This allows the exact slider to keep its value when the display is switched off. - - id: matrix_last_nonzero_intensity - type: int - restore_value: true - initial_value: '${matrix_initial_brightness}' - - # Brightness sync loop protection for HA light <-> MQTT brightness updates. - - id: suppress_mqtt_bright_sync - type: bool - restore_value: false - initial_value: 'false' - - - id: target_mqtt_bright - type: int - restore_value: false - initial_value: '-1' - - - id: last_mqtt_bright - type: int - restore_value: false - initial_value: '-1' - - # Re-entry effect after scrolling: 0=Slide up, 1=Slide down, 2=Pixel fall. - - id: transition_mode - type: int - restore_value: true - initial_value: '0' - - # Slide transition state. - - id: transition_active - type: bool - restore_value: false - initial_value: 'false' - - - id: transition_y - type: int - restore_value: false - initial_value: '0' - - # Pixel-fall transition state. - - id: pixel_fall_active - type: bool - restore_value: false - initial_value: 'false' - - - id: pixel_fall_gen - type: int - restore_value: false - initial_value: '0' - - # Announcement scheduler. Not restored after reboot. - # Each item has its own due time so repeated messages can interleave. - - id: announce_ids - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_messages - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_transition_modes - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_speeds - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_repeat_seconds - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_remaining - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_sounder_remaining - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_next_due_ms - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - - id: announce_tokens - type: std::vector - restore_value: false - initial_value: 'std::vector()' - - # Incrementing token used so duplicate IDs cannot accidentally alter a newer message. - - id: announce_next_token - type: uint32_t - restore_value: false - initial_value: '1' - - # Scheduler worker scratch values. - - id: announce_due_index - type: int - restore_value: false - initial_value: '-1' - - - id: announce_current_token - type: uint32_t - restore_value: false - initial_value: '0' - - - id: announce_current_id - type: std::string - restore_value: false - initial_value: "" - - - id: announce_current_message - type: std::string - restore_value: false - initial_value: "" - - - id: announce_current_transition_mode - type: int - restore_value: false - initial_value: '0' - - - id: announce_current_speed - type: int - restore_value: false - initial_value: '15' - - - id: announce_current_repeat_seconds - type: int - restore_value: false - initial_value: '0' - - - id: announce_current_should_sound - type: bool - restore_value: false - initial_value: 'false' - -#:########################################################################################:# -# FONT COMPONENT: -# https://esphome.io/components/font/ -#:########################################################################################:# -font: - # Clock digits. Keep glyphs limited to reduce firmware size. - - file: "fonts/Eight-Bit-Dragon.ttf" - id: digit5 - size: 8 - glyphs: ["0123456789: -"] - - # General 5x8 font for scroll text. - - file: "fonts/5x8.bdf" - id: font5x8 - size: 8 - -#:########################################################################################:# -# SPI COMPONENT: -# https://esphome.io/components/spi.html -#:########################################################################################:# -spi: - clk_pin: ${pin_sck} - mosi_pin: ${pin_mosi} - -#:########################################################################################:# -# TEXT COMPONENT: -# https://esphome.io/components/text/template/ -#:########################################################################################:# -text: - - platform: template - id: ha_scroll_text - name: "${friendly_name} Scroll Text" - icon: "mdi:message-text-outline" - mode: text - max_length: 80 - optimistic: false - set_action: - - lambda: |- - std::string s = x.c_str(); - - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20; }), s.end()); - - // Publish the cleaned text so the HA UI updates immediately. - id(ha_scroll_text).publish_state(s.c_str()); - - // Empty text only clears the entity. It should not start a scroll. - if (s.empty()) return; - - id(scroll_text) = s; - id(scroll_x) = ${num_chips} * 8; - id(scroll_effective_transition_mode) = id(transition_mode); - id(scroll_effective_delay_ms) = id(scroll_delay_runtime); - id(start_scroll_sequence).execute(); - id(clear_scroll_text_box).execute(); - -#:########################################################################################:# -# NUMBER COMPONENT: -# https://esphome.io/components/number/template/ -#:########################################################################################:# -number: - - platform: template - id: scroll_delay_number - name: "${friendly_name} Scroll Delay" - icon: "mdi:timer-outline" - unit_of_measurement: "ms" - min_value: 1 - max_value: 100 - step: 1 - restore_value: true - initial_value: ${scroll_delay_ms} - optimistic: true - set_action: - - lambda: |- - id(scroll_delay_runtime) = (int) x; - - - platform: template - id: matrix_brightness_number - name: "${friendly_name} Brightness Level" - icon: "mdi:brightness-6" - min_value: 1 - max_value: 15 - step: 1 - restore_value: true - initial_value: ${matrix_initial_brightness} - optimistic: true - set_action: - - lambda: |- - int v = (int) lroundf(x); - if (v < 1) v = 1; - if (v > 15) v = 15; - - id(matrix_last_nonzero_intensity) = v; - - // Setting the exact brightness slider also turns the display on. - auto call = id(matrix_light).turn_on(); - call.set_brightness((float) v / 15.0f); - call.perform(); - -#:########################################################################################:# -# OUTPUT COMPONENT: -# https://esphome.io/components/output/ -#:########################################################################################:# -output: - # Template output lets the HA light and brightness slider control MAX7219 intensity. - - platform: template - id: matrix_brightness_out - type: float - write_action: - - lambda: |- - int pub = 0; - - if (state <= 0.001f) { - id(matrix_intensity) = 0; - id(matrix).turn_on_off(false); - pub = 0; - } else { - int v = (int) lroundf(state * 15.0f); - if (v < 1) v = 1; - if (v > 15) v = 15; - - id(matrix_intensity) = v; - id(matrix_last_nonzero_intensity) = v; - id(matrix_brightness_number).publish_state((float) v); - id(matrix).turn_on_off(true); - pub = v; - } - - id(target_mqtt_bright) = pub; - - - component.update: matrix - - # Publish brightness back to MQTT unless this update came from MQTT. - - if: - condition: - lambda: |- - return !id(suppress_mqtt_bright_sync) - && (id(target_mqtt_bright) != id(last_mqtt_bright)); - then: - - mqtt.publish: - topic: "${mqtt_command_topic}/bright" - payload: !lambda |- - char buf[6]; - snprintf(buf, sizeof(buf), "%d", id(target_mqtt_bright)); - return std::string(buf); - qos: 0 - retain: true - - lambda: |- - id(last_mqtt_bright) = id(target_mqtt_bright); - - # Optional announcement sounder / active beeper output. - # Default GPIO is GPIO4. Change pin_sounder if this conflicts with your wiring. - - platform: gpio - id: announcement_sounder_output - pin: ${pin_sounder} - -#:########################################################################################:# -# LIGHT COMPONENT: -# https://esphome.io/components/light/monochromatic/ -#:########################################################################################:# -light: - - platform: monochromatic - id: matrix_light - name: "${friendly_name} Display" - icon: "mdi:brightness-6" - output: matrix_brightness_out - gamma_correct: 1.0 - default_transition_length: 0s - restore_mode: RESTORE_DEFAULT_ON - -#:########################################################################################:# -# SELECT COMPONENT: -# https://esphome.io/components/select/template/ -#:########################################################################################:# -select: - - platform: template - id: clock_format_select - name: "${friendly_name} Clock Format" - icon: "mdi:clock-time-three-outline" - options: - - "24" - - "12" - lambda: |- - return esphome::optional(std::string(id(use_12h_mode) ? "12" : "24")); - set_action: - - lambda: |- - id(use_12h_mode) = (x == "12"); - - component.update: clock_format_select - - component.update: matrix - - - platform: template - id: transition_select - name: "${friendly_name} Transition Effect" - icon: "mdi:transition" - options: - - "Slide up" - - "Slide down" - - "Pixel fall" - lambda: |- - if (id(transition_mode) == 2) return std::string("Pixel fall"); - if (id(transition_mode) == 1) return std::string("Slide down"); - return std::string("Slide up"); - set_action: - - lambda: |- - if (x == "Pixel fall") id(transition_mode) = 2; - else if (x == "Slide down") id(transition_mode) = 1; - else id(transition_mode) = 0; - - component.update: transition_select - -#:########################################################################################:# -# SCRIPT COMPONENT: -# https://esphome.io/components/script.html -#:########################################################################################:# -script: - # Clear the HA text box shortly after a scroll request has been accepted. - - id: clear_scroll_text_box - mode: restart - then: - - delay: 400ms - - lambda: |- - id(ha_scroll_text).publish_state(""); - - # Short active-beeper pulse before selected announcement repeats. - - id: announcement_sounder_beep - mode: restart - then: - - output.turn_on: announcement_sounder_output - - delay: ${sounder_pulse_ms}ms - - output.turn_off: announcement_sounder_output - - # Interleaved announcement scheduler worker. - # Each item scrolls once when due, then reschedules itself if repeats remain. - - id: announcement_queue_worker - mode: single - then: - - while: - condition: - lambda: |- - return !id(announce_ids).empty(); - then: - # Wait until the display is free before selecting the next due announcement. - - wait_until: - condition: - lambda: |- - return !id(scrolling) - && !id(transition_active) - && !id(pixel_fall_active); - - # Find the oldest due announcement. - # If nothing is due yet, leave announce_due_index as -1. - - lambda: |- - id(announce_due_index) = -1; - - if (id(announce_ids).empty()) return; - - const uint32_t now_ms = millis(); - int best_index = -1; - uint32_t best_due = 0; - - for (int i = 0; i < (int) id(announce_ids).size(); i++) { - const uint32_t due = id(announce_next_due_ms)[i]; - - // Signed subtraction handles millis rollover safely enough for this use. - if ((int32_t) (now_ms - due) >= 0) { - if (best_index < 0 || (int32_t) (best_due - due) > 0) { - best_index = i; - best_due = due; - } - } - } - - id(announce_due_index) = best_index; - - # Nothing is due yet. Check again shortly so new messages can still run quickly. - - if: - condition: - lambda: |- - return id(announce_due_index) < 0; - then: - - delay: 250ms - - # A message is due. Copy it into current scratch globals and scroll it once. - - if: - condition: - lambda: |- - return id(announce_due_index) >= 0 - && id(announce_due_index) < (int) id(announce_ids).size(); - then: - - lambda: |- - const int i = id(announce_due_index); - - id(announce_current_token) = id(announce_tokens)[i]; - id(announce_current_id) = id(announce_ids)[i]; - id(announce_current_message) = id(announce_messages)[i]; - id(announce_current_transition_mode) = id(announce_transition_modes)[i]; - id(announce_current_speed) = id(announce_speeds)[i]; - id(announce_current_repeat_seconds) = id(announce_repeat_seconds)[i]; - - id(announce_current_should_sound) = false; - if (id(announce_sounder_remaining)[i] > 0) { - id(announce_current_should_sound) = true; - id(announce_sounder_remaining)[i] -= 1; - } - - id(scroll_text) = id(announce_current_message); - id(scroll_x) = ${num_chips} * 8; - id(scroll_effective_transition_mode) = id(announce_current_transition_mode); - id(scroll_effective_delay_ms) = id(announce_current_speed); - - - if: - condition: - lambda: |- - return id(announce_current_should_sound); - then: - - script.execute: announcement_sounder_beep - - - script.execute: start_scroll_sequence - - # Give the scroll sequence time to enter its active state. - - delay: 100ms - - # Wait for scroll and re-entry animation to finish before rescheduling. - - wait_until: - condition: - lambda: |- - return !id(scrolling) - && !id(transition_active) - && !id(pixel_fall_active); - - # Find the same token again. It may have been cancelled or replaced while scrolling. - # If it still exists, decrement repeats and either remove or reschedule it. - - lambda: |- - int idx = -1; - for (int i = 0; i < (int) id(announce_tokens).size(); i++) { - if (id(announce_tokens)[i] == id(announce_current_token)) { - idx = i; - break; - } - } - - if (idx < 0) { - ESP_LOGI("announce", "Announcement token already cancelled/replaced id=%s", - id(announce_current_id).c_str()); - return; - } - - id(announce_remaining)[idx] -= 1; - - if (id(announce_remaining)[idx] <= 0) { - ESP_LOGI("announce", "Announcement complete id=%s", - id(announce_ids)[idx].c_str()); - - id(announce_ids).erase(id(announce_ids).begin() + idx); - id(announce_messages).erase(id(announce_messages).begin() + idx); - id(announce_transition_modes).erase(id(announce_transition_modes).begin() + idx); - id(announce_speeds).erase(id(announce_speeds).begin() + idx); - id(announce_repeat_seconds).erase(id(announce_repeat_seconds).begin() + idx); - id(announce_remaining).erase(id(announce_remaining).begin() + idx); - id(announce_sounder_remaining).erase(id(announce_sounder_remaining).begin() + idx); - id(announce_next_due_ms).erase(id(announce_next_due_ms).begin() + idx); - id(announce_tokens).erase(id(announce_tokens).begin() + idx); - } else { - const uint32_t gap_ms = (uint32_t) id(announce_repeat_seconds)[idx] * 1000UL; - id(announce_next_due_ms)[idx] = millis() + gap_ms; - - ESP_LOGI("announce", "Rescheduled announcement id=%s, remaining=%d, gap=%ds", - id(announce_ids)[idx].c_str(), - id(announce_remaining)[idx], - id(announce_repeat_seconds)[idx]); - } - - id(announce_due_index) = -1; - id(announce_current_token) = 0; - id(announce_current_id) = ""; - id(announce_current_message) = ""; - id(announce_current_should_sound) = false; - - # Sequence: exit slide-down -> scroll -> selected re-entry. - - id: start_scroll_sequence - mode: restart - then: - - lambda: |- - id(scrolling) = false; - id(pixel_fall_active) = false; - - component.update: matrix - - script.execute: clock_exit_down - - script.wait: clock_exit_down - - lambda: |- - id(scrolling) = true; - - script.execute: do_scroll - - # Scroll the stored text until it has moved fully off the left side. - - id: do_scroll - mode: restart - then: - - while: - condition: - lambda: |- - return id(scrolling); - then: - - lambda: |- - id(scroll_x) -= 1; - const int end_limit = - (int(id(scroll_text).length()) * 6); - if (id(scroll_x) < end_limit) { - id(scrolling) = false; - - int tm = id(scroll_effective_transition_mode); - if (tm < 0) tm = id(transition_mode); - - if (tm == 1) { - id(clock_slide_down_entry).execute(); - } else if (tm == 2) { - id(clock_pixel_fall_transition).execute(); - } else { - id(clock_slide_up_entry).execute(); - } - } - - component.update: matrix - - delay: !lambda |- - int d = id(scroll_effective_delay_ms); - if (d < 1) d = id(scroll_delay_runtime); - if (d < 1) d = 1; - if (d > 100) d = 100; - return (uint32_t) d; - - # Exit: slide clock down off-screen and hold it there to prevent a one-frame flash. - - id: clock_exit_down - mode: restart - then: - - lambda: |- - id(transition_active) = true; - id(transition_y) = ${y_offset}; - - repeat: - count: 9 - then: - - lambda: |- - id(transition_y) += 1; - - component.update: matrix - - delay: 45ms - - lambda: |- - id(transition_active) = true; - id(transition_y) = 8; - - component.update: matrix - - # Re-entry: clock slides down into place from above. - - id: clock_slide_down_entry - mode: restart - then: - - lambda: |- - id(transition_active) = true; - id(transition_y) = -8; - - repeat: - count: 9 - then: - - lambda: |- - id(transition_y) += 1; - - component.update: matrix - - delay: 60ms - - lambda: |- - id(transition_active) = false; - id(transition_y) = ${y_offset}; - - component.update: matrix - - # Re-entry: clock slides up into place from below. - - id: clock_slide_up_entry - mode: restart - then: - - lambda: |- - id(transition_active) = true; - id(transition_y) = 8; - - repeat: - count: 9 - then: - - lambda: |- - id(transition_y) -= 1; - - component.update: matrix - - delay: 60ms - - lambda: |- - id(transition_active) = false; - id(transition_y) = ${y_offset}; - - component.update: matrix - - # Re-entry: falling pixels effect before the clock redraws. - - id: clock_pixel_fall_transition - mode: restart - then: - - lambda: |- - id(transition_active) = false; - id(transition_y) = ${y_offset}; - id(pixel_fall_active) = true; - id(pixel_fall_gen) += 1; - - repeat: - count: 18 - then: - - component.update: matrix - - delay: 55ms - - lambda: |- - id(pixel_fall_active) = false; - id(transition_active) = false; - id(transition_y) = ${y_offset}; - - component.update: matrix - -#:########################################################################################:# -# DISPLAY COMPONENT: -# https://esphome.io/components/display/max7219digit/ -#:########################################################################################:# -display: - - platform: max7219digit - id: matrix - cs_pin: ${pin_cs} - num_chips: ${num_chips} - intensity: 2 - update_interval: 500ms - rotate_chip: 0 - reverse_enable: false - flip_x: false - - lambda: |- - // Apply brightness only when it changes to reduce flicker. - static int last_brightness = -1; - if (id(matrix_intensity) != last_brightness) { - if (id(matrix_intensity) > 0) { - int v = id(matrix_intensity); - if (v > 15) v = 15; - it.intensity((uint8_t) v); - } - last_brightness = id(matrix_intensity); - } - - if (id(matrix_intensity) == 0) return; - - it.clear(); - - // Scroll text has priority over clock display. - if (id(scrolling)) { - it.print(id(scroll_x), ${y_scroll_offset}, id(font5x8), id(scroll_text).c_str()); - return; - } - - // Pixel fall shows only particles while active. - if (id(pixel_fall_active)) { - static int last_gen = -1; - static int px[24]; - static int py[24]; - static uint32_t rng = 1; - - auto rand_u32 = [&]() -> uint32_t { - rng = rng * 1664525u + 1013904223u; - return rng; - }; - - if (last_gen != id(pixel_fall_gen)) { - last_gen = id(pixel_fall_gen); - rng = 0xA5A5A5A5u ^ (uint32_t) id(pixel_fall_gen); - for (int i = 0; i < 24; i++) { - px[i] = (int)(rand_u32() % 32); - py[i] = - (int)(rand_u32() % 16); - } - } - - for (int i = 0; i < 24; i++) { - int spd = 1 + (int)(rand_u32() % 2); - py[i] += spd; - - if (py[i] > 10) { - px[i] = (int)(rand_u32() % 32); - py[i] = - (int)(rand_u32() % 10); - } - - for (int t = 0; t < 2; t++) { - int yy = py[i] - t; - int xx = px[i]; - if (xx >= 0 && xx <= 31 && yy >= 0 && yy <= 7) { - it.draw_pixel_at(xx, yy, Color::WHITE); - } - } - } - return; - } - - // Prefer real SNTP time. Fall back to current_mins from common/sntp_common.yaml. - auto now = id(sntp_time).now(); - bool time_valid = now.is_valid(); - - int raw_hour = 0; - int minute_to_show = 0; - int second_to_show = 0; - - if (time_valid) { - raw_hour = now.hour; - minute_to_show = now.minute; - second_to_show = now.second; - } else { - int mins = id(current_mins); - if (mins < 0) mins = 0; - if (mins >= 1440) mins = mins % 1440; - - raw_hour = mins / 60; - minute_to_show = mins % 60; - second_to_show = 0; - } - - int hour_to_show = raw_hour; - if (id(use_12h_mode)) { - hour_to_show = raw_hour % 12; - if (hour_to_show == 0) hour_to_show = 12; - } - - char hh[3], mm[3]; - snprintf(hh, sizeof(hh), "%02d", hour_to_show); - snprintf(mm, sizeof(mm), "%02d", minute_to_show); - - const int x_h1 = 3; - const int x_h2 = 9; - const int x_m1 = 17; - const int x_m2 = 23; - - const int y = (id(transition_active) ? id(transition_y) : ${y_offset}); - - char ch[2] = { hh[0], 0 }; - it.print(x_h1, y, id(digit5), ch); - ch[0] = hh[1]; - it.print(x_h2, y, id(digit5), ch); - - ch[0] = mm[0]; - it.print(x_m1, y, id(digit5), ch); - ch[0] = mm[1]; - it.print(x_m2, y, id(digit5), ch); - - // In 12h mode, draw a tiny A or P indicator at the far right. - if (id(use_12h_mode)) { - const bool is_pm = raw_hour >= 12; - - const uint8_t A_rows[4] = { 2, 5, 7, 5 }; - const uint8_t P_rows[4] = { 6, 5, 6, 4 }; - const uint8_t *L = is_pm ? P_rows : A_rows; - - const int x_ampm = 29; - const int y_ampm = y + 4; - - for (int r = 0; r < 4; r++) { - uint8_t row = L[r]; - for (int c = 0; c < 3; c++) { - if (row & (1 << (2 - c))) { - int xx = x_ampm + c; - int yy = y_ampm + r; - if (yy >= 0 && yy <= 7 && xx >= 0 && xx <= 31) { - it.draw_pixel_at(xx, yy, Color::WHITE); - } - } - } - } - } - - // Blink the colon when SNTP is valid. Keep it solid during fallback time. - const bool colon_on = time_valid ? ((second_to_show % 2) == 0) : true; - if (colon_on) { - const int x_col = (x_h2 + x_m1) / 2 + 2; - int y1 = y + 2; - int y2 = y + 5; - if (y1 >= 0 && y1 <= 7) it.draw_pixel_at(x_col, y1, Color::WHITE); - if (y2 >= 0 && y2 <= 7) it.draw_pixel_at(x_col, y2, Color::WHITE); - } - -#:########################################################################################:# -# API COMPONENT: Device-specific Home Assistant native API actions -# Common API encryption/reboot settings remain in common/api_common.yaml. -# https://esphome.io/components/api.html -#:########################################################################################:# -api: - actions: - # Home Assistant action: esphome.esp_ledmatrix1_scroll_text - # Example HA data: { message: "Hello from Home Assistant" } - - action: scroll_text - variables: - message: string - then: - - lambda: |- - std::string s = message; - - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20; }), s.end()); - - // Ignore empty API scroll commands completely. - if (s.empty()) return; - - // Keep the HA text entity visually in sync with this direct API action. - id(ha_scroll_text).publish_state(s.c_str()); - - id(scroll_text) = s; - id(scroll_x) = ${num_chips} * 8; - id(scroll_effective_transition_mode) = id(transition_mode); - id(scroll_effective_delay_ms) = id(scroll_delay_runtime); - id(start_scroll_sequence).execute(); - id(clear_scroll_text_box).execute(); - -#:########################################################################################:# -# MQTT COMPONENT: Device-specific MQTT command triggers -# Common broker, login, topic_prefix, and reboot settings remain in common/mqtt_common.yaml. -#:########################################################################################:# -mqtt: - discovery: false - birth_message: - topic: "${mqtt_status_topic}/status" - payload: "online" - qos: 0 - retain: true - will_message: - topic: "${mqtt_status_topic}/status" - payload: "offline" - qos: 0 - retain: true - on_message: - # Scroll a text payload once across the matrix. - - topic: "${mqtt_command_topic}/scroll" - then: - - lambda: |- - std::string s = x.c_str(); - - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20; }), s.end()); - - // Ignore empty MQTT scroll commands completely. - if (s.empty()) return; - - id(scroll_text) = s; - id(scroll_x) = ${num_chips} * 8; - id(scroll_effective_transition_mode) = id(transition_mode); - id(scroll_effective_delay_ms) = id(scroll_delay_runtime); - id(start_scroll_sequence).execute(); - - # Scheduled/interleaved announcement with per-message parameters. - # Format: - # Message text [identifier,transition_mode,speed,repeat_seconds,repeat_times,sounder_quantity] - # - # Example: - # Washing machine complete [wash,1,15,10,6,1] - - topic: "${mqtt_command_topic}/announce" - then: - - lambda: |- - std::string payload = x.c_str(); - - payload.erase(std::remove_if(payload.begin(), payload.end(), - [](unsigned char c){ return c < 0x20 || c == 0x7F; }), payload.end()); - - auto trim = [](std::string &s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), - [](unsigned char ch){ return !std::isspace(ch); })); - s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch){ return !std::isspace(ch); }).base(), s.end()); - }; - - trim(payload); - if (payload.empty()) return; - - size_t rb = payload.rfind(']'); - size_t lb = payload.rfind('['); - - if (lb == std::string::npos || rb == std::string::npos || rb <= lb) { - ESP_LOGW("announce", "Invalid announcement format. Missing [parameters]."); - return; - } - - std::string message = payload.substr(0, lb); - std::string vars = payload.substr(lb + 1, rb - lb - 1); - trim(message); - trim(vars); - - if (message.empty() || vars.empty()) { - ESP_LOGW("announce", "Invalid announcement. Message or parameters are empty."); - return; - } - - std::vector parts; - std::string part; - for (char c : vars) { - if (c == ',') { - trim(part); - parts.push_back(part); - part.clear(); - } else { - part.push_back(c); - } - } - trim(part); - parts.push_back(part); - - if (parts.size() != 6) { - ESP_LOGW("announce", "Invalid announcement. Expected 6 parameters."); - return; - } - - auto clean_id = [](std::string s) -> std::string { - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ - return !(std::isalnum(c) || c == '_' || c == '-'); - }), s.end()); - - if (s.length() > 8) s = s.substr(0, 8); - return s; - }; - - auto parse_int = [](std::string s, int min_v, int max_v, int fallback) -> int { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), - [](unsigned char ch){ return !std::isspace(ch); })); - s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch){ return !std::isspace(ch); }).base(), s.end()); - - if (s.empty()) return fallback; - - for (char c : s) { - if (!std::isdigit((unsigned char) c)) return fallback; - } - - int v = atoi(s.c_str()); - if (v < min_v) v = min_v; - if (v > max_v) v = max_v; - return v; - }; - - std::string announce_id = clean_id(parts[0]); - if (announce_id.empty()) { - ESP_LOGW("announce", "Invalid announcement. Identifier is empty after cleaning."); - return; - } - - int new_transition_mode = parse_int(parts[1], 0, 2, 0); - int new_speed = parse_int(parts[2], 1, 100, id(scroll_delay_runtime)); - int new_repeat_seconds = parse_int(parts[3], 0, 99, 0); - int new_repeat_times = parse_int(parts[4], 0, 99, 1); - int new_sounder_quantity = parse_int(parts[5], 0, 99, 0); - - // Treat repeat_times 0 as a one-shot announcement. - if (new_repeat_times == 0) new_repeat_times = 1; - - if (new_sounder_quantity > new_repeat_times) { - new_sounder_quantity = new_repeat_times; - } - - // If the same ID already exists, remove all older scheduled copies first. - // The token system prevents the currently scrolling old copy from altering this new copy. - for (int i = (int) id(announce_ids).size() - 1; i >= 0; i--) { - if (id(announce_ids)[i] == announce_id) { - id(announce_ids).erase(id(announce_ids).begin() + i); - id(announce_messages).erase(id(announce_messages).begin() + i); - id(announce_transition_modes).erase(id(announce_transition_modes).begin() + i); - id(announce_speeds).erase(id(announce_speeds).begin() + i); - id(announce_repeat_seconds).erase(id(announce_repeat_seconds).begin() + i); - id(announce_remaining).erase(id(announce_remaining).begin() + i); - id(announce_sounder_remaining).erase(id(announce_sounder_remaining).begin() + i); - id(announce_next_due_ms).erase(id(announce_next_due_ms).begin() + i); - id(announce_tokens).erase(id(announce_tokens).begin() + i); - } - } - - uint32_t token = id(announce_next_token); - id(announce_next_token) += 1; - if (id(announce_next_token) == 0) id(announce_next_token) = 1; - - id(announce_ids).push_back(announce_id); - id(announce_messages).push_back(message); - id(announce_transition_modes).push_back(new_transition_mode); - id(announce_speeds).push_back(new_speed); - id(announce_repeat_seconds).push_back(new_repeat_seconds); - id(announce_remaining).push_back(new_repeat_times); - id(announce_sounder_remaining).push_back(new_sounder_quantity); - id(announce_next_due_ms).push_back(millis()); - id(announce_tokens).push_back(token); - - ESP_LOGI("announce", "Scheduled announcement id=%s, token=%u, repeats=%d, gap=%ds", - announce_id.c_str(), token, new_repeat_times, new_repeat_seconds); - - - script.execute: announcement_queue_worker - - # Cancel an active or scheduled announcement by short ID. - - topic: "${mqtt_command_topic}/cancel_id" - then: - - lambda: |- - std::string cancel_id = x.c_str(); - - cancel_id.erase(std::remove_if(cancel_id.begin(), cancel_id.end(), - [](unsigned char c){ - return !(std::isalnum(c) || c == '_' || c == '-'); - }), cancel_id.end()); - - if (cancel_id.length() > 8) cancel_id = cancel_id.substr(0, 8); - if (cancel_id.empty()) return; - - bool found = false; - - for (int i = (int) id(announce_ids).size() - 1; i >= 0; i--) { - if (id(announce_ids)[i] == cancel_id) { - id(announce_ids).erase(id(announce_ids).begin() + i); - id(announce_messages).erase(id(announce_messages).begin() + i); - id(announce_transition_modes).erase(id(announce_transition_modes).begin() + i); - id(announce_speeds).erase(id(announce_speeds).begin() + i); - id(announce_repeat_seconds).erase(id(announce_repeat_seconds).begin() + i); - id(announce_remaining).erase(id(announce_remaining).begin() + i); - id(announce_sounder_remaining).erase(id(announce_sounder_remaining).begin() + i); - id(announce_next_due_ms).erase(id(announce_next_due_ms).begin() + i); - id(announce_tokens).erase(id(announce_tokens).begin() + i); - found = true; - } - } - - if (found) { - ESP_LOGI("announce", "Cancelled scheduled announcement id=%s", cancel_id.c_str()); - } else { - ESP_LOGI("announce", "Cancel ID not found: %s", cancel_id.c_str()); - } - - # Set 12h or 24h clock mode from MQTT. - - topic: "${mqtt_command_topic}/mode" - then: - - lambda: |- - if (x == "12") { - id(use_12h_mode) = true; - } else if (x == "24") { - id(use_12h_mode) = false; - } - - component.update: clock_format_select - - component.update: matrix - - # Brightness from MQTT. 0=OFF, 1..15=ON brightness level. - - topic: "${mqtt_command_topic}/bright" - then: - - lambda: |- - std::string s = x.c_str(); - s.erase(std::remove_if(s.begin(), s.end(), - [](unsigned char c){ return c < 0x20 || c == 0x7F; }), s.end()); - s.erase(s.begin(), std::find_if(s.begin(), s.end(), - [](unsigned char ch){ return !std::isspace(ch); })); - s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch){ return !std::isspace(ch); }).base(), s.end()); - - if (s.empty() || !std::all_of(s.begin(), s.end(), - [](unsigned char c){ return std::isdigit(c); })) return; - - int v = atoi(s.c_str()); - if (v < 0) v = 0; - if (v > 15) v = 15; - - id(suppress_mqtt_bright_sync) = true; - - if (v == 0) { - id(matrix_light).turn_off().perform(); - } else { - id(matrix_last_nonzero_intensity) = v; - id(matrix_brightness_number).publish_state((float) v); - - auto call = id(matrix_light).turn_on(); - call.set_brightness((float) v / 15.0f); - call.perform(); - } - - id(last_mqtt_bright) = v; - id(suppress_mqtt_bright_sync) = false; diff --git a/packages/Appliance_Monitoring/appliance_monitor_processor.yaml b/packages/Appliance_Monitoring/appliance_monitor_processor.yaml index 3afc46f..aad733e 100644 --- a/packages/Appliance_Monitoring/appliance_monitor_processor.yaml +++ b/packages/Appliance_Monitoring/appliance_monitor_processor.yaml @@ -2,8 +2,8 @@ # PACKAGE: Appliance Monitor Processor ################################################################################ # -# Version: 1.2 -# Date: 2026-06-09 +# Version: 1.3 +# Date: 2026-07-11 # # Purpose: # Shared appliance power monitoring processor. @@ -20,6 +20,7 @@ # - One MQTT sensor per appliance. # - Timestamp-based energy and cost calculation. # - Rolling average state detection. +# - Completion announcements through script.view_road_announcement. # # Default values: # debug_flow: false @@ -40,6 +41,16 @@ # Finished # # Notes: +# Version 1.3: +# - Updated completion announcements to support all current View Road +# announcement router channels. +# - Added pushover_kim to the default appliance completion channels. +# - Added mqtt_view_rd_feed support by passing mqtt_feed to the announcement +# router. +# - Added led_matrix_2 support. +# - Added optional announcement_enabled and announcement_payload fields. +# - Kept existing field names for backwards compatibility. +# # Version 1.2: # - Added min_cycle_notify_seconds. # - Finished cycles shorter than this are still recorded, but do not send @@ -53,17 +64,39 @@ # ################################################################################ # FINISHED ANNOUNCEMENT -# The finishing script calls the package view_road_announcement_router.yaml. -# If you want to use your own announcement system, it will occur at the end -# at am_cycle_notify_allowed being true, so change to your own announcement -# method there. +################################################################################ +# +# The finishing script calls: +# +# script.view_road_announcement +# +# Current View Road announcement router channels: +# pushover_zorruno +# pushover_kim +# sony_tv_lounge +# lounge_google_home_voice +# google_home_voice +# lounge_touchscreen_voice +# mqtt_view_rd_feed +# led_matrix_1 +# led_matrix_2 +# +# Notes: +# - google_home_voice is an alias for lounge_google_home_voice. +# - lounge_touchscreen_voice is currently a placeholder route in the router. +# - mqtt_view_rd_feed requires mqtt_feed to be populated. +# - led_matrix_1 and led_matrix_2 use short_announcement and indicator. # # Announcements can use: -# am_announcement_channels -# am_announcement_title -# am_short_announcement -# am_long_announcement -# am_matrix_indicator +# announcement_enabled +# announcement_channels +# title +# short_announcement +# long_announcement +# announcement_payload +# payload +# matrix_indicator +# mqtt_feed # ################################################################################ @@ -127,6 +160,59 @@ script: step: 1 unit_of_measurement: s + announcement_enabled: + name: Announcement Enabled + description: Whether a valid finished cycle should send an announcement. + default: true + selector: + boolean: + + announcement_channels: + name: Announcement Channels + description: Comma-separated View Road announcement router channels. + default: "pushover_zorruno, pushover_kim, sony_tv_lounge, lounge_google_home_voice, mqtt_view_rd_feed, led_matrix_1" + selector: + text: + + title: + name: Announcement Title + description: Announcement title passed to the View Road announcement router. + selector: + text: + + short_announcement: + name: Short Announcement + description: Short announcement text for TV, voice and LED matrix. + selector: + text: + + long_announcement: + name: Long Announcement + description: Long announcement text for Pushover and detailed channels. + selector: + text: + multiline: true + + announcement_payload: + name: Announcement Payload + description: Optional payload passed to the announcement router. + selector: + text: + + matrix_indicator: + name: Matrix Indicator + description: LED matrix indicator data. + example: "wash,1,15,10,6,5,1,0" + selector: + text: + + mqtt_feed: + name: MQTT Activity Feed + description: Activity feed topic suffix used by mqtt_view_rd_feed. + example: "washing_machine" + selector: + text: + sequence: ########################################################################## # CORE DEFAULTS AND PASSED VALUES @@ -156,10 +242,36 @@ script: ########################################################################## # ANNOUNCEMENT DEFAULTS AND PASSED VALUES ########################################################################## + # + # These values are passed to script.view_road_announcement when an + # appliance cycle successfully finishes. + # + # Current supported router channels: + # pushover_zorruno + # pushover_kim + # sony_tv_lounge + # lounge_google_home_voice + # google_home_voice + # lounge_touchscreen_voice + # mqtt_view_rd_feed + # led_matrix_1 + # led_matrix_2 + # + # Notes: + # - google_home_voice is an alias for lounge_google_home_voice. + # - lounge_touchscreen_voice is currently a placeholder route. + # - mqtt_view_rd_feed requires mqtt_feed to be populated. + # + ########################################################################## - variables: + am_announcement_enabled: "{{ announcement_enabled | default(true, true) | bool(true) }}" + + am_default_announcement_channels: >- + pushover_zorruno, pushover_kim, sony_tv_lounge, lounge_google_home_voice, mqtt_view_rd_feed, led_matrix_1 + am_announcement_channels: >- - {{ announcement_channels | default('pushover_zorruno, sony_tv_lounge, lounge_google_home_voice, led_matrix_1', true) | string }} + {{ announcement_channels | default(am_default_announcement_channels, true) | string }} am_short_announcement: >- {{ short_announcement | default((am_appliance_action | title) ~ ' complete', true) | string }} @@ -170,7 +282,20 @@ script: am_announcement_title: >- {{ title | default(am_appliance_name ~ ' Notification', true) | string }} - am_matrix_indicator: "{{ matrix_indicator | default('', true) | string }}" + am_announcement_payload: >- + {% if announcement_payload is defined %} + {{ announcement_payload }} + {% elif payload is defined %} + {{ payload }} + {% else %} + {{ '' }} + {% endif %} + + am_matrix_indicator: >- + {{ matrix_indicator | default('', true) | string }} + + am_mqtt_feed: >- + {{ mqtt_feed | default(am_appliance_id, true) | string }} ########################################################################## # PREVIOUS STATE FROM MQTT MONITOR SENSOR ATTRIBUTES @@ -622,6 +747,12 @@ script: and (am_cycle_duration_seconds_new | int(0)) >= (am_min_cycle_notify_seconds | int(300)) }} + am_cycle_announcement_allowed: >- + {{ + am_cycle_notify_allowed | bool(false) + and am_announcement_enabled | bool(true) + }} + am_cycle_notification_suppressed: >- {{ am_cycle_finished | bool(false) @@ -631,6 +762,8 @@ script: am_cycle_notification_suppressed_reason: >- {% if am_cycle_notification_suppressed | bool(false) %} Cycle was shorter than {{ am_min_cycle_notify_seconds | int(300) }} seconds + {% elif am_cycle_notify_allowed | bool(false) and not (am_announcement_enabled | bool(true)) %} + Announcement disabled for this appliance {% endif %} ########################################################################## @@ -673,6 +806,10 @@ script: min_cycle_notify_seconds=am_min_cycle_notify_seconds | int(300), cycle_notify_allowed=am_cycle_notify_allowed | bool(false), + cycle_announcement_allowed=am_cycle_announcement_allowed | bool(false), + announcement_enabled=am_announcement_enabled | bool(true), + announcement_channels=am_announcement_channels, + mqtt_feed=am_mqtt_feed, cycle_notification_suppressed=am_cycle_notification_suppressed | bool(false), cycle_notification_suppressed_reason=am_cycle_notification_suppressed_reason | trim, @@ -740,18 +877,45 @@ script: threshold. No announcement was sent. ########################################################################## - # FINISHED ANNOUNCEMENT - ########################################################################## - # This script is in additional package view_road_announcement_router.yaml. - # If you want to use your own announcement system, it will occur here - # at am_cycle_notify_allowed being true. - # - # Change to your own announcement method here if needed. + # ANNOUNCEMENT DISABLED LOG ########################################################################## - if: - condition: template - value_template: "{{ am_cycle_notify_allowed | bool(false) }}" + value_template: >- + {{ + am_cycle_notify_allowed | bool(false) + and not (am_announcement_enabled | bool(true)) + }} + then: + - action: logbook.log + data: + name: "Appliance Monitor" + message: >- + {{ am_appliance_name }} finished after + {{ am_cycle_duration_formatted | trim }}, but appliance + completion announcements are disabled for this appliance. + + ########################################################################## + # FINISHED ANNOUNCEMENT + ########################################################################## + # This calls script.view_road_announcement from the announcement router + # package when a valid appliance cycle finishes. + # + # All supported router channels can be used through am_announcement_channels. + # + # mqtt_view_rd_feed requires: + # mqtt_feed + # + # led_matrix_1 and led_matrix_2 require: + # short_announcement + # indicator + # + ########################################################################## + + - if: + - condition: template + value_template: "{{ am_cycle_announcement_allowed | bool(false) }}" then: - action: script.view_road_announcement data: @@ -759,5 +923,6 @@ script: title: "{{ am_announcement_title }}" short_announcement: "{{ am_short_announcement }}" long_announcement: "{{ am_long_announcement }}" - payload: "" + payload: "{{ am_announcement_payload }}" indicator: "{{ am_matrix_indicator }}" + mqtt_feed: "{{ am_mqtt_feed }}" diff --git a/packages/Appliance_Monitoring/appliances_status_monitoring.yaml b/packages/Appliance_Monitoring/appliances_status_monitoring.yaml index 2ce3830..56a9471 100644 --- a/packages/Appliance_Monitoring/appliances_status_monitoring.yaml +++ b/packages/Appliance_Monitoring/appliances_status_monitoring.yaml @@ -1,49 +1,85 @@ -################################################################################ -# PACKAGE: Appliances Status Monitoring -################################################################################ +#:########################################################################################:# +# Appliances Status Monitoring Package # +#:########################################################################################:# # -# Version: 1.4 -# Date: 2026-06-09 +# FILE: +# appliances_status_monitoring.yaml # -# Purpose: +# VERSION: +# V1.5 2026-07-11 +# +# PURPOSE: # Appliance status monitoring package for multiple appliances. # # This package defines a central appliance registry and one automation that: # -# 1. Publishes MQTT discovery config for each appliance monitor sensor. +# 1. Publishes MQTT Discovery configuration for each appliance monitor +# sensor. # 2. Samples appliance power sensors once per minute. # 3. Calls script.appliance_monitor_process_sample. +# 4. Passes per-appliance completion announcement settings to the processor. # -# Processor package required: -# appliance_monitor_processor.yaml -# -# Announcement router required: -# view_road_announcement_router.yaml +# REQUIRED PACKAGES: +# appliance_monitor_processor.yaml V1.3 or newer +# view_road_announcement_router.yaml V1.8 or newer # # MQTT: -# MQTT is used only as retained state storage for appliance monitor sensors. -# Sensor entities are created using MQTT discovery. +# MQTT is used as retained state storage for appliance monitor sensors. +# Sensor entities are created using MQTT Discovery. # -# Adding another appliance: -# Add one entry to the am_appliances list. +# ADDING AN APPLIANCE: +# Add one entry to the am_appliances registry below. # -# Version 1.4: -# Added min_cycle_notify_seconds support. -# Default is 300 seconds, so cycles shorter than 5 minutes are recorded but -# do not send finished notifications. -# Per-appliance overrides can be set in the am_appliances list. +# ANNOUNCEMENT ROUTER CHANNELS: +# The following channel IDs are accepted by the current announcement router: # -# Version 1.3: -# Removed the is_number gate before calling the processor. The processor is -# now always called once per appliance per sample, and power is safely converted -# to 0 if the source sensor is unavailable or non-numeric. +# pushover_zorruno +# pushover_kim +# sony_tv_lounge +# lounge_google_home_voice +# google_home_voice +# lounge_touchscreen_voice +# mqtt_view_rd_feed +# led_matrix_1 +# led_matrix_2 # -################################################################################ +# Notes: +# - google_home_voice is an alias for lounge_google_home_voice. +# - lounge_touchscreen_voice is currently a placeholder route. +# - mqtt_view_rd_feed requires mqtt_feed. +# - led_matrix_1 and led_matrix_2 use short_announcement and +# matrix_indicator. +# +# VERSION HISTORY: +# V1.5 2026-07-11 +# - Added full View Road announcement router parameter support. +# - Added per-appliance announcement_enabled. +# - Added per-appliance announcement_payload. +# - Added per-appliance mqtt_feed for mqtt_view_rd_feed. +# - Added safe defaults for short announcement and matrix indicator. +# - Documented all current announcement router channel names. +# - Existing per-appliance channel selections retained unchanged. +# +# V1.4 2026-06-09 +# - Added min_cycle_notify_seconds support. +# - Default is 300 seconds, so cycles shorter than 5 minutes are recorded +# but do not send finished notifications. +# - Per-appliance overrides can be set in the am_appliances list. +# +# V1.3: +# - Removed the is_number gate before calling the processor. +# - The processor is always called once per appliance per sample. +# - Power is safely converted to 0 if unavailable or non-numeric. +# +#:########################################################################################:# automation: - id: appliances_status_monitoring_process_samples - alias: Appliances Status Monitoring - Process Samples - description: Publish MQTT discovery, sample appliance power sensors, and process appliance status/cycle data. + alias: "Appliances Status Monitoring - Process Samples" + description: >- + Publishes MQTT Discovery, samples appliance power sensors, processes + appliance status and cycle data, and passes completion announcement + settings to the shared processor. mode: queued max: 20 @@ -57,17 +93,17 @@ automation: id: periodic_sample variables: - ########################################################################## - # MQTT LOCAL CONTROLS - ########################################################################## + #:####################################################################################:# + # MQTT LOCAL CONTROLS # + #:####################################################################################:# mqtt_device_name: "appliance_monitor" mqtt_local_status_topic: !secret mqtt_status_main_topic mqtt_discovery_prefix: "homeassistant" - ########################################################################## - # DEFAULTS - ########################################################################## + #:####################################################################################:# + # PROCESSOR DEFAULTS # + #:####################################################################################:# am_default_debug_flow: false am_default_resolution: 2 @@ -75,91 +111,184 @@ automation: am_default_operating_power_min: 12 am_default_min_cycle_notify_seconds: 300 - #am_default_announcement_channels: "pushover_zorruno, sony_tv_lounge, lounge_google_home_voice, led_matrix_1" + #:####################################################################################:# + # ANNOUNCEMENT DEFAULTS # + #:####################################################################################:# + # + # These defaults apply when an appliance does not define its own values. + # + # A conservative default channel is retained to prevent additional + # notifications suddenly being enabled for every appliance. + # + # To make all implemented channels the default, this could instead be: + # + # am_default_announcement_channels: >- + # pushover_zorruno,pushover_kim,sony_tv_lounge, + # lounge_google_home_voice,mqtt_view_rd_feed, + # led_matrix_1,led_matrix_2 + # + # Available channels: + # pushover_zorruno + # pushover_kim + # sony_tv_lounge + # lounge_google_home_voice + # google_home_voice + # lounge_touchscreen_voice + # mqtt_view_rd_feed + # led_matrix_1 + # led_matrix_2 + + am_default_announcement_enabled: true am_default_announcement_channels: "pushover_zorruno" + am_default_announcement_payload: "" - # Optional appliance-specific overrides available: - # resolution: 4 - # The number of samples for the average, with one sample per minute. + #:####################################################################################:# + # OPTIONAL PER-APPLIANCE OVERRIDES # + #:####################################################################################:# # - # standby_power: 3 - # Below this value, the appliance will show "Off". - # Between this value and operating_power_min, it will show "Standby". + # debug_flow: true + # Writes state-processing details to the Home Assistant logbook. # - # operating_power_min: 20 - # The minimum average power needed for the appliance to show "Operating". + # resolution: 4 + # Number of one-minute samples used for the rolling average. # - # min_cycle_notify_seconds: 600 - # Finished cycles shorter than this are recorded, but no notification - # is sent. Default is 300 seconds. + # standby_power: 3 + # Below this value, the appliance shows Off. + # Between standby_power and operating_power_min, it shows Standby. # - # announcement_channels: "pushover_zorruno" - # Announcement channels for this appliance. + # operating_power_min: 20 + # Minimum rolling-average power required for Operating. # - # long_announcement: "The device has finished. Please empty it when convenient." - # Override the long announcement. + # min_cycle_notify_seconds: 600 + # Minimum cycle duration before a completion announcement is allowed. # - # min_cycle_notify_seconds: 600 - # Minimum time for an appliance to be operating before a completion announcement generated. + # announcement_enabled: false + # Disables cycle-completion announcements for this appliance. + # + # announcement_channels: >- + # pushover_zorruno,pushover_kim,sony_tv_lounge, + # lounge_google_home_voice,mqtt_view_rd_feed, + # led_matrix_1,led_matrix_2 + # Comma-separated View Road announcement router channels. + # + # title: "Washing Machine Notification" + # Optional announcement title. + # + # short_announcement: "Washing complete" + # Used by TV, voice and LED matrix channels. + # + # long_announcement: "The washing machine has finished." + # Used by Pushover and other detailed channels. + # If omitted, the processor creates a detailed cycle summary. + # + # announcement_payload: "" + # Optional payload passed to the announcement router. + # + # matrix_indicator: "wash,1,15,10,6,5,1,0" + # LED matrix indicator data. + # + # mqtt_feed: "washing_machine" + # Activity-feed suffix used by mqtt_view_rd_feed. + # If omitted, appliance_id is used. + # + #:####################################################################################:# - ########################################################################## - # APPLIANCE REGISTRY - ########################################################################## + #:####################################################################################:# + # APPLIANCE REGISTRY # + #:####################################################################################:# am_appliances: + #:##################################################################################:# + # Washing Machine # + #:##################################################################################:# + - appliance_id: washing_machine appliance_name: "Washing Machine" appliance_action: "washing" power_entity: "sensor.esp_laundrywashpow_load_power" icon: "mdi:washing-machine" - short_announcement: "/s/sWashing complete" - # long_announcement: # Created by the processor - matrix_indicator: "wash,1,15,10,6,5,1,0" - # Optional washing-machine-specific overrides + resolution: 5 standby_power: 3 operating_power_min: 10 - announcement_channels: "pushover_zorruno" + + announcement_enabled: true + announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1" + short_announcement: "/s/sWashing complete" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "wash,1,15,10,6,5,1,0" + mqtt_feed: "Washer_complete" + + #:##################################################################################:# + # Dryer # + #:##################################################################################:# - appliance_id: dryer appliance_name: "Dryer" appliance_action: "drying" power_entity: "sensor.esp_laundrydrypow_load_power" icon: "mdi:tumble-dryer" - short_announcement: "/s/sDryer complete" - # long_announcement: # Created by the processor - matrix_indicator: "dry,1,15,10,6,5,1,0" - # Optional dryer-specific overrides + resolution: 4 standby_power: 3 operating_power_min: 10 - announcement_channels: "pushover_zorruno" + + announcement_enabled: true + announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1" + short_announcement: "/s/sDryer complete" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "dry,1,15,10,6,5,1,0" + mqtt_feed: "Dryer_complete" + + #:##################################################################################:# + # Main Dishwasher # + #:##################################################################################:# - appliance_id: main_dishwasher appliance_name: "Main Dishwasher" appliance_action: "dishwashing" power_entity: "sensor.esp_maindishwasherpower_power" icon: "mdi:dishwasher" - short_announcement: "/sDishwasher complete" - # long_announcement: # Created by the processor - matrix_indicator: "dish,1,15,10,6,5,1,0" + resolution: 5 standby_power: 3 operating_power_min: 30 - announcement_channels: "pushover_zorruno" + + announcement_enabled: true + announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1" + short_announcement: "/sDishwasher complete" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "dish,1,15,10,6,5,1,0" + mqtt_feed: "Dishwasher_complete" + + #:##################################################################################:# + # Downstairs Dishwasher # + #:##################################################################################:# - appliance_id: downstairs_dishwasher appliance_name: "Downstairs Dishwasher" appliance_action: "dishwashing" power_entity: "sensor.esp_downstdishwasher_power" icon: "mdi:dishwasher" - short_announcement: "/sDownstairs Dishwasher complete" - # long_announcement: # Created by the processor - matrix_indicator: "dish2,1,15,10,6,5,1,0" + resolution: 5 standby_power: 3 operating_power_min: 30 + + announcement_enabled: true announcement_channels: "pushover_zorruno" + short_announcement: "/sDownstairs Dishwasher complete" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "dish2,1,15,10,6,5,1,0" + mqtt_feed: "Downstairs_dishwasher_complete" + + #:##################################################################################:# + # EV Charger 32A # + #:##################################################################################:# - appliance_id: evcharger_32a appliance_name: "EV Charger (32A)" @@ -167,88 +296,134 @@ automation: power_entity: "sensor.main_house_3_phase_power_monitor_32a_wallcharger_power" monitor_sensor: "sensor.appliance_monitor_ev_charger_32a_monitor" icon: "mdi:ev-plug-type2" - short_announcement: "/s/s32A EV Charging complete" - # long_announcement: # Created by the processor - matrix_indicator: "ev32a,1,15,10,6,5,1,0" + resolution: 6 standby_power: 3 operating_power_min: 100 - announcement_channels: "pushover_zorruno" + + announcement_enabled: true + announcement_channels: "pushover_zorruno,pushover_kim" + short_announcement: "/s/s32A EV Charging complete" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "ev32a,1,15,10,6,5,1,0" + mqtt_feed: "Atto_Charger_complete" + + #:##################################################################################:# + # EV Charger 16A # + #:##################################################################################:# - appliance_id: evcharger_16a appliance_name: "EV Charger (16A)" appliance_action: "charging" - power_entity: "sensor.main_house_3_phase_power_monitor_32a_wallcharger_power" - monitor_sensor: "sensor.garage_db_control_16a_wallcharger_power" + power_entity: "sensor.garage_db_control_16a_wallcharger_power" + monitor_sensor: "sensor.appliance_monitor_ev_charger_16a_monitor" icon: "mdi:ev-plug-type1" - short_announcement: "/s/s16A EV Charging complete" - # long_announcement: # Created by the processor - matrix_indicator: "ev16a,1,15,10,6,5,1,0" + resolution: 6 standby_power: 3 operating_power_min: 100 + + announcement_enabled: true announcement_channels: "pushover_zorruno" + short_announcement: "/s/s16A EV Charging complete" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "ev16a,1,15,10,6,5,1,0" + mqtt_feed: "Leaf_Charger_complete" + + #:##################################################################################:# + # Ryobi Charger Left # + #:##################################################################################:# - appliance_id: ryobicharger_left appliance_name: "Ryobi Charger Left" appliance_action: "charging" power_entity: "sensor.ryobi_charger_left_power_monitor_x17pp_power" - #monitor_sensor: "sensor.ryobi_charger_left_power_monitor_x17pp_power" icon: "mdi:battery-charging-medium" - short_announcement: "/s/sRyobi Charging Left" - # long_announcement: # Created by the processor - matrix_indicator: "ryo1,1,15,10,6,5,1,0" + resolution: 4 standby_power: 1 operating_power_min: 20 + + announcement_enabled: true announcement_channels: "pushover_zorruno" + short_announcement: "/s/sRyobi Charging Left" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "ryo1,1,15,10,6,5,1,0" + mqtt_feed: "Ryobi_Charger_1_complete" + + #:##################################################################################:# + # Ryobi Charger Right # + #:##################################################################################:# - appliance_id: ryobicharger_right appliance_name: "Ryobi Charger Right" appliance_action: "charging" power_entity: "sensor.ryobi_charger_right_power_monitor_x16pp_power" - #monitor_sensor: "sensor.ryobi_charger_right_power_monitor_x16pp_power" icon: "mdi:battery-charging-medium" - short_announcement: "/s/sRyobi Charging Right" - # long_announcement: # Created by the processor - matrix_indicator: "ryo2,1,15,10,6,5,1,0" + resolution: 4 standby_power: 1 operating_power_min: 20 + + announcement_enabled: true announcement_channels: "pushover_zorruno" + short_announcement: "/s/sRyobi Charging Right" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "ryo2,1,15,10,6,5,1,0" + mqtt_feed: "Ryobi_Charger_2_complete" + + #:##################################################################################:# + # Pool Pump # + #:##################################################################################:# - appliance_id: pool_pump appliance_name: "Pool Pump" appliance_action: "pumping" power_entity: "sensor.esp_poolpumppower_power" - #monitor_sensor: "sensor.ryobi_charger_right_power_monitor_x16pp_power" icon: "mdi:pool" - short_announcement: "Pool Pump Off" - # long_announcement: # Created by the processor - matrix_indicator: "pump,1,15,10,6,5,1,0" + resolution: 4 standby_power: 5 operating_power_min: 20 + + announcement_enabled: true announcement_channels: "pushover_zorruno" + short_announcement: "Pool Pump Off" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "pump,1,15,10,6,5,1,0" + mqtt_feed: "pool_pump" + + #:##################################################################################:# + # 3D Printer # + #:##################################################################################:# - appliance_id: 3d_printer appliance_name: "3D Printer" appliance_action: "printing" power_entity: "sensor.esp_3dprinterpow_load_power" - #monitor_sensor: "sensor.ryobi_charger_right_power_monitor_x16pp_power" icon: "mdi:printer-3d" - short_announcement: "3D printer finished" - # long_announcement: # Created by the processor - matrix_indicator: "pump,1,15,10,6,5,1,0" + resolution: 4 standby_power: 2 operating_power_min: 20 + + announcement_enabled: true announcement_channels: "pushover_zorruno" + short_announcement: "3D printer finished" + # long_announcement: # Generated by the processor + announcement_payload: "" + matrix_indicator: "pump,1,15,10,6,5,1,0" + mqtt_feed: "3d_printer" action: - ########################################################################## - # PUBLISH / REFRESH MQTT DISCOVERY CONFIG - ########################################################################## + #:####################################################################################:# + # PUBLISH / REFRESH MQTT DISCOVERY CONFIG # + #:####################################################################################:# - repeat: for_each: "{{ am_appliances }}" @@ -285,19 +460,18 @@ automation: identifiers=[mqtt_device_name], name='Appliance Monitor', manufacturer='View Road Home Assistant', - model='Shared appliance power monitor' + model='Shared appliance power monitor', + sw_version='1.5' ) ) | to_json }} - ########################################################################## - # PROCESS SAMPLES - ########################################################################## + #:####################################################################################:# + # PROCESS SAMPLES # + #:####################################################################################:# # # Always process every appliance. The processor receives power_w as a safe - # float value, so unavailable/non-numeric sensors become 0 W. - # - ########################################################################## + # float value, so unavailable or non-numeric sensors become 0 W. - repeat: for_each: "{{ am_appliances }}" @@ -309,36 +483,97 @@ automation: am_monitor_sensor: >- {{ repeat.item.monitor_sensor - | default('sensor.' ~ mqtt_device_name ~ '_' ~ repeat.item.appliance_id ~ '_monitor', true) + | default( + 'sensor.' + ~ mqtt_device_name + ~ '_' + ~ repeat.item.appliance_id + ~ '_monitor', + true + ) }} am_title: >- - {{ repeat.item.title | default(repeat.item.appliance_name ~ ' Notification', true) }} + {{ + repeat.item.title + | default( + repeat.item.appliance_name ~ ' Notification', + true + ) + }} + + am_announcement_enabled: >- + {{ + repeat.item.announcement_enabled + | default(am_default_announcement_enabled) + }} am_announcement_channels: >- - {{ repeat.item.announcement_channels | default(am_default_announcement_channels, true) }} + {{ + repeat.item.announcement_channels + | default(am_default_announcement_channels, true) + }} - am_debug_flow: >- - {{ repeat.item.debug_flow | default(am_default_debug_flow, true) }} + am_announcement_payload: >- + {{ + repeat.item.announcement_payload + | default(am_default_announcement_payload, true) + }} - am_resolution: >- - {{ repeat.item.resolution | default(am_default_resolution, true) }} + am_mqtt_feed: >- + {{ + repeat.item.mqtt_feed + | default(repeat.item.appliance_id, true) + }} - am_standby_power: >- - {{ repeat.item.standby_power | default(am_default_standby_power, true) }} + am_short_announcement: >- + {{ + repeat.item.short_announcement + | default( + (repeat.item.appliance_action | title) ~ ' complete', + true + ) + }} - am_operating_power_min: >- - {{ repeat.item.operating_power_min | default(am_default_operating_power_min, true) }} - - am_min_cycle_notify_seconds: >- - {{ repeat.item.min_cycle_notify_seconds | default(am_default_min_cycle_notify_seconds, true) }} - - am_power_w: >- - {{ states(repeat.item.power_entity) | float(0) }} + am_matrix_indicator: >- + {{ repeat.item.matrix_indicator | default('', true) }} am_long_announcement: >- {{ repeat.item.long_announcement | default('', true) }} + am_debug_flow: >- + {{ + repeat.item.debug_flow + | default(am_default_debug_flow) + }} + + am_resolution: >- + {{ + repeat.item.resolution + | default(am_default_resolution, true) + }} + + am_standby_power: >- + {{ + repeat.item.standby_power + | default(am_default_standby_power, true) + }} + + am_operating_power_min: >- + {{ + repeat.item.operating_power_min + | default(am_default_operating_power_min, true) + }} + + am_min_cycle_notify_seconds: >- + {{ + repeat.item.min_cycle_notify_seconds + | default(am_default_min_cycle_notify_seconds, true) + }} + + am_power_w: >- + {{ states(repeat.item.power_entity) | float(0) }} + - action: script.appliance_monitor_process_sample data: appliance_id: "{{ repeat.item.appliance_id }}" @@ -355,8 +590,11 @@ automation: operating_power_min: "{{ am_operating_power_min }}" min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}" + announcement_enabled: "{{ am_announcement_enabled }}" announcement_channels: "{{ am_announcement_channels }}" - short_announcement: "{{ repeat.item.short_announcement }}" - long_announcement: "{{ am_long_announcement }}" title: "{{ am_title }}" - matrix_indicator: "{{ repeat.item.matrix_indicator }}" + short_announcement: "{{ am_short_announcement }}" + long_announcement: "{{ am_long_announcement }}" + announcement_payload: "{{ am_announcement_payload }}" + matrix_indicator: "{{ am_matrix_indicator }}" + mqtt_feed: "{{ am_mqtt_feed }}" diff --git a/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml b/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml index d842d9e..6e5c097 100644 --- a/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml +++ b/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml @@ -9,271 +9,84 @@ # jobs_tracker_jobs.yaml # # VERSION: -# V1.4 2026-06-20 +# V2.3 2026-07-20 # # PURPOSE: -# Defines the job list, MQTT entities, completion triggers and summary output -# for the Jobs Tracker system. +# Defines the job registry, MQTT Discovery entities, completion triggers and +# summary output for the Jobs Tracker system. # # DEPENDS ON: -# jobs_tracker_processor.yaml V1.3.1 or newer +# jobs_tracker_processor.yaml V1.6 or newer # -# MQTT TOPICS: -# viewroad-status/jobs_tracker/summary -# viewroad-status/jobs_tracker/dog_feeding_morning -# viewroad-status/jobs_tracker/dog_feeding_evening -# viewroad-status/jobs_tracker/dog_depooping -# viewroad-status/jobs_tracker/dishwasher_unpacked -# viewroad-status/jobs_tracker/rubbish_removed +# OPERATION: +# Add and edit jobs in the JOB REGISTRY section below. Each job has its own +# job_id, name, repeat type, due time, MQTT topic, state entity and completion +# entity/entities. Most future job changes should only require editing that +# registry block. +# +# If a new job uses a completion entity that is already listed in +# jobs_tracker_completion_entities, no trigger changes are needed. If the job +# uses a brand-new physical button, event entity, sensor or other completion +# entity, add that entity once to jobs_tracker_completion_entities. +# +# MQTT Discovery is used for job sensors, complete binary sensors, manual +# completion buttons, summary sensors and count sensors. This means adding a new +# job does not require hand-writing new MQTT sensor YAML blocks. # # JOB STATES: # Not Due +# Missed # Due # Overdue # Complete # # VERSION HISTORY: -# V1.4 2026-06-20 -# - Updated shared dog-fed button behaviour. -# - If either dog-feeding job is Due or Overdue, the shared physical button -# completes every dog-feeding job that is currently Due or Overdue. -# - If neither dog-feeding job is Due or Overdue, the shared physical button -# falls back to time-based early completion. +# V2.3 2026-07-20 +# - All completion methods now obey the configured completion window. +# - Physical completion entities, Home Assistant discovery buttons and direct +# MQTT completion commands are ignored outside the job's window. +# - Jobs without an explicit window continue to use the full-day default of +# 00:00 through 23:59. +# - Discovery device firmware version updated to 2.3. # -# V1.3 2026-06-20 -# - Added Not Due support for daily jobs before configured due time. -# - Added grouped MQTT Jobs Tracker device. +# V2.2 2026-07-10 +# - Renamed Previous Missed to Missed. +# - Windowed jobs now show Missed when the completion window ends. +# - Dog feeding can show Missed after the morning/evening window, while still +# resetting to Not Due / Due in the next daily period. +# - Discovery device firmware version updated to 2.2. +# +# V2.1 2026-07-08 +# - Fixed MQTT Discovery value_template strings by escaping inner templates. +# - This prevents Home Assistant rendering value_json too early when publishing +# discovery config. +# - Discovery device firmware version updated to 2.1. +# - Discovery automation now bootstraps job states after publishing discovery. +# +# V2.0 2026-07-07 +# - Refactored around a single job registry block. +# - Added per-job completion_entities. +# - Added optional per-job completion windows. +# - Dog feeding remains two separate daily jobs with one shared physical +# button and separate completion windows. +# - Replaced static MQTT sensor blocks with MQTT Discovery publishing. +# - Added MQTT manual completion buttons for each job. +# - Summary and counts now loop over the job registry. # # NOTES: # - due_weekday uses Python numbering: # Monday=0, Tuesday=1, Wednesday=2, Thursday=3, # Friday=4, Saturday=5, Sunday=6 # -# - event.dog_fed_button_xbt14_action is shared by: -# Dog Feeding Morning -# Dog Feeding Evening +# - completion_window_start and completion_window_end are optional. +# If omitted/commented, the completion entity is accepted at any time in +# the current reset period. # -# - sensor.jobs_done marks both: -# Dishwasher Unpacked -# Rubbish Removed +# - Dog feeding uses completion windows because both feeding jobs share the +# same physical button. # #:########################################################################################:# -#:########################################################################################:# -# MANUAL TEST BUTTONS # -#:########################################################################################:# - -input_button: - jobs_tracker_complete_dog_feeding_morning: - name: "Jobs Tracker Complete Dog Feeding Morning" - icon: mdi:dog-side - - jobs_tracker_complete_dog_feeding_evening: - name: "Jobs Tracker Complete Dog Feeding Evening" - icon: mdi:dog-side - - jobs_tracker_complete_dog_depooping: - name: "Jobs Tracker Complete Dog Depooping" - icon: mdi:emoticon-poop - - jobs_tracker_complete_dishwasher_unpacked: - name: "Jobs Tracker Complete Dishwasher Unpacked" - icon: mdi:dishwasher - - jobs_tracker_complete_rubbish_removed: - name: "Jobs Tracker Complete Rubbish Removed" - icon: mdi:trash-can - -#:########################################################################################:# -# MQTT SENSORS # -#:########################################################################################:# - -mqtt: - sensor: - #:####################################################################################:# - # Summary sensor # - #:####################################################################################:# - - - name: "Jobs Tracker - Summary" - unique_id: jobs_tracker_summary - state_topic: "viewroad-status/jobs_tracker/summary" - value_template: "{{ value_json.state | default('unknown') }}" - json_attributes_topic: "viewroad-status/jobs_tracker/summary" - icon: mdi:clipboard-list - device: - identifiers: - - viewroad_jobs_tracker - name: "Jobs Tracker" - manufacturer: "View Road Home Assistant" - model: "Shared household jobs tracker" - sw_version: "1.4" - - #:####################################################################################:# - # Individual job sensors # - #:####################################################################################:# - - - name: "Jobs Tracker - Dog Feeding Morning" - unique_id: jobs_tracker_dog_feeding_morning - state_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" - value_template: "{{ value_json.state | default('unknown') }}" - json_attributes_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" - icon: mdi:dog-side - device: - identifiers: - - viewroad_jobs_tracker - name: "Jobs Tracker" - manufacturer: "View Road Home Assistant" - model: "Shared household jobs tracker" - sw_version: "1.4" - - - name: "Jobs Tracker - Dog Feeding Evening" - unique_id: jobs_tracker_dog_feeding_evening - state_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" - value_template: "{{ value_json.state | default('unknown') }}" - json_attributes_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" - icon: mdi:dog-side - device: - identifiers: - - viewroad_jobs_tracker - name: "Jobs Tracker" - manufacturer: "View Road Home Assistant" - model: "Shared household jobs tracker" - sw_version: "1.4" - - - name: "Jobs Tracker - Dog Depooping" - unique_id: jobs_tracker_dog_depooping - state_topic: "viewroad-status/jobs_tracker/dog_depooping" - value_template: "{{ value_json.state | default('unknown') }}" - json_attributes_topic: "viewroad-status/jobs_tracker/dog_depooping" - icon: mdi:emoticon-poop - device: - identifiers: - - viewroad_jobs_tracker - name: "Jobs Tracker" - manufacturer: "View Road Home Assistant" - model: "Shared household jobs tracker" - sw_version: "1.4" - - - name: "Jobs Tracker - Dishwasher Unpacked" - unique_id: jobs_tracker_dishwasher_unpacked - state_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" - value_template: "{{ value_json.state | default('unknown') }}" - json_attributes_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" - icon: mdi:dishwasher - device: - identifiers: - - viewroad_jobs_tracker - name: "Jobs Tracker" - manufacturer: "View Road Home Assistant" - model: "Shared household jobs tracker" - sw_version: "1.4" - - - name: "Jobs Tracker - Rubbish Removed" - unique_id: jobs_tracker_rubbish_removed - state_topic: "viewroad-status/jobs_tracker/rubbish_removed" - value_template: "{{ value_json.state | default('unknown') }}" - json_attributes_topic: "viewroad-status/jobs_tracker/rubbish_removed" - icon: mdi:trash-can - device: - identifiers: - - viewroad_jobs_tracker - name: "Jobs Tracker" - manufacturer: "View Road Home Assistant" - model: "Shared household jobs tracker" - sw_version: "1.4" - -#:########################################################################################:# -# TEMPLATE SENSORS # -#:########################################################################################:# - -template: - - binary_sensor: - - name: "Job Complete - Dog Feeding Morning" - unique_id: job_complete_dog_feeding_morning - icon: mdi:dog-side - state: "{{ is_state('sensor.jobs_tracker_dog_feeding_morning', 'Complete') }}" - - - name: "Job Complete - Dog Feeding Evening" - unique_id: job_complete_dog_feeding_evening - icon: mdi:dog-side - state: "{{ is_state('sensor.jobs_tracker_dog_feeding_evening', 'Complete') }}" - - - name: "Job Complete - Dog Depooping" - unique_id: job_complete_dog_depooping - icon: mdi:emoticon-poop - state: "{{ is_state('sensor.jobs_tracker_dog_depooping', 'Complete') }}" - - - name: "Job Complete - Dishwasher Unpacked" - unique_id: job_complete_dishwasher_unpacked - icon: mdi:dishwasher - state: "{{ is_state('sensor.jobs_tracker_dishwasher_unpacked', 'Complete') }}" - - - name: "Job Complete - Rubbish Removed" - unique_id: job_complete_rubbish_removed - icon: mdi:trash-can - state: "{{ is_state('sensor.jobs_tracker_rubbish_removed', 'Complete') }}" - - - sensor: - - name: "Jobs Tracker - Complete Count" - unique_id: jobs_tracker_complete_count - icon: mdi:clipboard-check - state: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Complete') | list | count - }} - - - name: "Jobs Tracker - Not Due Count" - unique_id: jobs_tracker_not_due_count - icon: mdi:clipboard-clock-outline - state: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Not Due') | list | count - }} - - - name: "Jobs Tracker - Due Count" - unique_id: jobs_tracker_due_count - icon: mdi:clipboard-alert - state: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Due') | list | count - }} - - - name: "Jobs Tracker - Overdue Count" - unique_id: jobs_tracker_overdue_count - icon: mdi:clipboard-alert-outline - state: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Overdue') | list | count - }} - -#:########################################################################################:# -# SCRIPTS # -#:########################################################################################:# - script: jobs_tracker_dispatch: alias: "Jobs Tracker - Dispatch" @@ -291,14 +104,37 @@ script: example: "dog_feeding_morning" sequence: + #:####################################################################################:# + # JOB REGISTRY # + #:####################################################################################:# + # + # Add or edit jobs here. + # + # To add a new job: + # 1. Copy one full job block. + # 2. Change job_id, job_name, schedule settings and completion_entities. + # 3. If the completion entity is brand new, add it once to + # jobs_tracker_completion_entities below. + # + # Completion windows: + # - Leave commented out for normal jobs. + # - Uncomment and set for jobs sharing the same physical button. + # + # MQTT Discovery is generated from this list. + - variables: - jobs_tracker_jobs: + jobs_tracker_jobs: &jobs_tracker_jobs - job_id: dog_feeding_morning job_name: "Dog Feeding Morning" repeat_type: daily due_time: "06:00" due_weekday: 0 due_day: 1 + completion_entities: + - event.jobs_dog_fed_button_x14bt_action + completion_window_start: "00:00" + completion_window_end: "11:59" + icon: mdi:dog-side mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" state_entity: "sensor.jobs_tracker_dog_feeding_morning" @@ -308,6 +144,11 @@ script: due_time: "17:00" due_weekday: 0 due_day: 1 + completion_entities: + - event.jobs_dog_fed_button_x14bt_action + completion_window_start: "12:00" + completion_window_end: "23:59" + icon: mdi:dog-side mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" state_entity: "sensor.jobs_tracker_dog_feeding_evening" @@ -317,6 +158,11 @@ script: due_time: "08:00" due_weekday: 4 due_day: 1 + completion_entities: + - event.jobs_dog_depooped_button_x15bt_action + # completion_window_start: "00:00" + # completion_window_end: "23:59" + icon: mdi:emoticon-poop mqtt_topic: "viewroad-status/jobs_tracker/dog_depooping" state_entity: "sensor.jobs_tracker_dog_depooping" @@ -326,6 +172,11 @@ script: due_time: "15:30" due_weekday: 0 due_day: 1 + completion_entities: + - event.kitchen_jobs_kitchen_completed_buttons_button_1 + # completion_window_start: "00:00" + # completion_window_end: "23:59" + icon: mdi:dishwasher mqtt_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" state_entity: "sensor.jobs_tracker_dishwasher_unpacked" @@ -335,9 +186,27 @@ script: due_time: "15:30" due_weekday: 0 due_day: 1 + completion_entities: + - event.kitchen_jobs_kitchen_completed_buttons_button_2 + # completion_window_start: "00:00" + # completion_window_end: "23:59" + icon: mdi:trash-can mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed" state_entity: "sensor.jobs_tracker_rubbish_removed" + #:################################################################################:# + # COMPLETION ENTITY TRIGGER LIST # + #:################################################################################:# + # + # Add brand-new physical/action/sensor completion entities here. + # If a new job reuses an entity already listed here, no change is needed. + + jobs_tracker_completion_entities: &jobs_tracker_completion_entities + - event.jobs_dog_fed_button_x14bt_action + - event.jobs_dog_depooped_button_x15bt_action + - event.kitchen_jobs_kitchen_completed_buttons_button_1 + - event.kitchen_jobs_kitchen_completed_buttons_button_2 + - choose: #:################################################################################:# # Refresh all jobs # @@ -360,6 +229,8 @@ script: due_day: "{{ repeat.item.due_day }}" mqtt_topic: "{{ repeat.item.mqtt_topic }}" state_entity: "{{ repeat.item.state_entity }}" + completion_window_start: "{{ repeat.item.completion_window_start | default('') }}" + completion_window_end: "{{ repeat.item.completion_window_end | default('') }}" mark_complete: false #:##################################################################################:# @@ -384,14 +255,317 @@ script: due_day: "{{ repeat.item.due_day }}" mqtt_topic: "{{ repeat.item.mqtt_topic }}" state_entity: "{{ repeat.item.state_entity }}" + completion_window_start: "{{ repeat.item.completion_window_start | default('') }}" + completion_window_end: "{{ repeat.item.completion_window_end | default('') }}" mark_complete: true - # Give the MQTT sensors a moment to update from the retained job payloads + # Give MQTT sensors a moment to update from the retained job payloads # before building the summary payload. - delay: "00:00:01" - action: script.jobs_tracker_publish_summary + jobs_tracker_route_completion: + alias: "Jobs Tracker - Route Completion" + description: "Routes a physical/sensor/MQTT completion action to matching jobs." + mode: queued + max: 20 + + fields: + completion_entity: + description: "Entity ID that triggered completion." + example: "event.jobs_dog_fed_button_x14bt_action" + + manual_job_id: + description: "Job ID completed by MQTT/manual button." + example: "dog_feeding_morning" + + sequence: + - variables: + jobs_tracker_jobs: *jobs_tracker_jobs + completion_entity_safe: "{{ completion_entity | default('') }}" + manual_job_id_safe: "{{ manual_job_id | default('') }}" + now_minutes: "{{ (now().hour * 60) + now().minute }}" + + - repeat: + for_each: "{{ jobs_tracker_jobs }}" + sequence: + - variables: + completion_entities_safe: "{{ repeat.item.completion_entities | default([]) }}" + window_start_safe: "{{ repeat.item.completion_window_start | default('00:00') }}" + window_end_safe: "{{ repeat.item.completion_window_end | default('23:59') }}" + + window_start_minutes: >- + {% set t = window_start_safe.split(':') %} + {{ (t[0] | int(0) * 60) + (t[1] | int(0)) }} + + window_end_minutes: >- + {% set t = window_end_safe.split(':') %} + {{ (t[0] | int(23) * 60) + (t[1] | int(59)) }} + + within_completion_window: >- + {% set n = now_minutes | int(0) %} + {% set s = window_start_minutes | int(0) %} + {% set e = window_end_minutes | int(1439) %} + {% if s <= e %} + {{ n >= s and n <= e }} + {% else %} + {{ n >= s or n <= e }} + {% endif %} + + physical_entity_matches: >- + {{ + completion_entity_safe != '' + and completion_entity_safe in completion_entities_safe + }} + + manual_job_matches: >- + {{ + manual_job_id_safe != '' + and manual_job_id_safe == repeat.item.job_id + }} + + completion_source_matches: >- + {{ + physical_entity_matches | bool + or manual_job_matches | bool + }} + + should_complete_job: >- + {{ + completion_source_matches | bool + and within_completion_window | bool + }} + + - if: + - condition: template + value_template: "{{ should_complete_job | bool }}" + then: + - action: script.jobs_tracker_dispatch + data: + job_id: "{{ repeat.item.job_id }}" + + jobs_tracker_publish_discovery: + alias: "Jobs Tracker - Publish MQTT Discovery" + description: "Publishes retained MQTT Discovery config for Jobs Tracker entities." + mode: queued + max: 5 + + sequence: + - variables: + jobs_tracker_jobs: *jobs_tracker_jobs + discovery_prefix: "homeassistant" + device_identifiers: + - viewroad_jobs_tracker + device_name: "Jobs Tracker" + device_manufacturer: "View Road Home Assistant" + device_model: "Shared household jobs tracker" + device_sw_version: "2.3" + + value_template_state: >- + {% raw %}{{ value_json.state | default('unknown') }}{% endraw %} + + value_template_complete_count: >- + {% raw %}{{ value_json.complete_count | default(0) }}{% endraw %} + + value_template_not_due_count: >- + {% raw %}{{ value_json.not_due_count | default(0) }}{% endraw %} + + value_template_missed_count: >- + {% raw %}{{ value_json.missed_count | default(value_json.previous_missed_count | default(0)) }}{% endraw %} + + value_template_due_count: >- + {% raw %}{{ value_json.due_count | default(0) }}{% endraw %} + + value_template_overdue_count: >- + {% raw %}{{ value_json.overdue_count | default(0) }}{% endraw %} + + value_template_total_count: >- + {% raw %}{{ value_json.total_count | default(0) }}{% endraw %} + + value_template_job_complete: >- + {% raw %}{{ 'ON' if value_json.state == 'Complete' else 'OFF' }}{% endraw %} + + #:####################################################################################:# + # Summary sensor discovery # + #:####################################################################################:# + + - action: mqtt.publish + data: + topic: "{{ discovery_prefix }}/sensor/jobs_tracker_summary/config" + retain: true + qos: 1 + payload: >- + {{ + { + "name": "Jobs Tracker - Summary", + "unique_id": "jobs_tracker_summary", + "object_id": "jobs_tracker_summary", + "state_topic": "viewroad-status/jobs_tracker/summary", + "value_template": value_template_state, + "json_attributes_topic": "viewroad-status/jobs_tracker/summary", + "icon": "mdi:clipboard-list", + "device": { + "identifiers": device_identifiers, + "name": device_name, + "manufacturer": device_manufacturer, + "model": device_model, + "sw_version": device_sw_version + } + } | tojson + }} + + # Remove old V2.1 discovery topic for the renamed count sensor. + - action: mqtt.publish + data: + topic: "{{ discovery_prefix }}/sensor/jobs_tracker_previous_missed_count/config" + retain: true + qos: 1 + payload: "" + + #:####################################################################################:# + # Count sensor discovery # + #:####################################################################################:# + + - repeat: + for_each: + - count_id: complete_count + name: "Jobs Tracker - Complete Count" + icon: mdi:clipboard-check + value_template: "{{ value_template_complete_count }}" + + - count_id: not_due_count + name: "Jobs Tracker - Not Due Count" + icon: mdi:clipboard-clock-outline + value_template: "{{ value_template_not_due_count }}" + + - count_id: missed_count + name: "Jobs Tracker - Missed Count" + icon: mdi:clipboard-remove-outline + value_template: "{{ value_template_missed_count }}" + + - count_id: due_count + name: "Jobs Tracker - Due Count" + icon: mdi:clipboard-alert + value_template: "{{ value_template_due_count }}" + + - count_id: overdue_count + name: "Jobs Tracker - Overdue Count" + icon: mdi:clipboard-alert-outline + value_template: "{{ value_template_overdue_count }}" + + - count_id: total_count + name: "Jobs Tracker - Total Count" + icon: mdi:clipboard-list-outline + value_template: "{{ value_template_total_count }}" + + sequence: + - action: mqtt.publish + data: + topic: "{{ discovery_prefix }}/sensor/jobs_tracker_{{ repeat.item.count_id }}/config" + retain: true + qos: 1 + payload: >- + {{ + { + "name": repeat.item.name, + "unique_id": "jobs_tracker_" ~ repeat.item.count_id, + "object_id": "jobs_tracker_" ~ repeat.item.count_id, + "state_topic": "viewroad-status/jobs_tracker/summary", + "value_template": repeat.item.value_template, + "icon": repeat.item.icon, + "device": { + "identifiers": device_identifiers, + "name": device_name, + "manufacturer": device_manufacturer, + "model": device_model, + "sw_version": device_sw_version + } + } | tojson + }} + + #:####################################################################################:# + # Per-job sensor, binary sensor and manual button discovery # + #:####################################################################################:# + + - repeat: + for_each: "{{ jobs_tracker_jobs }}" + sequence: + - action: mqtt.publish + data: + topic: "{{ discovery_prefix }}/sensor/jobs_tracker_{{ repeat.item.job_id }}/config" + retain: true + qos: 1 + payload: >- + {{ + { + "name": "Jobs Tracker - " ~ repeat.item.job_name, + "unique_id": "jobs_tracker_" ~ repeat.item.job_id, + "object_id": "jobs_tracker_" ~ repeat.item.job_id, + "state_topic": repeat.item.mqtt_topic, + "value_template": value_template_state, + "json_attributes_topic": repeat.item.mqtt_topic, + "icon": repeat.item.icon | default("mdi:clipboard-check-outline"), + "device": { + "identifiers": device_identifiers, + "name": device_name, + "manufacturer": device_manufacturer, + "model": device_model, + "sw_version": device_sw_version + } + } | tojson + }} + + - action: mqtt.publish + data: + topic: "{{ discovery_prefix }}/binary_sensor/job_complete_{{ repeat.item.job_id }}/config" + retain: true + qos: 1 + payload: >- + {{ + { + "name": "Job Complete - " ~ repeat.item.job_name, + "unique_id": "job_complete_" ~ repeat.item.job_id, + "object_id": "job_complete_" ~ repeat.item.job_id, + "state_topic": repeat.item.mqtt_topic, + "value_template": value_template_job_complete, + "payload_on": "ON", + "payload_off": "OFF", + "icon": repeat.item.icon | default("mdi:clipboard-check-outline"), + "device": { + "identifiers": device_identifiers, + "name": device_name, + "manufacturer": device_manufacturer, + "model": device_model, + "sw_version": device_sw_version + } + } | tojson + }} + + - action: mqtt.publish + data: + topic: "{{ discovery_prefix }}/button/jobs_tracker_complete_{{ repeat.item.job_id }}/config" + retain: true + qos: 1 + payload: >- + {{ + { + "name": "Complete Job - " ~ repeat.item.job_name, + "unique_id": "jobs_tracker_complete_" ~ repeat.item.job_id, + "object_id": "jobs_tracker_complete_" ~ repeat.item.job_id, + "command_topic": "viewroad-commands/jobs_tracker/" ~ repeat.item.job_id ~ "/complete", + "payload_press": "PRESS", + "icon": repeat.item.icon | default("mdi:clipboard-check-outline"), + "device": { + "identifiers": device_identifiers, + "name": device_name, + "manufacturer": device_manufacturer, + "model": device_model, + "sw_version": device_sw_version + } + } | tojson + }} + jobs_tracker_publish_summary: alias: "Jobs Tracker - Publish Summary" description: "Publishes retained MQTT JSON summary for all tracked jobs." @@ -400,82 +574,7 @@ script: sequence: - variables: - complete_count: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Complete') | list | count - }} - - not_due_count: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Not Due') | list | count - }} - - due_count: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Due') | list | count - }} - - overdue_count: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] | select('eq', 'Overdue') | list | count - }} - - unknown_count: >- - {{ - [ - states('sensor.jobs_tracker_dog_feeding_morning'), - states('sensor.jobs_tracker_dog_feeding_evening'), - states('sensor.jobs_tracker_dog_depooping'), - states('sensor.jobs_tracker_dishwasher_unpacked'), - states('sensor.jobs_tracker_rubbish_removed') - ] - | reject('in', ['Complete', 'Not Due', 'Due', 'Overdue']) - | list - | count - }} - - summary_state: >- - {% if overdue_count | int(0) > 0 and due_count | int(0) > 0 %} - {{ overdue_count }} Overdue, {{ due_count }} Due - {% elif overdue_count | int(0) > 0 %} - {{ overdue_count }} Overdue - {% elif due_count | int(0) > 0 %} - {{ due_count }} Due - {% elif not_due_count | int(0) > 0 and complete_count | int(0) > 0 %} - {{ complete_count }} Complete, {{ not_due_count }} Not Due - {% elif not_due_count | int(0) > 0 %} - {{ not_due_count }} Not Due - {% elif complete_count | int(0) == 5 %} - All Complete - {% elif unknown_count | int(0) > 0 %} - {{ unknown_count }} Unknown - {% else %} - Unknown - {% endif %} + jobs_tracker_jobs: *jobs_tracker_jobs - action: mqtt.publish data: @@ -483,72 +582,95 @@ script: retain: true qos: 1 payload: >- + {% set ns = namespace( + jobs=[], + complete=0, + not_due=0, + missed=0, + due=0, + overdue=0, + unknown=0 + ) %} + + {% for job in jobs_tracker_jobs %} + {% set s = states(job.state_entity) %} + {% if s == 'Complete' %} + {% set ns.complete = ns.complete + 1 %} + {% elif s == 'Not Due' %} + {% set ns.not_due = ns.not_due + 1 %} + {% elif s == 'Missed' %} + {% set ns.missed = ns.missed + 1 %} + {% elif s == 'Due' %} + {% set ns.due = ns.due + 1 %} + {% elif s == 'Overdue' %} + {% set ns.overdue = ns.overdue + 1 %} + {% else %} + {% set ns.unknown = ns.unknown + 1 %} + {% endif %} + + {% set ns.jobs = ns.jobs + [ + { + "job_id": job.job_id, + "job_name": job.job_name, + "state": s, + "repeat_type": job.repeat_type, + "due_time": job.due_time, + "due_weekday": job.due_weekday, + "due_day": job.due_day, + "completion_entities": job.completion_entities | default([]), + "completion_window_start": job.completion_window_start | default(""), + "completion_window_end": job.completion_window_end | default(""), + "mqtt_topic": job.mqtt_topic, + "state_entity": job.state_entity, + "next_due_human": state_attr(job.state_entity, 'next_due_human'), + "last_completed_human": state_attr(job.state_entity, 'last_completed_human'), + "missed": state_attr(job.state_entity, 'missed'), + "completion_window_missed": state_attr(job.state_entity, 'completion_window_missed'), + "previous_missed": state_attr(job.state_entity, 'previous_missed'), + "previous_missed_active": state_attr(job.state_entity, 'previous_missed_active'), + "minutes_since_required": state_attr(job.state_entity, 'minutes_since_required'), + "hours_since_required": state_attr(job.state_entity, 'hours_since_required'), + "hours_since_completed": state_attr(job.state_entity, 'hours_since_completed') + } + ] %} + {% endfor %} + + {% set parts = [] %} + {% if ns.overdue > 0 %} + {% set parts = parts + [ns.overdue ~ ' Overdue'] %} + {% endif %} + {% if ns.due > 0 %} + {% set parts = parts + [ns.due ~ ' Due'] %} + {% endif %} + {% if ns.missed > 0 %} + {% set parts = parts + [ns.missed ~ ' Missed'] %} + {% endif %} + {% if ns.not_due > 0 %} + {% set parts = parts + [ns.not_due ~ ' Not Due'] %} + {% endif %} + + {% if parts | count > 0 %} + {% set summary_state = parts | join(', ') %} + {% elif ns.complete == (jobs_tracker_jobs | count) %} + {% set summary_state = 'All Complete' %} + {% elif ns.unknown > 0 %} + {% set summary_state = ns.unknown ~ ' Unknown' %} + {% else %} + {% set summary_state = 'Unknown' %} + {% endif %} + {{ { - "state": summary_state | trim, - "complete_count": complete_count | int(0), - "not_due_count": not_due_count | int(0), - "due_count": due_count | int(0), - "overdue_count": overdue_count | int(0), - "unknown_count": unknown_count | int(0), - "total_count": 5, - "jobs": [ - { - "job_id": "dog_feeding_morning", - "job_name": "Dog Feeding Morning", - "state": states('sensor.jobs_tracker_dog_feeding_morning'), - "next_due_human": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'next_due_human'), - "last_completed_human": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'last_completed_human'), - "minutes_since_required": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'minutes_since_required'), - "hours_since_required": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'hours_since_required'), - "hours_since_completed": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'hours_since_completed') - }, - { - "job_id": "dog_feeding_evening", - "job_name": "Dog Feeding Evening", - "state": states('sensor.jobs_tracker_dog_feeding_evening'), - "next_due_human": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'next_due_human'), - "last_completed_human": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'last_completed_human'), - "minutes_since_required": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'minutes_since_required'), - "hours_since_required": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'hours_since_required'), - "hours_since_completed": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'hours_since_completed') - }, - { - "job_id": "dog_depooping", - "job_name": "Dog De-pooping", - "state": states('sensor.jobs_tracker_dog_depooping'), - "next_due_human": state_attr('sensor.jobs_tracker_dog_depooping', 'next_due_human'), - "last_completed_human": state_attr('sensor.jobs_tracker_dog_depooping', 'last_completed_human'), - "minutes_since_required": state_attr('sensor.jobs_tracker_dog_depooping', 'minutes_since_required'), - "hours_since_required": state_attr('sensor.jobs_tracker_dog_depooping', 'hours_since_required'), - "hours_since_completed": state_attr('sensor.jobs_tracker_dog_depooping', 'hours_since_completed') - }, - { - "job_id": "dishwasher_unpacked", - "job_name": "Dishwasher Unpacked", - "state": states('sensor.jobs_tracker_dishwasher_unpacked'), - "next_due_human": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'next_due_human'), - "last_completed_human": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'last_completed_human'), - "minutes_since_required": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'minutes_since_required'), - "hours_since_required": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'hours_since_required'), - "hours_since_completed": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'hours_since_completed') - }, - { - "job_id": "rubbish_removed", - "job_name": "Rubbish Removed", - "state": states('sensor.jobs_tracker_rubbish_removed'), - "next_due_human": state_attr('sensor.jobs_tracker_rubbish_removed', 'next_due_human'), - "last_completed_human": state_attr('sensor.jobs_tracker_rubbish_removed', 'last_completed_human'), - "minutes_since_required": state_attr('sensor.jobs_tracker_rubbish_removed', 'minutes_since_required'), - "hours_since_required": state_attr('sensor.jobs_tracker_rubbish_removed', 'hours_since_required'), - "hours_since_completed": state_attr('sensor.jobs_tracker_rubbish_removed', 'hours_since_completed') - } - ], - "dog_feeding_morning": states('sensor.jobs_tracker_dog_feeding_morning'), - "dog_feeding_evening": states('sensor.jobs_tracker_dog_feeding_evening'), - "dog_depooping": states('sensor.jobs_tracker_dog_depooping'), - "dishwasher_unpacked": states('sensor.jobs_tracker_dishwasher_unpacked'), - "rubbish_removed": states('sensor.jobs_tracker_rubbish_removed'), + "state": summary_state, + "complete_count": ns.complete, + "not_due_count": ns.not_due, + "missed_count": ns.missed, + "previous_missed_count": ns.missed, + "due_count": ns.due, + "overdue_count": ns.overdue, + "unknown_count": ns.unknown, + "total_count": jobs_tracker_jobs | count, + "jobs": ns.jobs, "last_updated_local": now().strftime('%Y-%m-%d %H:%M:%S') } | tojson }} @@ -558,6 +680,31 @@ script: #:########################################################################################:# automation: + - id: jobs_tracker_publish_discovery + alias: "Jobs Tracker - Publish Discovery" + description: "Publishes retained MQTT Discovery config and bootstraps Jobs Tracker state." + mode: single + + triggers: + - trigger: homeassistant + event: start + id: ha_start + + - trigger: time + at: "03:05:00" + id: daily_republish + + actions: + - action: script.jobs_tracker_publish_discovery + + # Give Home Assistant a moment to process MQTT Discovery. + - delay: "00:00:10" + + # Force retained state payloads onto each job topic. + - action: script.jobs_tracker_dispatch + data: + update_all: true + - id: jobs_tracker_refresh_all_jobs alias: "Jobs Tracker - Refresh All Jobs" description: "Refreshes derived due/overdue values for every job." @@ -573,12 +720,15 @@ automation: id: tick actions: - # Let retained MQTT job messages restore before the first refresh. - if: - condition: template value_template: "{{ trigger.id == 'ha_start' }}" then: - - delay: "00:01:00" + - action: script.jobs_tracker_publish_discovery + + # Let retained MQTT Discovery and job state messages restore before + # the first refresh. + - delay: "00:01:30" - action: script.jobs_tracker_dispatch data: @@ -586,50 +736,13 @@ automation: - id: jobs_tracker_completion_actions alias: "Jobs Tracker - Completion Actions" - description: "Marks jobs complete when physical/action/manual buttons are pressed." + description: "Marks jobs complete when physical/action/sensor entities change." mode: queued max: 20 triggers: - #:####################################################################################:# - # Physical / action entities # - #:####################################################################################:# - - trigger: state - entity_id: event.dog_fed_button_xbt14_action - id: dog_fed_button - - - trigger: state - entity_id: event.dog_depooped_button_xbt15_action - id: dog_depooped_button - - - trigger: state - entity_id: sensor.jobs_done - id: jobs_done_sensor - - #:####################################################################################:# - # Manual test buttons # - #:####################################################################################:# - - - trigger: state - entity_id: input_button.jobs_tracker_complete_dog_feeding_morning - id: manual_dog_feeding_morning - - - trigger: state - entity_id: input_button.jobs_tracker_complete_dog_feeding_evening - id: manual_dog_feeding_evening - - - trigger: state - entity_id: input_button.jobs_tracker_complete_dog_depooping - id: manual_dog_depooping - - - trigger: state - entity_id: input_button.jobs_tracker_complete_dishwasher_unpacked - id: manual_dishwasher_unpacked - - - trigger: state - entity_id: input_button.jobs_tracker_complete_rubbish_removed - id: manual_rubbish_removed + entity_id: *jobs_tracker_completion_entities conditions: - condition: template @@ -640,170 +753,24 @@ automation: }} actions: - - choose: - #:################################################################################:# - # Dog fed physical button # - #:################################################################################:# - # - # Shared button behaviour: - # - # If any dog feeding jobs are already Due or Overdue: - # - Complete every linked dog-feeding job that is currently Due or Overdue. - # - # If no dog feeding jobs are currently Due or Overdue: - # - Fall back to time-based behaviour so early completion still works. - # - # Examples: - # - # 07:00, morning Due, evening Not Due: - # - Completes morning only. - # - # 23:30, morning Overdue, evening Overdue: - # - Completes morning and evening. - # - # 16:30, morning Complete, evening Not Due: - # - Completes evening early. - # - #:################################################################################:# + - action: script.jobs_tracker_route_completion + data: + completion_entity: "{{ trigger.entity_id }}" - - conditions: - - condition: template - value_template: "{{ trigger.id == 'dog_fed_button' }}" - sequence: - - variables: - dog_feeding_morning_state: "{{ states('sensor.jobs_tracker_dog_feeding_morning') }}" - dog_feeding_evening_state: "{{ states('sensor.jobs_tracker_dog_feeding_evening') }}" + - id: jobs_tracker_mqtt_manual_completion + alias: "Jobs Tracker - MQTT Manual Completion" + description: "Marks one job complete from its MQTT Discovery manual button." + mode: queued + max: 20 - dog_feeding_any_due_or_overdue: >- - {{ - dog_feeding_morning_state in ['Due', 'Overdue'] - or dog_feeding_evening_state in ['Due', 'Overdue'] - }} + triggers: + - trigger: mqtt + topic: "viewroad-commands/jobs_tracker/+/complete" - #:############################################################################:# - # Complete all linked dog feeding jobs that are already Due or Overdue # - #:############################################################################:# + actions: + - variables: + manual_job_id: "{{ trigger.topic.split('/')[2] }}" - - if: - - condition: template - value_template: "{{ dog_feeding_morning_state in ['Due', 'Overdue'] }}" - then: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_feeding_morning - - - if: - - condition: template - value_template: "{{ dog_feeding_evening_state in ['Due', 'Overdue'] }}" - then: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_feeding_evening - - #:############################################################################:# - # Fallback for early completion # - #:############################################################################:# - # - # Only used when neither dog feeding job is currently Due or Overdue. - # This keeps useful behaviour such as feeding early before the due time. - # It will not overwrite an already Complete job. - - - if: - - condition: template - value_template: >- - {{ - not (dog_feeding_any_due_or_overdue | bool) - and now().hour < 12 - and dog_feeding_morning_state != 'Complete' - }} - then: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_feeding_morning - - - if: - - condition: template - value_template: >- - {{ - not (dog_feeding_any_due_or_overdue | bool) - and now().hour >= 12 - and dog_feeding_evening_state != 'Complete' - }} - then: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_feeding_evening - - #:################################################################################:# - # Dog de-pooped physical button # - #:################################################################################:# - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'dog_depooped_button' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_depooping - - #:################################################################################:# - # Shared jobs_done sensor # - #:################################################################################:# - # - # One state change marks both jobs complete. - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'jobs_done_sensor' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: dishwasher_unpacked - - - action: script.jobs_tracker_dispatch - data: - job_id: rubbish_removed - - #:################################################################################:# - # Manual test buttons # - #:################################################################################:# - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'manual_dog_feeding_morning' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_feeding_morning - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'manual_dog_feeding_evening' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_feeding_evening - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'manual_dog_depooping' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: dog_depooping - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'manual_dishwasher_unpacked' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: dishwasher_unpacked - - - conditions: - - condition: template - value_template: "{{ trigger.id == 'manual_rubbish_removed' }}" - sequence: - - action: script.jobs_tracker_dispatch - data: - job_id: rubbish_removed + - action: script.jobs_tracker_route_completion + data: + manual_job_id: "{{ manual_job_id }}" diff --git a/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml b/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml index ecc88d5..fe549ef 100644 --- a/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml +++ b/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml @@ -1,6 +1,6 @@ -################################################################################ -# Jobs Tracker Processor Package -################################################################################ +#:########################################################################################:# +# Jobs Tracker Processor Package # +#:########################################################################################:# # # TITLE: # Jobs Tracker Processor @@ -9,20 +9,122 @@ # jobs_tracker_processor.yaml # # VERSION: -# V1.3.1 2026-06-20 +# V1.6 2026-07-10 # -# CHANGELOG: -# V1.3.1: +# PURPOSE: +# Generic processing engine for the Jobs Tracker system. +# +# OPERATION: +# This package is the generic processing engine for the Jobs Tracker system. +# It is not normally edited when adding, removing or changing household jobs. +# Jobs are defined in the separate job-list package, which calls this processor +# through script.jobs_tracker_update_job. The processor calculates whether each +# job is Not Due, Missed, Due, Overdue or Complete, then publishes the +# result as retained MQTT JSON to that job's configured MQTT topic. The retained +# MQTT payload stores the last completion timestamp, so after a Home Assistant +# restart the processor can recalculate the correct state from the last physical +# button/action press. +# +# Normal operation is automatic. The job-list package refreshes all jobs on a +# regular timer and after Home Assistant starts, and it calls this processor with +# mark_complete: true when a physical button, event, sensor, MQTT button or other +# completion action confirms a job has been completed. A job is considered +# Complete when its stored last_completed_ts is within the current reset period. +# +# Daily jobs reset at 00:00 but only become Due at their configured due_time. +# Weekly jobs reset at the week rollover, currently Monday 00:00. Monthly jobs +# reset at the month rollover, currently the 1st day of the month at 00:00. +# +# If the previous reset period was missed, jobs without a completion window are +# marked Missed after the reset and before the current due time. Jobs with a +# completion window are marked Missed after the current completion window ends. +# This lets jobs such as morning dog feeding show Missed after the morning +# window, but then reset to Not Due/Due again the next morning. +# +# Once the current due time arrives, a job changes to Due, then Overdue after +# the configured threshold unless the job has reached the end of its completion +# window, in which case it changes to Missed. Completing a job early is allowed +# once the current reset period has started. +# +# SETTINGS: +# The job-list package passes these settings into this processor for each job: +# +# job_id: +# Machine-safe ID used for matching and MQTT topic naming. +# Example: dog_feeding_morning +# +# job_name: +# Human-readable job name used in the retained MQTT JSON. +# Example: Dog Feeding Morning +# +# repeat_type: +# Schedule type. Valid values are daily, weekly or monthly. +# +# due_time: +# Time the job is due, in HH:MM 24-hour format. +# Example: "06:00" +# +# due_weekday: +# Weekly jobs only. Monday=0, Tuesday=1, Wednesday=2, Thursday=3, +# Friday=4, Saturday=5, Sunday=6. +# +# due_day: +# Monthly jobs only. Use 1-28 for best reliability across all months. +# +# mqtt_topic: +# Retained MQTT state topic for this job. +# Example: viewroad-status/jobs_tracker/dog_feeding_morning +# +# state_entity: +# MQTT sensor entity that reads this job's retained JSON payload. +# Example: sensor.jobs_tracker_dog_feeding_morning +# +# mark_complete: +# false when refreshing/calculating the job state. +# true when a physical/manual completion action has just occurred. +# +# completion_window_start / completion_window_end: +# Optional HH:MM window used for jobs that share a physical button or have +# a clear daily completion window. If the current window ends before the +# job is completed, the job is marked Missed instead of remaining Overdue. +# Example: "00:00" to "11:59" for morning dog feeding. +# +# Current built-in overdue thresholds are: +# - Daily jobs: 2 hours after due_time +# - Weekly jobs: 2 days after due_time +# - Monthly jobs: 2 weeks after due_time +# +# VERSION HISTORY: +# V1.6 2026-07-10 +# - Renamed displayed state from Missed to Missed. +# - Added optional completion_window_start and completion_window_end fields. +# - Windowed jobs now become Missed when the completion window has ended. +# - Windowed jobs reset normally into Not Due / Due in the next period. +# +# V1.5 2026-07-07 +# - Documentation update for the refactored registry-driven job-list package. +# - No functional processing change from V1.4. +# +# V1.4 2026-07-06 +# - Added Missed state. +# - If the previous reset period was missed, the job now shows Previous +# Missed until the current period's due time. +# - Missed applies to daily, weekly and monthly jobs. +# - Due now starts at the configured due time for daily, weekly and monthly +# jobs. +# - Early completion remains supported after the current reset period starts. +# +# V1.3.1 2026-06-20 # - Corrected script field definitions so Home Assistant reliably creates # script.jobs_tracker_update_job. # -# V1.3: +# V1.3 2026-06-20 # - Daily jobs reset at 00:00 but do not become Due until their configured # due time. -# - Added "Not Due" state. +# - Added Not Due state. # - Completion is recalculated from retained last_completed_ts after restart. # -################################################################################ +#:########################################################################################:# script: jobs_tracker_update_job: @@ -59,6 +161,12 @@ script: mark_complete: description: "Set true when a human has completed the job." example: true + completion_window_start: + description: "Optional HH:MM completion window start." + example: "00:00" + completion_window_end: + description: "Optional HH:MM completion window end." + example: "11:59" sequence: - variables: @@ -73,6 +181,17 @@ script: now_ts: "{{ as_timestamp(now()) }}" today_midnight_ts: "{{ as_timestamp(today_at('00:00')) }}" topic_safe: "{{ mqtt_topic | default('viewroad-status/jobs_tracker/' ~ job_id) }}" + completion_window_start_safe: "{{ completion_window_start | default('') }}" + completion_window_end_safe: "{{ completion_window_end | default('') }}" + has_completion_window: >- + {{ + completion_window_start_safe not in ['', none, 'unknown', 'unavailable'] + and completion_window_end_safe not in ['', none, 'unknown', 'unavailable'] + }} + + #:####################################################################################:# + # Current and next reset periods # + #:####################################################################################:# - variables: current_period_start_ts: >- @@ -98,6 +217,28 @@ script: {{ (today_midnight_ts | float(0)) + 86400 }} {% endif %} + #:####################################################################################:# + # Previous reset period # + #:####################################################################################:# + + - variables: + previous_period_start_ts: >- + {% if repeat_type_safe == 'daily' %} + {{ (current_period_start_ts | float(0)) - 86400 }} + {% elif repeat_type_safe == 'weekly' %} + {{ (current_period_start_ts | float(0)) - 604800 }} + {% elif repeat_type_safe == 'monthly' %} + {% set first_day_this_month = now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) %} + {% set last_day_previous_month = first_day_this_month - timedelta(days=1) %} + {{ as_timestamp(last_day_previous_month.replace(day=1, hour=0, minute=0, second=0, microsecond=0)) }} + {% else %} + {{ (current_period_start_ts | float(0)) - 86400 }} + {% endif %} + + #:####################################################################################:# + # Due times # + #:####################################################################################:# + - variables: current_due_ts: >- {% if repeat_type_safe == 'daily' %} @@ -110,6 +251,19 @@ script: {{ (current_period_start_ts | float(0)) + (due_seconds | int) }} {% endif %} + previous_due_ts: >- + {% if repeat_type_safe == 'daily' %} + {{ (previous_period_start_ts | float(0)) + (due_seconds | int) }} + {% elif repeat_type_safe == 'weekly' %} + {{ (previous_period_start_ts | float(0)) + ((due_weekday_safe | int) * 86400) + (due_seconds | int) }} + {% elif repeat_type_safe == 'monthly' %} + {% set first_day_this_month = now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) %} + {% set last_day_previous_month = first_day_this_month - timedelta(days=1) %} + {{ as_timestamp(last_day_previous_month.replace(day=(due_day_safe | int), hour=(due_hour | int), minute=(due_minute | int), second=0, microsecond=0)) }} + {% else %} + {{ (previous_period_start_ts | float(0)) + (due_seconds | int) }} + {% endif %} + next_due_ts: >- {% if repeat_type_safe == 'daily' %} {{ (next_period_start_ts | float(0)) + (due_seconds | int) }} @@ -133,6 +287,60 @@ script: 120 {% endif %} + #:####################################################################################:# + # Completion window timing # + #:####################################################################################:# + # + # For windowed jobs, Missed begins when the completion window has ended. + # The window is anchored to the current due date. End times are treated as + # inclusive through the final minute, so "11:59" ends at 11:59:59. + + - variables: + completion_window_start_seconds: >- + {% if has_completion_window | bool %} + {% set t = completion_window_start_safe.split(':') %} + {{ ((t[0] | int(0)) * 3600) + ((t[1] | int(0)) * 60) }} + {% else %} + 0 + {% endif %} + + completion_window_end_seconds: >- + {% if has_completion_window | bool %} + {% set t = completion_window_end_safe.split(':') %} + {{ ((t[0] | int(23)) * 3600) + ((t[1] | int(59)) * 60) + 59 }} + {% else %} + 86399 + {% endif %} + + current_due_day_start_ts: >- + {% set d = (current_due_ts | float(0)) | timestamp_custom('%Y-%m-%d', true) %} + {{ as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) }} + + current_completion_window_start_ts: >- + {% if has_completion_window | bool %} + {{ (current_due_day_start_ts | float(0)) + (completion_window_start_seconds | int(0)) }} + {% else %} + 0 + {% endif %} + + current_completion_window_end_ts: >- + {% if has_completion_window | bool %} + {% set start_seconds = completion_window_start_seconds | int(0) %} + {% set end_seconds = completion_window_end_seconds | int(86399) %} + {% set base_end = (current_due_day_start_ts | float(0)) + end_seconds %} + {% if end_seconds < start_seconds %} + {{ base_end + 86400 }} + {% else %} + {{ base_end }} + {% endif %} + {% else %} + 0 + {% endif %} + + #:####################################################################################:# + # Restore retained completion timestamp # + #:####################################################################################:# + - variables: stored_last_completed_ts: >- {% set v = state_attr(state_entity, 'last_completed_ts') %} @@ -141,20 +349,52 @@ script: last_completed_ts: >- {{ now_ts if mark_complete_bool else stored_last_completed_ts }} + #:####################################################################################:# + # Completion and timing state # + #:####################################################################################:# + - variables: complete_bool: >- {{ (last_completed_ts | float(0)) >= (current_period_start_ts | float(0)) }} + previous_completed_bool: >- + {{ + (last_completed_ts | float(0)) >= (previous_period_start_ts | float(0)) + and (last_completed_ts | float(0)) < (current_period_start_ts | float(0)) + }} + + previous_missed_bool: >- + {{ not (previous_completed_bool | bool) }} + due_started_bool: >- - {% if repeat_type_safe == 'daily' %} - {{ (now_ts | float(0)) >= (current_due_ts | float(0)) }} - {% else %} - true - {% endif %} + {{ (now_ts | float(0)) >= (current_due_ts | float(0)) }} overdue_started_bool: >- {{ (now_ts | float(0)) >= ((current_due_ts | float(0)) + ((overdue_after_minutes | int(0)) * 60)) }} + previous_missed_active_bool: >- + {{ + previous_missed_bool | bool + and not (complete_bool | bool) + and not (due_started_bool | bool) + }} + + completion_window_missed_bool: >- + {{ + has_completion_window | bool + and not (complete_bool | bool) + and (now_ts | float(0)) > (current_completion_window_end_ts | float(0)) + }} + + missed_bool: >- + {{ + completion_window_missed_bool | bool + or ( + previous_missed_active_bool | bool + and not (has_completion_window | bool) + ) + }} + minutes_since_required: >- {% if (now_ts | float(0)) <= (current_due_ts | float(0)) %} 0 @@ -169,10 +409,16 @@ script: {{ (((now_ts | float(0)) - (current_due_ts | float(0))) / 3600) | round(0) | int }} {% endif %} + #:####################################################################################:# + # Display state # + #:####################################################################################:# + - variables: job_status: >- {% if complete_bool | bool %} Complete + {% elif missed_bool | bool %} + Missed {% elif not (due_started_bool | bool) %} Not Due {% elif overdue_started_bool | bool %} @@ -265,52 +511,64 @@ script: {{ ts | timestamp_custom('%Y-%m-%d @ %H:%M', true) }} {% endif %} + #:####################################################################################:# + # Publish retained MQTT JSON # + #:####################################################################################:# + + - variables: + completed_for_period_ts_safe: >- + {% if complete_bool | bool %} + {{ current_period_start_ts | float(0) }} + {% else %} + 0 + {% endif %} + - action: mqtt.publish data: topic: "{{ topic_safe }}" retain: true qos: 1 payload: >- - {{ - { - "job_id": job_id, - "job_name": job_name, - "repeat": repeat_type_safe, - "due_time": due_time_safe, - "due_weekday": due_weekday_safe | int, - "due_day": due_day_safe | int, - - "state": job_status | trim, - "complete": complete_bool | bool, - - "last_completed_ts": last_completed_ts | float(0), - "last_completed_time": last_completed_time, - "last_completed_human": last_completed_human, - - "completed_for_period_ts": current_period_start_ts | float(0) if complete_bool | bool else 0, - "completed_for_due_ts": current_period_start_ts | float(0) if complete_bool | bool else 0, - - "current_period_start_ts": current_period_start_ts | float(0), - "next_period_start_ts": next_period_start_ts | float(0), - - "current_due_ts": current_due_ts | float(0), - "next_due_ts": next_due_ts | float(0), - "display_due_ts": display_due_ts | float(0), - "next_due_human": next_due_human, - - "due_started": due_started_bool | bool, - "overdue_started": overdue_started_bool | bool, - - "minutes_since_required": minutes_since_required | int(0), - "hours_since_required": hours_since_required | int(0), - - "overdue_after_minutes": overdue_after_minutes | int(0), - "overdue_minutes": overdue_minutes | int(0), - "overdue_hours": overdue_hours | int(0), - - "hours_since_completed": hours_since_completed, - - "last_updated_local": (now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true), - "mqtt_topic": topic_safe - } | tojson - }} + { + "job_id": {{ job_id | tojson }}, + "job_name": {{ job_name | tojson }}, + "repeat": {{ repeat_type_safe | tojson }}, + "due_time": {{ due_time_safe | tojson }}, + "due_weekday": {{ due_weekday_safe | int }}, + "due_day": {{ due_day_safe | int }}, + "state": {{ job_status | trim | tojson }}, + "complete": {{ complete_bool | bool | tojson }}, + "last_completed_ts": {{ last_completed_ts | float(0) }}, + "last_completed_time": {{ last_completed_time | tojson }}, + "last_completed_human": {{ last_completed_human | tojson }}, + "previous_period_start_ts": {{ previous_period_start_ts | float(0) }}, + "previous_due_ts": {{ previous_due_ts | float(0) }}, + "previous_completed": {{ previous_completed_bool | bool | tojson }}, + "previous_missed": {{ previous_missed_bool | bool | tojson }}, + "previous_missed_active": {{ previous_missed_active_bool | bool | tojson }}, + "missed": {{ missed_bool | bool | tojson }}, + "completion_window_start": {{ completion_window_start_safe | tojson }}, + "completion_window_end": {{ completion_window_end_safe | tojson }}, + "has_completion_window": {{ has_completion_window | bool | tojson }}, + "completion_window_missed": {{ completion_window_missed_bool | bool | tojson }}, + "current_completion_window_start_ts": {{ current_completion_window_start_ts | float(0) }}, + "current_completion_window_end_ts": {{ current_completion_window_end_ts | float(0) }}, + "completed_for_period_ts": {{ completed_for_period_ts_safe | float(0) }}, + "completed_for_due_ts": {{ completed_for_period_ts_safe | float(0) }}, + "current_period_start_ts": {{ current_period_start_ts | float(0) }}, + "next_period_start_ts": {{ next_period_start_ts | float(0) }}, + "current_due_ts": {{ current_due_ts | float(0) }}, + "next_due_ts": {{ next_due_ts | float(0) }}, + "display_due_ts": {{ display_due_ts | float(0) }}, + "next_due_human": {{ next_due_human | tojson }}, + "due_started": {{ due_started_bool | bool | tojson }}, + "overdue_started": {{ overdue_started_bool | bool | tojson }}, + "minutes_since_required": {{ minutes_since_required | int(0) }}, + "hours_since_required": {{ hours_since_required | int(0) }}, + "overdue_after_minutes": {{ overdue_after_minutes | int(0) }}, + "overdue_minutes": {{ overdue_minutes | int(0) }}, + "overdue_hours": {{ overdue_hours | int(0) }}, + "hours_since_completed": {{ hours_since_completed | tojson }}, + "last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }}, + "mqtt_topic": {{ topic_safe | tojson }} + } diff --git a/packages/appliance_status_mqttfeed.yaml b/packages/appliance_status_mqttfeed.yaml index 9b82326..00a0eb4 100644 --- a/packages/appliance_status_mqttfeed.yaml +++ b/packages/appliance_status_mqttfeed.yaml @@ -13,6 +13,10 @@ mqtt: name: "Dishwasher Finished" state_topic: "viewroad-status/activityfeed/Dishwasher_complete" icon: mdi:dishwasher + - unique_id: downstairs_dishwasher_finished + name: "Downstairs Dishwasher Finished" + state_topic: "viewroad-status/activityfeed/Downstairs_dishwasher_complete" + icon: mdi:dishwasher - unique_id: 16Acharger_finished name: "Leaf Done" state_topic: "viewroad-status/activityfeed/Leaf_Charger_complete" diff --git a/packages/bedtime_mode_actions.yaml b/packages/bedtime_mode_actions.yaml index 5c2ea76..fb35d1e 100644 --- a/packages/bedtime_mode_actions.yaml +++ b/packages/bedtime_mode_actions.yaml @@ -67,6 +67,12 @@ automation: variables: bedtime_delayed_off_targets: + - entity_id: switch.esp_centralstairs_bottom_relay_1_main_stair_lights_lower + delay_seconds: 0 + + - entity_id: switch.esp_centralstairs_bottom_relay_2_stair_footer_lights + delay_seconds: 0 + - entity_id: switch.esp_mainkitchenlights_relay_1_main_lights delay_seconds: 4 diff --git a/packages/open_meteo_rain_chance.yaml b/packages/open_meteo_rain_chance.yaml new file mode 100644 index 0000000..ff5a44c --- /dev/null +++ b/packages/open_meteo_rain_chance.yaml @@ -0,0 +1,277 @@ +#:########################################################################################:# +# Open-Meteo Rain Chance Package # +#:########################################################################################:# +# +# TITLE: +# Open-Meteo Rain Chance +# +# FILE: +# open_meteo_rain_chance.yaml +# +# VERSION: +# V1.2 2026-07-19 +# - Changed today's forecast period to use remaining daylight hours. +# - Before sunrise, today's forecast covers sunrise through sunset. +# - During daylight, today's forecast covers now through sunset. +# - At sunset or later, today's forecast covers now through midnight. +# - Tomorrow continues to cover daylight hours only. +# +# V1.1 2026-07-19 +# - Changed today's prediction to use only the remaining forecast hours +# from the current local hour until midnight. +# - Changed tomorrow's prediction to cover daylight hours only. +# - Added hourly precipitation probability data. +# - Added today's and tomorrow's sunrise and sunset data. +# +# V1.0 2026-07-18 +# - Initial version. +# - Used the full-day maximum precipitation probability for today and +# tomorrow. +# +# PURPOSE: +# Creates dedicated Home Assistant sensors for the maximum hourly +# probability of precipitation during useful forecast periods today and +# tomorrow. +# +# SOURCE: +# Open-Meteo Forecast API +# +# LOCATION: +# Uses the latitude and longitude attributes of zone.home. +# +# OUTPUT ENTITIES: +# sensor.open_meteo_rain_chance_today +# sensor.open_meteo_rain_chance_tomorrow +# +# FORECAST PERIODS: +# Today before sunrise: +# Sunrise through sunset. +# +# Today during daylight: +# Current time through sunset. +# +# Today at or after sunset: +# Current time through midnight. +# +# Tomorrow: +# Sunrise through sunset. +# +# UPDATE RATE: +# Every 30 minutes. +# +# NOTES: +# - No API key is required. +# - timezone=auto makes Open-Meteo return forecast times in the local +# timezone determined from the zone.home coordinates. +# - Each Open-Meteo hourly precipitation probability applies to the +# preceding hour. +# - Hourly forecast periods that partly overlap sunrise or sunset are +# included. +# - The displayed result is the highest hourly probability within the +# applicable period. +# - The result is not a separately calculated probability of receiving +# rain at any point across the whole selected period. +# - Sensor values and forecast-period changes are recalculated whenever +# the REST request updates. +# +#:########################################################################################:# + +rest: + - resource_template: >- + https://api.open-meteo.com/v1/forecast?latitude={{ state_attr('zone.home', 'latitude') }}&longitude={{ state_attr('zone.home', 'longitude') }}&hourly=precipitation_probability&daily=sunrise,sunset&timezone=auto&forecast_days=2 + + method: GET + scan_interval: 1800 + timeout: 20 + + sensor: + #:###################################################################################:# + # Rain Chance Today # + #:###################################################################################:# + # + # Before sunrise: + # Uses today's sunrise through sunset. + # + # During daylight: + # Uses the current time through today's sunset. + # + # At or after sunset: + # Uses the current time through midnight. + # + + - name: "Open Meteo Rain Chance Today" + unique_id: open_meteo_rain_chance_today + unit_of_measurement: "%" + state_class: measurement + icon: mdi:weather-rainy + + value_template: >- + {% set current_timestamp = + as_timestamp(now()) + %} + + {% set sunrise_timestamp = + as_timestamp( + value_json.daily.sunrise[0], + 0 + ) + %} + + {% set sunset_timestamp = + as_timestamp( + value_json.daily.sunset[0], + 0 + ) + %} + + {% set tomorrow_midnight_timestamp = + as_timestamp( + value_json.daily.time[1] ~ 'T00:00', + 0 + ) + %} + + {% if current_timestamp < sunrise_timestamp %} + + {% set period_start_timestamp = + sunrise_timestamp + %} + + {% set period_end_timestamp = + sunset_timestamp + %} + + {% elif current_timestamp < sunset_timestamp %} + + {% set period_start_timestamp = + current_timestamp + %} + + {% set period_end_timestamp = + sunset_timestamp + %} + + {% else %} + + {% set period_start_timestamp = + current_timestamp + %} + + {% set period_end_timestamp = + tomorrow_midnight_timestamp + %} + + {% endif %} + + {% set ns = namespace( + maximum=0, + found=false + ) %} + + {% for index in range( + value_json.hourly.time | count + ) %} + + {% set forecast_end_timestamp = + as_timestamp( + value_json.hourly.time[index], + 0 + ) + %} + + {% set probability = + value_json.hourly.precipitation_probability[index] + %} + + {% if + forecast_end_timestamp > period_start_timestamp + and forecast_end_timestamp < period_end_timestamp + 3600 + and probability is not none + %} + + {% set ns.found = true %} + + {% if probability | int(0) > ns.maximum %} + + {% set ns.maximum = + probability | int(0) + %} + + {% endif %} + + {% endif %} + + {% endfor %} + + {{ ns.maximum if ns.found else 0 }} + + #:###################################################################################:# + # Rain Chance Tomorrow — Daylight # + #:###################################################################################:# + # + # Uses hourly forecast periods that overlap tomorrow's daylight + # period between sunrise and sunset. + # + + - name: "Open Meteo Rain Chance Tomorrow" + unique_id: open_meteo_rain_chance_tomorrow + unit_of_measurement: "%" + state_class: measurement + icon: mdi:weather-sunset-up + + value_template: >- + {% set sunrise_timestamp = + as_timestamp( + value_json.daily.sunrise[1], + 0 + ) + %} + + {% set sunset_timestamp = + as_timestamp( + value_json.daily.sunset[1], + 0 + ) + %} + + {% set ns = namespace( + maximum=0, + found=false + ) %} + + {% for index in range( + value_json.hourly.time | count + ) %} + + {% set forecast_end_timestamp = + as_timestamp( + value_json.hourly.time[index], + 0 + ) + %} + + {% set probability = + value_json.hourly.precipitation_probability[index] + %} + + {% if + forecast_end_timestamp > sunrise_timestamp + and forecast_end_timestamp < sunset_timestamp + 3600 + and probability is not none + %} + + {% set ns.found = true %} + + {% if probability | int(0) > ns.maximum %} + + {% set ns.maximum = + probability | int(0) + %} + + {% endif %} + + {% endif %} + + {% endfor %} + + {{ ns.maximum if ns.found else 0 }} diff --git a/packages/view_road_announcement_router.yaml b/packages/view_road_announcement_router.yaml index eaa1390..0e0797a 100644 --- a/packages/view_road_announcement_router.yaml +++ b/packages/view_road_announcement_router.yaml @@ -1,6 +1,11 @@ ################################################################################ # PACKAGE: View Road Announcement Router ################################################################################ +# 1.8: Date: 2026-07-11 +# - Added MQTT View Rd Feed announcement channel. +# - Added mqtt_feed script field. +# - MQTT feed publishes the current local date/time to a topic built from +# mqtt_view_rd_feed_topic_template by replacing TOPIC with mqtt_feed. # 1.7: # - Updated Lounge Google Home Voice to speak short_announcement only. # - This allows long_announcement to remain available for detailed channels @@ -14,7 +19,7 @@ # Central announcement router for general Home Assistant announcements. # Other scripts and automations can call script.view_road_announcement and pass # values such as channels, title, short_announcement, long_announcement, -# payload, and indicator. +# payload, indicator, and mqtt_feed. # # How to call this script: # @@ -62,7 +67,13 @@ # Blocked by default when input_boolean.quiet_time is on. # # mqtt_view_rd_feed: -# Placeholder only. +# Uses: +# mqtt_feed +# Publishes the current local date/time to MQTT. +# Topic format: +# viewroad-status/activityfeed/ +# Payload format: +# YYYY-MM-DD HH:MM:SS # # led_matrix_1: # Uses: @@ -285,6 +296,13 @@ script: selector: text: + mqtt_feed: + name: MQTT Feed Topic + description: Activity feed topic suffix. Replaces TOPIC in mqtt_view_rd_feed_topic_template. + example: "washing_machine" + selector: + text: + sequence: ########################################################################## # PACKAGE SETTINGS / SUBSTITUTIONS @@ -305,6 +323,7 @@ script: default_long_announcement: "Home automation announcement" default_payload: "" default_indicator: "" + default_mqtt_feed: "" quiet_time_blocked_channels: - "google_home_voice" @@ -322,6 +341,11 @@ script: led_matrix_mqtt_qos: 0 led_matrix_mqtt_retain: false + mqtt_view_rd_feed_topic_template: "viewroad-status/activityfeed/TOPIC" + mqtt_view_rd_feed_datetime_format: "%Y-%m-%d %H:%M:%S" + mqtt_view_rd_feed_mqtt_qos: 0 + mqtt_view_rd_feed_mqtt_retain: true + ########################################################################## # RESOLVE PASSED VALUES ########################################################################## @@ -333,6 +357,7 @@ script: # long_announcement # payload # indicator + # mqtt_feed # # Some uppercase variants are also accepted for tolerance. # @@ -395,6 +420,17 @@ script: {{ default_indicator }} {% endif %} + ann_mqtt_feed: >- + {% if mqtt_feed is defined %} + {{ mqtt_feed }} + {% elif MQTT_Feed is defined %} + {{ MQTT_Feed }} + {% elif Mqtt_Feed is defined %} + {{ Mqtt_Feed }} + {% else %} + {{ default_mqtt_feed }} + {% endif %} + ########################################################################## # NORMALISE CHANNEL LISTS AND MESSAGE TEXT ########################################################################## @@ -406,6 +442,15 @@ script: ann_quiet_time_blocked_channels_normalised: >- {{ ',' ~ (quiet_time_blocked_channels | join(',') | lower | replace(' ', '') | replace('\n', '') | trim(',')) ~ ',' }} + ann_mqtt_feed_topic_suffix: >- + {{ ann_mqtt_feed | string | trim }} + + ann_mqtt_feed_topic: >- + {{ mqtt_view_rd_feed_topic_template | replace('TOPIC', ann_mqtt_feed_topic_suffix) }} + + ann_mqtt_feed_datetime: >- + {{ now().strftime(mqtt_view_rd_feed_datetime_format) }} + ann_short_announcement_raw: >- {% if short_announcement is defined %} {{ short_announcement }} @@ -575,10 +620,43 @@ script: # CHANNEL: MQTT VIEW RD FEED ########################################################################## # - # Placeholder only. No action yet. + # Channel name: + # mqtt_view_rd_feed + # + # Uses: + # mqtt_feed + # + # Publishes the current Home Assistant local time in AM/PM format. + # + # Topic format: + # viewroad-status/activityfeed/ + # + # Example: + # mqtt_feed: "dogfed" + # + # Publishes: + # Topic: viewroad-status/activityfeed/dogfed + # Payload: 12:32 PM # ########################################################################## + - if: + - condition: template + value_template: "{{ ',mqtt_view_rd_feed,' in ann_channels_normalised }}" + - condition: state + entity_id: input_boolean.announcement_channel_mqtt_view_rd_feed + state: "on" + - condition: template + value_template: "{{ ann_mqtt_feed_topic_suffix | string | trim | length > 0 }}" + then: + - action: mqtt.publish + data: + topic: "{{ ann_mqtt_feed_topic }}" + #payload: "{{ now().strftime('%I:%M %p') | regex_replace('^0', '') }}" + payload: "{{ now().strftime('%H:%M') }}" + qos: "{{ mqtt_view_rd_feed_mqtt_qos | int(0) }}" + retain: "{{ mqtt_view_rd_feed_mqtt_retain | bool(true) }}" + ########################################################################## # CHANNEL: LOUNGE LED MATRIX DISPLAY - MATRIX 1 ##########################################################################