various packages

This commit is contained in:
zorruno
2026-07-20 19:24:35 +12:00
parent afcbf1a94b
commit d8bcf05bf6
15 changed files with 4743 additions and 5310 deletions
+1 -1
View File
@@ -1 +1 @@
{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783321380.267702}
{"pid": 71, "version": 1, "ha_version": "2026.7.2", "start_ts": 1784512870.0398877}
+43
View File
@@ -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"
+807
View File
@@ -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 `<integration>/llm.py` and registered LLM APIs. New Home Assistant builds may also expose those APIs over native MCP endpoints such as `/api/mcp/<API ID>`; 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 `<integration>/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 <issue_id>`
- **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 <command> --help` for full usage details.
<!-- HAB_LIVE_HELP_START -->
```
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.
```
<!-- HAB_LIVE_HELP_END -->
**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`.
<!-- ZIGPORTER_LIVE_HELP_START -->
```
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. │
╰──────────────────────────────────────────────────────────────────────────────╯
```
<!-- ZIGPORTER_LIVE_HELP_END -->
**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 `<integration>/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
+2103 -543
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-916
View File
@@ -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>(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);
}
File diff suppressed because it is too large Load Diff
@@ -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 }}"
@@ -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"
am_default_announcement_channels: "pushover_zorruno"
#:####################################################################################:#
# 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
# Optional appliance-specific overrides available:
am_default_announcement_enabled: true
am_default_announcement_channels: "pushover_zorruno"
am_default_announcement_payload: ""
#:####################################################################################:#
# OPTIONAL PER-APPLIANCE OVERRIDES #
#:####################################################################################:#
#
# debug_flow: true
# Writes state-processing details to the Home Assistant logbook.
#
# resolution: 4
# The number of samples for the average, with one sample per minute.
# Number of one-minute samples used for the rolling average.
#
# standby_power: 3
# Below this value, the appliance will show "Off".
# Between this value and operating_power_min, it will show "Standby".
# Below this value, the appliance shows Off.
# Between standby_power and operating_power_min, it shows Standby.
#
# operating_power_min: 20
# The minimum average power needed for the appliance to show "Operating".
# Minimum rolling-average power required for Operating.
#
# min_cycle_notify_seconds: 600
# Finished cycles shorter than this are recorded, but no notification
# is sent. Default is 300 seconds.
# Minimum cycle duration before a completion announcement is allowed.
#
# announcement_channels: "pushover_zorruno"
# Announcement channels for this appliance.
# announcement_enabled: false
# Disables cycle-completion announcements for this appliance.
#
# long_announcement: "The device has finished. Please empty it when convenient."
# Override the long announcement.
# 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.
#
# min_cycle_notify_seconds: 600
# Minimum time for an appliance to be operating before a completion announcement generated.
# 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 }}"
File diff suppressed because it is too large Load Diff
@@ -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 %}
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 }}
}
+4
View File
@@ -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"
+6
View File
@@ -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
+277
View File
@@ -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 }}
+81 -3
View File
@@ -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/<mqtt_feed>
# 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/<mqtt_feed>
#
# 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
##########################################################################