Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8bcf05bf6 | |||
| afcbf1a94b | |||
| 027af09e40 | |||
| 90a6469161 | |||
| bffcda38d9 | |||
| cb95ceacea | |||
| 941334ecfd | |||
| a8b0990000 | |||
| 809f9ada66 | |||
| 14d65a8cf1 | |||
| 9afee6e638 | |||
| fd2fe422d0 | |||
| f07cd6d0c4 | |||
| ea604b8a30 | |||
| 241a4954bd | |||
| b2876e937a | |||
| 0dd442f34a | |||
| 67a8ed7436 | |||
| f92ffa3eab | |||
| b893249959 | |||
| c658792f5d | |||
| ca60f73b9d | |||
| 5747968e96 | |||
| 3938515564 | |||
| 526fd23567 | |||
| c8e9992c32 | |||
| 5e731f791e | |||
| 4bc8c308a6 | |||
| dcf28a3c8f | |||
| 9bcc9b9b4d | |||
| a95036e0c5 | |||
| 347e20f552 | |||
| ec195d33ce | |||
| d0df25acf3 | |||
| 1dbe748e19 | |||
| 3dcaa1b06b | |||
| d324ec568b | |||
| 97669cb664 | |||
| a81f56a261 | |||
| 3cc5a757bc | |||
| 0794008667 | |||
| cafc856a3c | |||
| 38281df1dd | |||
| c197172608 | |||
| 034c87317f | |||
| 824fd3fac6 | |||
| 41836a3dee | |||
| 45f1f0540a | |||
| c374e32ded | |||
| 3dc72898c9 | |||
| 324cefef4c | |||
| 07048708a9 | |||
| 355b3166f7 | |||
| 8b31b780b0 | |||
| ee08838c1f | |||
| aa4b632916 | |||
| 5be9125eec | |||
| 5f540de6af | |||
| 95835550de | |||
| 20a40b2b3f | |||
| cf857f322f | |||
| 497cf553ad | |||
| caa4d906c4 | |||
| b8681f7741 | |||
| 5403d27f10 | |||
| 54f4da6c18 | |||
| 19ba5a0c8c | |||
| 6ae711d922 | |||
| e90b6b2b8c | |||
| 24bb66893a | |||
| 905a16236b | |||
| cc720cfd00 | |||
| 34a84d0d0c | |||
| 5855dbb5a9 | |||
| 2a8c9584d7 | |||
| bb97b2ac12 | |||
| 0ca6fefd08 | |||
| ea7eedff93 | |||
| 79054490d0 | |||
| d27ba8bd14 | |||
| 63209b5364 | |||
| 1b9552e328 | |||
| ae566427c2 | |||
| f63ff4bfe2 | |||
| 7bd3e3a0c2 | |||
| 06a482b43f | |||
| e013bab5c9 | |||
| 4fab9755ec | |||
| 769cb34eb6 | |||
| ce7a456d1f | |||
| e586ff8f28 | |||
| f571919352 | |||
| 2e9a37a9b9 | |||
| d45474036c | |||
| 5c5256caa9 | |||
| 2a5725b16d | |||
| 706844e6e8 | |||
| 2b2b8edc37 | |||
| c11728dba6 | |||
| 0377bf0b08 | |||
| 893ed9dcfc | |||
| 1d00da6514 | |||
| 1aa5985c2c | |||
| 97efa38777 | |||
| cd39558b1f | |||
| 5b7a9d39d2 |
+1
-1
@@ -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}
|
||||||
@@ -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"
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+256
-69
@@ -6,79 +6,119 @@
|
|||||||
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-3dprinterpow.yaml
|
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-3dprinterpow.yaml
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# VERSIONS:
|
# VERSIONS:
|
||||||
# V1.1 2026-02-25 Updated yaml to zorruno layout V1.1
|
# V1.3 2026-07-12
|
||||||
# V1.0 2025-06-29 Initial Version
|
# - Added cumulative Load Energy integration sensor for Home Assistant.
|
||||||
|
# - Added seven-day maximum power sensor.
|
||||||
|
# - Maximum power resets after 168 powered hours.
|
||||||
|
# - Added a single-process compile limit to reduce Builder memory use.
|
||||||
|
#
|
||||||
|
# V1.2 2026-07-11
|
||||||
|
# - Removed the optimistic template relay switch.
|
||||||
|
# - Exposed the actual GPIO12 relay directly to Home Assistant.
|
||||||
|
# - Physical button and MQTT commands now control the same relay entity.
|
||||||
|
# - Status LED is now synchronised from the real relay state callbacks.
|
||||||
|
# - Prevents the Home Assistant switch state becoming desynchronised from the relay.
|
||||||
|
#
|
||||||
|
# V1.1 2026-02-25
|
||||||
|
# - Updated YAML to zorruno layout V1.1.
|
||||||
|
#
|
||||||
|
# V1.0 2025-06-29
|
||||||
|
# - Initial version.
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# HARDWARE:
|
# HARDWARE:
|
||||||
# Sonoff POW R1 (ESP8266)
|
# Sonoff POW R1 (ESP8266)
|
||||||
# https://devices.esphome.io/devices/Sonoff-POW-R1
|
# https://devices.esphome.io/devices/Sonoff-POW-R1
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# OPERATION NOTES:
|
# OPERATION NOTES:
|
||||||
# - Inline power monitor (HLW8012) plus relay control for the 3D printer power feed
|
# - Inline power monitor using the HLW8012.
|
||||||
# - Local MQTT control (ON/OFF) without needing Home Assistant
|
# - Reports current, voltage, instantaneous power, and cumulative energy.
|
||||||
# - Publishes current relay state on boot (burst) and on relay changes
|
# - Load Energy integrates measured power over time and reports kWh.
|
||||||
# - "Running" binary sensor is derived from measured power > 4 W (with delayed_off)
|
# - Load Energy resets to zero if the ESPHome device reboots.
|
||||||
|
# - Home Assistant handles the reset because the sensor uses total_increasing.
|
||||||
|
# - Load Maximum Power 7 Days records the highest measured power.
|
||||||
|
# - Maximum power resets after 168 powered hours.
|
||||||
|
# - Maximum power and the elapsed-hour counter are retained across reboots.
|
||||||
|
# - The maximum value is checked for saving every five minutes.
|
||||||
|
# - The elapsed-hour counter is checked for saving every six hours.
|
||||||
|
# - An unexpected power loss may extend the peak reset period by up to about six hours.
|
||||||
|
# - GPIO12 directly controls the 3D printer power relay.
|
||||||
|
# - The physical button toggles the real relay.
|
||||||
|
# - Home Assistant directly controls and reports the real relay state.
|
||||||
|
# - Local MQTT ON/OFF control works without needing Home Assistant.
|
||||||
|
# - The status LED follows the actual relay state.
|
||||||
|
# - Publishes current relay state on boot and whenever the relay changes.
|
||||||
|
# - "Running" binary sensor is derived from measured power greater than 4 W.
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# MQTT COMMANDS:
|
# MQTT COMMANDS:
|
||||||
# - Command topic (local):
|
# - Command topic:
|
||||||
# ${mqtt_local_command_topic}
|
# ${mqtt_local_command_topic}
|
||||||
# Payload "ON" -> relay ON + status LED ON
|
|
||||||
# Payload "OFF" -> relay OFF + status LED OFF
|
|
||||||
#
|
#
|
||||||
# - Status topic (local):
|
# Payload "ON" -> relay ON
|
||||||
|
# Payload "OFF" -> relay OFF
|
||||||
|
#
|
||||||
|
# - Status topic:
|
||||||
# ${mqtt_local_status_topic}
|
# ${mqtt_local_status_topic}
|
||||||
# Payload "ON"/"OFF" published on relay state changes, plus a short burst on boot
|
#
|
||||||
|
# Payload "ON"/"OFF" is published:
|
||||||
|
# - whenever the actual relay state changes;
|
||||||
|
# - as a short status burst following boot.
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# OFFLINE NOTES:
|
# OFFLINE NOTES:
|
||||||
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
|
# a) Home Assistant OFFLINE, but Network and MQTT ONLINE
|
||||||
# - Device remains controllable via MQTT local command topic
|
# - Device remains controllable through the MQTT local command topic.
|
||||||
# - Status publishes to MQTT still occur (relay state burst + changes)
|
# - Relay status changes continue to publish through MQTT.
|
||||||
# - HA entities are unavailable until HA returns
|
# - Energy integration and maximum power monitoring continue locally.
|
||||||
# b) MQTT OFFLINE (but WiFi/Network and HA API ONLINE)
|
# - Home Assistant entities remain unavailable until HA returns.
|
||||||
# - Device remains controllable from Home Assistant via API (if enabled)
|
#
|
||||||
# - Local MQTT commands will not work and status publishes will fail while MQTT is down
|
# b) MQTT OFFLINE, but WiFi/Network and HA API ONLINE
|
||||||
|
# - Device remains controllable from Home Assistant through the native API.
|
||||||
|
# - Local MQTT commands do not work while MQTT is unavailable.
|
||||||
|
# - Energy integration and maximum power monitoring continue locally.
|
||||||
|
# - MQTT status publishing resumes when MQTT reconnects.
|
||||||
|
#
|
||||||
# c) Entire WiFi/Network OFFLINE
|
# c) Entire WiFi/Network OFFLINE
|
||||||
# - No HA API and no MQTT control available
|
# - No Home Assistant API or MQTT control is available.
|
||||||
# - Local button still toggles the relay (and status LED follows)
|
# - The local physical button continues to toggle the relay.
|
||||||
# - Accurate time is NOT needed (SNTP not needed)
|
# - The status LED continues to follow the real relay state.
|
||||||
|
# - Energy integration and maximum power monitoring continue locally.
|
||||||
|
# - Accurate time is not needed for the seven-day powered-time reset.
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# SUBSTITUTIONS: Specific device variable substitutions
|
# SUBSTITUTIONS: Specific device variable substitutions
|
||||||
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
|
# If not using a secrets file, replace these with the required values in quotes.
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
substitutions:
|
substitutions:
|
||||||
# Device Naming
|
# Device Naming
|
||||||
device_name: "esp-3dprinterpow"
|
device_name: "esp-3dprinterpow"
|
||||||
friendly_name: "3D Printer Power"
|
friendly_name: "3D Printer Power"
|
||||||
description_comment: "3D Printer inline monitor and switch override using a Sonoff Pow R1. (Layout V1.1)"
|
description_comment: "3D Printer inline monitor and switch override using a Sonoff POW R1. (Layout V1.1)"
|
||||||
device_area: "Underhouse" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
|
device_area: "Underhouse"
|
||||||
|
|
||||||
# Project Naming
|
# Project Naming
|
||||||
project_name: "Sonoff Technologies.POW R1" # Project Details
|
project_name: "Sonoff Technologies.POW R1"
|
||||||
project_version: "v1.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version
|
project_version: "v1.3"
|
||||||
|
|
||||||
# Passwords & Secrets
|
# Passwords and Secrets
|
||||||
api_key: !secret esp-api_key
|
api_key: !secret esp-api_key
|
||||||
ota_pass: !secret esp-ota_pass
|
ota_pass: !secret esp-ota_pass
|
||||||
static_ip_address: !secret esp-3dprinterpow_ip # unfortunately you can't use substitutions inside secrets names
|
static_ip_address: !secret esp-3dprinterpow_ip
|
||||||
mqtt_command_main_topic: !secret mqtt_command_main_topic
|
mqtt_command_main_topic: !secret mqtt_command_main_topic
|
||||||
mqtt_status_main_topic: !secret mqtt_status_main_topic
|
mqtt_status_main_topic: !secret mqtt_status_main_topic
|
||||||
|
|
||||||
# If we are changing IP addresses, you must update the current IP address here, otherwise it remains
|
# If changing IP addresses, update the current IP address here.
|
||||||
# Don't forget to switch it back when changed.
|
# Switch it back once the address change is complete.
|
||||||
current_ip_address: ${static_ip_address}
|
current_ip_address: ${static_ip_address}
|
||||||
|
|
||||||
# MQTT LOCAL Controls
|
# MQTT Local Controls
|
||||||
mqtt_local_device_name: "3dprinter-power"
|
mqtt_local_device_name: "3dprinter-power"
|
||||||
mqtt_local_command_topic: "${mqtt_command_main_topic}/${mqtt_local_device_name}" # Topic we will use to command this locally without HA
|
mqtt_local_command_topic: "${mqtt_command_main_topic}/${mqtt_local_device_name}"
|
||||||
mqtt_local_status_topic: "${mqtt_status_main_topic}/${mqtt_local_device_name}" # Topic we will use to view status locally without HA
|
mqtt_local_status_topic: "${mqtt_status_main_topic}/${mqtt_local_device_name}"
|
||||||
|
|
||||||
# Device Settings
|
# Device Settings
|
||||||
log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG (default), VERBOSE, VERY_VERBOSE
|
log_level: "INFO"
|
||||||
update_interval: "60s" # update time for for general sensors etc
|
update_interval: "60s"
|
||||||
entity_prefix: "Load" # Simple device name where we want to prefix a sensor or switch, eg "Load" Current.
|
entity_prefix: "Load"
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# PACKAGES: Included Common Packages
|
# PACKAGES: Included Common Packages
|
||||||
@@ -126,14 +166,29 @@ packages:
|
|||||||
esphome:
|
esphome:
|
||||||
name: "${device_name}"
|
name: "${device_name}"
|
||||||
friendly_name: "${friendly_name}"
|
friendly_name: "${friendly_name}"
|
||||||
comment: "${description_comment}" # Appears on the esphome page in HA
|
comment: "${description_comment}"
|
||||||
area: "${device_area}"
|
area: "${device_area}"
|
||||||
|
|
||||||
|
# Restrict PlatformIO to one simultaneous compiler process to reduce host RAM use.
|
||||||
|
compile_process_limit: 1
|
||||||
|
|
||||||
project:
|
project:
|
||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: -10
|
priority: -10
|
||||||
then:
|
then:
|
||||||
|
# Ensure the status LED reflects the restored physical relay state.
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
switch.is_on: relay
|
||||||
|
then:
|
||||||
|
- light.turn_on: led
|
||||||
|
else:
|
||||||
|
- light.turn_off: led
|
||||||
|
|
||||||
|
# Publish a short MQTT status burst after startup.
|
||||||
- script.execute: publish_state_burst
|
- script.execute: publish_state_burst
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
@@ -142,108 +197,235 @@ esphome:
|
|||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
esp8266:
|
esp8266:
|
||||||
board: esp01_1m
|
board: esp01_1m
|
||||||
early_pin_init: false # Recommended false where switches are involved. Defaults to true.
|
|
||||||
board_flash_mode: dout # Default is dout
|
# Recommended false where switches are involved.
|
||||||
|
early_pin_init: false
|
||||||
|
|
||||||
|
# Default is DOUT for this hardware.
|
||||||
|
board_flash_mode: dout
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# LOGGING: ESPHome Logging Enable
|
# LOGGING: ESPHome Logging Enable
|
||||||
# https://esphome.io/components/logger.html
|
# https://esphome.io/components/logger.html
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
logger:
|
logger:
|
||||||
level: "${log_level}" # INFO Level suggested, or DEBUG for testing
|
level: "${log_level}"
|
||||||
#baud_rate: 0 # set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM)
|
|
||||||
|
# Set to 0 if the UART is required for another component.
|
||||||
|
#baud_rate: 0
|
||||||
|
|
||||||
#esp8266_store_log_strings_in_flash: false
|
#esp8266_store_log_strings_in_flash: false
|
||||||
#tx_buffer_size: 64
|
#tx_buffer_size: 64
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# MQTT: Device-specific MQTT command triggers
|
# MQTT: Device-specific MQTT command triggers
|
||||||
# This adds device-specific MQTT command triggers to the common MQTT configuration.
|
# Adds local ON/OFF relay control to the common MQTT configuration.
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
mqtt:
|
mqtt:
|
||||||
on_message:
|
on_message:
|
||||||
# Switch control from MQTT
|
|
||||||
- topic: "${mqtt_local_command_topic}"
|
- topic: "${mqtt_local_command_topic}"
|
||||||
payload: "ON"
|
payload: "ON"
|
||||||
then:
|
then:
|
||||||
- switch.turn_on: relay
|
- switch.turn_on: relay
|
||||||
- light.turn_on: led
|
|
||||||
- topic: "${mqtt_local_command_topic}"
|
- topic: "${mqtt_local_command_topic}"
|
||||||
payload: "OFF"
|
payload: "OFF"
|
||||||
then:
|
then:
|
||||||
- switch.turn_off: relay
|
- switch.turn_off: relay
|
||||||
- light.turn_off: led
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# GLOBALS:
|
||||||
|
# Retained maximum power and elapsed-hour counter
|
||||||
|
# https://esphome.io/components/globals.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
globals:
|
||||||
|
# Highest measured power during the current seven-day period.
|
||||||
|
# Changed values are checked for saving every five minutes.
|
||||||
|
- id: max_power_7d
|
||||||
|
type: float
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0.0'
|
||||||
|
update_interval: 5min
|
||||||
|
|
||||||
|
# Powered hours elapsed during the current seven-day period.
|
||||||
|
# Changed values are checked for saving every six hours to reduce flash writes.
|
||||||
|
- id: max_power_hours_elapsed
|
||||||
|
type: uint16_t
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
update_interval: 6h
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# BINARY SENSORS:
|
# BINARY SENSORS:
|
||||||
# https://esphome.io/components/binary_sensor/
|
# https://esphome.io/components/binary_sensor/
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
|
# Physical Sonoff button.
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
|
name: "Button"
|
||||||
|
|
||||||
pin:
|
pin:
|
||||||
number: GPIO0
|
number: GPIO0
|
||||||
mode: INPUT_PULLUP
|
mode: INPUT_PULLUP
|
||||||
inverted: true
|
inverted: true
|
||||||
name: "Button"
|
|
||||||
on_press:
|
|
||||||
- switch.toggle: virtual_button
|
|
||||||
|
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- switch.toggle: relay
|
||||||
|
|
||||||
|
# Indicates when the connected 3D printer is consuming more than 4 W.
|
||||||
- platform: template
|
- platform: template
|
||||||
name: "${entity_prefix} Running"
|
name: "${entity_prefix} Running"
|
||||||
|
|
||||||
filters:
|
filters:
|
||||||
- delayed_off: 15s
|
- delayed_off: 15s
|
||||||
|
|
||||||
lambda: |-
|
lambda: |-
|
||||||
if (isnan(id(power).state)) {
|
if (isnan(id(power).state)) {
|
||||||
return {};
|
return {};
|
||||||
} else if (id(power).state > 4) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return id(power).state > 4.0f;
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# SENSORS COMPONENT:
|
# SENSOR COMPONENT:
|
||||||
# https://esphome.io/components/sensor/
|
# https://esphome.io/components/sensor/
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
sensor:
|
sensor:
|
||||||
- platform: hlw8012
|
- platform: hlw8012
|
||||||
sel_pin: 5
|
sel_pin: GPIO5
|
||||||
cf_pin: 14
|
cf_pin: GPIO14
|
||||||
cf1_pin: 13
|
cf1_pin: GPIO13
|
||||||
update_interval: 2s
|
update_interval: 2s
|
||||||
|
|
||||||
current:
|
current:
|
||||||
name: "${entity_prefix} Current"
|
name: "${entity_prefix} Current"
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "A"
|
||||||
|
accuracy_decimals: 2
|
||||||
|
|
||||||
voltage:
|
voltage:
|
||||||
name: "${entity_prefix} Voltage"
|
name: "${entity_prefix} Voltage"
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
power:
|
power:
|
||||||
name: "${entity_prefix} Power"
|
name: "${entity_prefix} Power"
|
||||||
id: power
|
id: power
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
# Update the seven-day maximum whenever a new peak is measured.
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (!isnan(x) && x > id(max_power_7d)) {
|
||||||
|
id(max_power_7d) = x;
|
||||||
|
id(max_power_7d_sensor).publish_state(x);
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"max_power",
|
||||||
|
"New seven-day maximum power: %.1f W",
|
||||||
|
x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Integrates instantaneous watts over time.
|
||||||
|
# Watts integrated over hours produces Wh; the filter converts Wh to kWh.
|
||||||
|
- platform: integration
|
||||||
|
name: "${entity_prefix} Energy"
|
||||||
|
id: energy
|
||||||
|
sensor: power
|
||||||
|
time_unit: h
|
||||||
|
integration_method: trapezoid
|
||||||
|
restore: false
|
||||||
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
accuracy_decimals: 3
|
||||||
|
filters:
|
||||||
|
- multiply: 0.001
|
||||||
|
|
||||||
|
# Highest power recorded during the current 168 powered-hour period.
|
||||||
|
- platform: template
|
||||||
|
name: "${entity_prefix} Maximum Power 7 Days"
|
||||||
|
id: max_power_7d_sensor
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
accuracy_decimals: 1
|
||||||
|
update_interval: 60s
|
||||||
|
lambda: |-
|
||||||
|
return id(max_power_7d);
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# INTERVAL:
|
||||||
|
# Seven-day maximum power reset
|
||||||
|
# https://esphome.io/components/interval.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
interval:
|
||||||
|
# Count one powered hour at a time.
|
||||||
|
# 168 hours equals seven days.
|
||||||
|
- interval: 1h
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(max_power_hours_elapsed) += 1;
|
||||||
|
|
||||||
|
ESP_LOGD(
|
||||||
|
"max_power",
|
||||||
|
"Seven-day maximum power period: %u of 168 powered hours elapsed",
|
||||||
|
id(max_power_hours_elapsed)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (id(max_power_hours_elapsed) >= 168) {
|
||||||
|
id(max_power_hours_elapsed) = 0;
|
||||||
|
id(max_power_7d) = 0.0f;
|
||||||
|
id(max_power_7d_sensor).publish_state(0.0f);
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"max_power",
|
||||||
|
"Seven-day maximum power value has been reset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# SWITCH COMPONENT:
|
# SWITCH COMPONENT:
|
||||||
# https://esphome.io/components/switch/
|
# https://esphome.io/components/switch/
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
# The actual GPIO relay is exposed directly to Home Assistant.
|
||||||
name: "Relay"
|
# This is the single authoritative relay state for:
|
||||||
optimistic: true
|
# - Home Assistant API control;
|
||||||
id: virtual_button
|
# - MQTT control;
|
||||||
turn_on_action:
|
# - physical button control;
|
||||||
- switch.turn_on: relay
|
# - relay state restoration;
|
||||||
- light.turn_on: led
|
# - MQTT status publishing;
|
||||||
turn_off_action:
|
# - status LED synchronisation.
|
||||||
- switch.turn_off: relay
|
|
||||||
- light.turn_off: led
|
|
||||||
|
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
|
name: "Relay"
|
||||||
id: relay
|
id: relay
|
||||||
pin: GPIO12
|
pin: GPIO12
|
||||||
|
|
||||||
restore_mode: RESTORE_DEFAULT_ON
|
restore_mode: RESTORE_DEFAULT_ON
|
||||||
|
|
||||||
on_turn_on:
|
on_turn_on:
|
||||||
|
then:
|
||||||
|
- light.turn_on: led
|
||||||
|
|
||||||
- mqtt.publish:
|
- mqtt.publish:
|
||||||
topic: "${mqtt_local_status_topic}"
|
topic: "${mqtt_local_status_topic}"
|
||||||
payload: "ON"
|
payload: "ON"
|
||||||
retain: false
|
retain: false
|
||||||
|
|
||||||
on_turn_off:
|
on_turn_off:
|
||||||
|
then:
|
||||||
|
- light.turn_off: led
|
||||||
|
|
||||||
- mqtt.publish:
|
- mqtt.publish:
|
||||||
topic: "${mqtt_local_status_topic}"
|
topic: "${mqtt_local_status_topic}"
|
||||||
payload: "OFF"
|
payload: "OFF"
|
||||||
@@ -256,6 +438,7 @@ switch:
|
|||||||
output:
|
output:
|
||||||
- platform: esp8266_pwm
|
- platform: esp8266_pwm
|
||||||
id: pow_blue_led
|
id: pow_blue_led
|
||||||
|
|
||||||
pin:
|
pin:
|
||||||
number: GPIO15
|
number: GPIO15
|
||||||
inverted: true
|
inverted: true
|
||||||
@@ -267,8 +450,8 @@ output:
|
|||||||
light:
|
light:
|
||||||
- platform: monochromatic
|
- platform: monochromatic
|
||||||
name: "Status LED"
|
name: "Status LED"
|
||||||
output: pow_blue_led
|
|
||||||
id: led
|
id: led
|
||||||
|
output: pow_blue_led
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# SCRIPT COMPONENT:
|
# SCRIPT COMPONENT:
|
||||||
@@ -277,13 +460,17 @@ light:
|
|||||||
script:
|
script:
|
||||||
- id: publish_state_burst
|
- id: publish_state_burst
|
||||||
mode: restart
|
mode: restart
|
||||||
|
|
||||||
then:
|
then:
|
||||||
- repeat:
|
- repeat:
|
||||||
count: 5
|
count: 5
|
||||||
|
|
||||||
then:
|
then:
|
||||||
- mqtt.publish:
|
- mqtt.publish:
|
||||||
topic: "${mqtt_local_status_topic}"
|
topic: "${mqtt_local_status_topic}"
|
||||||
payload: !lambda 'return id(relay).state ? "ON" : "OFF";'
|
payload: !lambda |-
|
||||||
|
return id(relay).state ? "ON" : "OFF";
|
||||||
retain: false
|
retain: false
|
||||||
qos: 1
|
qos: 1
|
||||||
|
|
||||||
- delay: 5s
|
- delay: 5s
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
|||||||
|
#:########################################################################################:#
|
||||||
|
# ESP-NOW Test Receiver
|
||||||
|
#:########################################################################################:#
|
||||||
|
#
|
||||||
|
# DEVICE:
|
||||||
|
# ESP32 DevKit V1 / ESP-WROOM-32
|
||||||
|
#
|
||||||
|
# VERSION:
|
||||||
|
# 1.0 - 2026-07-11
|
||||||
|
# - Initial ESP-NOW receiver test build.
|
||||||
|
# - Receives broadcast packets from ESP32-S3-Zero transmitter.
|
||||||
|
# - Publishes last sender MAC, payload, RSSI, received count and duplicate count.
|
||||||
|
# - Creates a short Home Assistant event pulse for each unique press_id.
|
||||||
|
#
|
||||||
|
# NOTES:
|
||||||
|
# - This device is the powered receiver.
|
||||||
|
# - The transmitter should send the same packet 2-3 times for reliability.
|
||||||
|
# - This receiver de-duplicates repeated packets using the full payload string.
|
||||||
|
# - Expected test payload format from transmitter:
|
||||||
|
# REMOTE_A:BUTTON_1:123
|
||||||
|
#
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
device_name: esp-espnow_test_receiver
|
||||||
|
friendly_name: "ESPNow Test Receiver"
|
||||||
|
area: "Test Bench"
|
||||||
|
|
||||||
|
# Passwords & Secrets (Unfortunately, you can't use substitutions inside secret names)
|
||||||
|
# Re-using your existing bulb flasher secrets (adjust if you prefer per-device secrets)
|
||||||
|
api_key: !secret esp-api_key
|
||||||
|
ota_pass: !secret esp-ota_pass
|
||||||
|
#static_ip_address: !secret esp-bedrm1arlecbulb_ip
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# PACKAGES: Included Common Packages
|
||||||
|
# https://esphome.io/components/packages.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
packages:
|
||||||
|
#### WIFI, Network (DHCP/IPV6 etc), Fallback AP, Safemode ####
|
||||||
|
common_wifi: !include
|
||||||
|
file: common/network_common_dhcp.yaml
|
||||||
|
vars:
|
||||||
|
local_device_name: "${device_name}"
|
||||||
|
local_ota_pass: "${ota_pass}"
|
||||||
|
|
||||||
|
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
|
||||||
|
common_api: !include
|
||||||
|
file: common/api_common.yaml
|
||||||
|
#file: common/api_common_noencryption.yaml
|
||||||
|
vars:
|
||||||
|
local_api_key: "${api_key}"
|
||||||
|
|
||||||
|
#### MQTT ####
|
||||||
|
#common_mqtt: !include
|
||||||
|
# file: common/mqtt_common.yaml
|
||||||
|
# vars:
|
||||||
|
# local_device_name: "${device_name}"
|
||||||
|
|
||||||
|
#### WEB PORTAL ####
|
||||||
|
#common_webportal: !include common/webportal_common.yaml
|
||||||
|
common_webportal: !include common/webportal_common_nopasswd.yaml
|
||||||
|
|
||||||
|
#### SNTP (Only use if you want/need accurate timeclocks) ####
|
||||||
|
#common_sntp: !include common/sntp_common.yaml
|
||||||
|
|
||||||
|
#### DIAGNOSTICS Sensors ####
|
||||||
|
diag_basic: !include common/include_basic_diag_sensors.yaml
|
||||||
|
diag_more: !include common/include_more_diag_sensors_nomqtt.yaml
|
||||||
|
#diag_debug: !include common/include_debug_diag_sensors.yaml
|
||||||
|
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${device_name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
area: ${area}
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# ESP-NOW Receiver
|
||||||
|
#:########################################################################################:#
|
||||||
|
#
|
||||||
|
# Do not set channel here while using Wi-Fi.
|
||||||
|
# The receiver's ESP-NOW channel follows the connected Wi-Fi channel.
|
||||||
|
#
|
||||||
|
|
||||||
|
espnow:
|
||||||
|
auto_add_peer: true
|
||||||
|
|
||||||
|
on_broadcast:
|
||||||
|
- then:
|
||||||
|
- lambda: |-
|
||||||
|
char src_mac[18];
|
||||||
|
snprintf(src_mac, sizeof(src_mac), "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||||
|
info.src_addr[0], info.src_addr[1], info.src_addr[2],
|
||||||
|
info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||||
|
|
||||||
|
std::string payload(reinterpret_cast<const char *>(data), size);
|
||||||
|
int rssi = info.rx_ctrl ? info.rx_ctrl->rssi : 0;
|
||||||
|
|
||||||
|
id(last_sender_mac).publish_state(src_mac);
|
||||||
|
id(last_payload).publish_state(payload);
|
||||||
|
id(last_rssi).publish_state(rssi);
|
||||||
|
|
||||||
|
id(received_count) += 1;
|
||||||
|
id(received_count_sensor).publish_state(id(received_count));
|
||||||
|
|
||||||
|
ESP_LOGI("espnow_rx", "ESP-NOW broadcast from %s, RSSI %d dBm, payload: %s",
|
||||||
|
src_mac, rssi, payload.c_str());
|
||||||
|
|
||||||
|
// Test payload format:
|
||||||
|
// REMOTE_A:BUTTON_1:123
|
||||||
|
//
|
||||||
|
// For now, use the whole payload as the duplicate key.
|
||||||
|
// This means the 2nd and 3rd repeated sends from one button press are ignored.
|
||||||
|
if (payload == id(last_unique_payload)) {
|
||||||
|
id(duplicate_count) += 1;
|
||||||
|
id(duplicate_count_sensor).publish_state(id(duplicate_count));
|
||||||
|
id(packet_is_unique) = false;
|
||||||
|
|
||||||
|
ESP_LOGD("espnow_rx", "Duplicate ESP-NOW payload ignored: %s", payload.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
id(last_unique_payload) = payload;
|
||||||
|
id(packet_is_unique) = true;
|
||||||
|
|
||||||
|
// Split simple colon-delimited payload for HA visibility.
|
||||||
|
size_t first_colon = payload.find(':');
|
||||||
|
size_t second_colon = payload.find(':', first_colon + 1);
|
||||||
|
|
||||||
|
if (first_colon != std::string::npos && second_colon != std::string::npos) {
|
||||||
|
std::string remote_id = payload.substr(0, first_colon);
|
||||||
|
std::string button_id = payload.substr(first_colon + 1, second_colon - first_colon - 1);
|
||||||
|
std::string press_id = payload.substr(second_colon + 1);
|
||||||
|
|
||||||
|
id(last_remote_id).publish_state(remote_id);
|
||||||
|
id(last_button_id).publish_state(button_id);
|
||||||
|
id(last_press_id).publish_state(press_id);
|
||||||
|
|
||||||
|
ESP_LOGI("espnow_rx", "Unique press: remote=%s button=%s press_id=%s",
|
||||||
|
remote_id.c_str(), button_id.c_str(), press_id.c_str());
|
||||||
|
} else {
|
||||||
|
id(last_remote_id).publish_state("unknown");
|
||||||
|
id(last_button_id).publish_state("unknown");
|
||||||
|
id(last_press_id).publish_state("unknown");
|
||||||
|
|
||||||
|
ESP_LOGW("espnow_rx", "Payload did not match expected format: %s", payload.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return id(packet_is_unique);
|
||||||
|
then:
|
||||||
|
- binary_sensor.template.publish:
|
||||||
|
id: espnow_unique_press_event
|
||||||
|
state: ON
|
||||||
|
- script.execute: clear_unique_press_event
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# Globals
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: received_count
|
||||||
|
type: int
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: duplicate_count
|
||||||
|
type: int
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: last_unique_payload
|
||||||
|
type: std::string
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '""'
|
||||||
|
|
||||||
|
- id: packet_is_unique
|
||||||
|
type: bool
|
||||||
|
restore_value: false
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# Scripts
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: clear_unique_press_event
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- delay: 500ms
|
||||||
|
- binary_sensor.template.publish:
|
||||||
|
id: espnow_unique_press_event
|
||||||
|
state: OFF
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# Home Assistant Entities
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Last Sender MAC"
|
||||||
|
id: last_sender_mac
|
||||||
|
icon: mdi:access-point-network
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Last Payload"
|
||||||
|
id: last_payload
|
||||||
|
icon: mdi:message-text
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Last Remote ID"
|
||||||
|
id: last_remote_id
|
||||||
|
icon: mdi:remote
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Last Button ID"
|
||||||
|
id: last_button_id
|
||||||
|
icon: mdi:gesture-tap-button
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Last Press ID"
|
||||||
|
id: last_press_id
|
||||||
|
icon: mdi:identifier
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Last RSSI"
|
||||||
|
id: last_rssi
|
||||||
|
unit_of_measurement: "dBm"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
icon: mdi:wifi-strength-2
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Received Count"
|
||||||
|
id: received_count_sensor
|
||||||
|
accuracy_decimals: 0
|
||||||
|
icon: mdi:counter
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Duplicate Count"
|
||||||
|
id: duplicate_count_sensor
|
||||||
|
accuracy_decimals: 0
|
||||||
|
icon: mdi:counter
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Unique Press Event"
|
||||||
|
id: espnow_unique_press_event
|
||||||
|
icon: mdi:gesture-tap-button
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Reset ESP-NOW Test Counts"
|
||||||
|
icon: mdi:counter
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(received_count) = 0;
|
||||||
|
id(duplicate_count) = 0;
|
||||||
|
id(last_unique_payload) = "";
|
||||||
|
id(received_count_sensor).publish_state(0);
|
||||||
|
id(duplicate_count_sensor).publish_state(0);
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
#:########################################################################################:#
|
||||||
|
# ESP-NOW Test Transmitter 1
|
||||||
|
#:########################################################################################:#
|
||||||
|
#
|
||||||
|
# DEVICE:
|
||||||
|
# ESP32-S3-Zero / ESP32-S3 Super Mini
|
||||||
|
#
|
||||||
|
# VERSION:
|
||||||
|
# 1.2 - 2026-07-11
|
||||||
|
# - Changed GPIO4 wake source to ESP32 EXT1 ANY_LOW.
|
||||||
|
# - Added delay to allow ESP-NOW to initialise before transmission.
|
||||||
|
# - Added GPIO4 input and pull-up initialisation.
|
||||||
|
# - Waits for the wake button to be released before entering deep sleep.
|
||||||
|
# - Logs the ESP32 wake cause.
|
||||||
|
# - Sends each message three times for improved broadcast reliability.
|
||||||
|
#
|
||||||
|
# WIRING:
|
||||||
|
#
|
||||||
|
# 3.3 V
|
||||||
|
# |
|
||||||
|
# 10 kΩ
|
||||||
|
# |
|
||||||
|
# +-------- GPIO4
|
||||||
|
# |
|
||||||
|
# Push button
|
||||||
|
# |
|
||||||
|
# GND
|
||||||
|
#
|
||||||
|
# OPERATION:
|
||||||
|
# - Power-up or reset causes one transmission.
|
||||||
|
# - The transmitter sends the same ESP-NOW message three times.
|
||||||
|
# - It waits for GPIO4 to return HIGH.
|
||||||
|
# - It then enters deep sleep.
|
||||||
|
# - Pulling GPIO4 LOW wakes the transmitter and repeats the sequence.
|
||||||
|
#
|
||||||
|
# MESSAGE FORMAT:
|
||||||
|
# REMOTE_A:BUTTON_1:<random press ID>
|
||||||
|
#
|
||||||
|
# IMPORTANT:
|
||||||
|
# - ESP-NOW channel must match the receiver network channel.
|
||||||
|
# - The receiver is currently operating on channel 5.
|
||||||
|
# - This transmitter deliberately does not connect to normal Wi-Fi.
|
||||||
|
# - Firmware updates are performed by USB.
|
||||||
|
#
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
device_name: esp-espnow-test-transmitter1
|
||||||
|
friendly_name: "ESPNow Test Transmitter 1"
|
||||||
|
|
||||||
|
remote_id: "REMOTE_A"
|
||||||
|
button_id: "BUTTON_1"
|
||||||
|
|
||||||
|
espnow_channel: "5"
|
||||||
|
wake_gpio: "4"
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# ESPHome Core
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${device_name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
|
||||||
|
on_boot:
|
||||||
|
priority: -100
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
// Explicitly configure GPIO4 as an input with a pull-up.
|
||||||
|
gpio_set_direction(GPIO_NUM_4, GPIO_MODE_INPUT);
|
||||||
|
gpio_set_pull_mode(GPIO_NUM_4, GPIO_PULLUP_ONLY);
|
||||||
|
|
||||||
|
const esp_sleep_wakeup_cause_t wake_cause =
|
||||||
|
esp_sleep_get_wakeup_cause();
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"wake",
|
||||||
|
"ESP-NOW transmitter booted. Wake cause: %d",
|
||||||
|
static_cast<int>(wake_cause)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wake_cause == ESP_SLEEP_WAKEUP_EXT1) {
|
||||||
|
ESP_LOGI("wake", "Wake was caused by the GPIO4 EXT1 input.");
|
||||||
|
} else if (wake_cause == ESP_SLEEP_WAKEUP_UNDEFINED) {
|
||||||
|
ESP_LOGI("wake", "Wake was caused by power-up or reset.");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(
|
||||||
|
"wake",
|
||||||
|
"Wake was caused by another source: %d",
|
||||||
|
static_cast<int>(wake_cause)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allow all ESPHome components, including ESP-NOW, to finish setup.
|
||||||
|
- delay: 750ms
|
||||||
|
|
||||||
|
- lambda: |-
|
||||||
|
id(press_id) = esp_random();
|
||||||
|
|
||||||
|
char payload_buffer[96];
|
||||||
|
|
||||||
|
snprintf(
|
||||||
|
payload_buffer,
|
||||||
|
sizeof(payload_buffer),
|
||||||
|
"${remote_id}:${button_id}:%lu",
|
||||||
|
static_cast<unsigned long>(id(press_id))
|
||||||
|
);
|
||||||
|
|
||||||
|
id(transmit_payload) = payload_buffer;
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"espnow_tx",
|
||||||
|
"Prepared payload: %s",
|
||||||
|
id(transmit_payload).c_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
- script.execute: send_espnow_press
|
||||||
|
- script.wait: send_espnow_press
|
||||||
|
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "Transmission sequence completed."
|
||||||
|
|
||||||
|
# Do not enter sleep while GPIO4 is still held LOW.
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "Waiting for GPIO4 button release."
|
||||||
|
|
||||||
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return gpio_get_level(GPIO_NUM_4) == 1;
|
||||||
|
|
||||||
|
- delay: 100ms
|
||||||
|
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "GPIO4 released. Entering deep sleep."
|
||||||
|
|
||||||
|
- delay: 100ms
|
||||||
|
|
||||||
|
- deep_sleep.enter: deep_sleep_control
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# ESP32 Hardware
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32-s3-devkitc-1
|
||||||
|
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# Logging
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# ESP-NOW
|
||||||
|
#:########################################################################################:#
|
||||||
|
#
|
||||||
|
# The transmitter does not connect to Wi-Fi, so its radio channel is set
|
||||||
|
# explicitly. Both transmitter and receiver must use the same channel.
|
||||||
|
#
|
||||||
|
|
||||||
|
espnow:
|
||||||
|
channel: ${espnow_channel}
|
||||||
|
enable_on_boot: true
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# Globals
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: press_id
|
||||||
|
type: uint32_t
|
||||||
|
restore_value: false
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
- id: transmit_payload
|
||||||
|
type: std::string
|
||||||
|
restore_value: false
|
||||||
|
initial_value: '""'
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# ESP-NOW Transmission Script
|
||||||
|
#:########################################################################################:#
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: send_espnow_press
|
||||||
|
mode: single
|
||||||
|
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "Sending ESP-NOW broadcast 1 of 3."
|
||||||
|
|
||||||
|
- espnow.broadcast:
|
||||||
|
data: !lambda |-
|
||||||
|
return std::vector<uint8_t>(
|
||||||
|
id(transmit_payload).begin(),
|
||||||
|
id(transmit_payload).end()
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_for_sent: true
|
||||||
|
continue_on_error: true
|
||||||
|
|
||||||
|
on_sent:
|
||||||
|
- logger.log:
|
||||||
|
level: DEBUG
|
||||||
|
format: "Broadcast 1 submitted successfully."
|
||||||
|
|
||||||
|
on_error:
|
||||||
|
- logger.log:
|
||||||
|
level: WARN
|
||||||
|
format: "Broadcast 1 reported an error."
|
||||||
|
|
||||||
|
- delay: 75ms
|
||||||
|
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "Sending ESP-NOW broadcast 2 of 3."
|
||||||
|
|
||||||
|
- espnow.broadcast:
|
||||||
|
data: !lambda |-
|
||||||
|
return std::vector<uint8_t>(
|
||||||
|
id(transmit_payload).begin(),
|
||||||
|
id(transmit_payload).end()
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_for_sent: true
|
||||||
|
continue_on_error: true
|
||||||
|
|
||||||
|
on_sent:
|
||||||
|
- logger.log:
|
||||||
|
level: DEBUG
|
||||||
|
format: "Broadcast 2 submitted successfully."
|
||||||
|
|
||||||
|
on_error:
|
||||||
|
- logger.log:
|
||||||
|
level: WARN
|
||||||
|
format: "Broadcast 2 reported an error."
|
||||||
|
|
||||||
|
- delay: 75ms
|
||||||
|
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "Sending ESP-NOW broadcast 3 of 3."
|
||||||
|
|
||||||
|
- espnow.broadcast:
|
||||||
|
data: !lambda |-
|
||||||
|
return std::vector<uint8_t>(
|
||||||
|
id(transmit_payload).begin(),
|
||||||
|
id(transmit_payload).end()
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_for_sent: true
|
||||||
|
continue_on_error: true
|
||||||
|
|
||||||
|
on_sent:
|
||||||
|
- logger.log:
|
||||||
|
level: DEBUG
|
||||||
|
format: "Broadcast 3 submitted successfully."
|
||||||
|
|
||||||
|
on_error:
|
||||||
|
- logger.log:
|
||||||
|
level: WARN
|
||||||
|
format: "Broadcast 3 reported an error."
|
||||||
|
|
||||||
|
- delay: 75ms
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# Deep Sleep and GPIO4 Wake
|
||||||
|
#:########################################################################################:#
|
||||||
|
#
|
||||||
|
# ESP32-S3 supports EXT1 ANY_LOW wake.
|
||||||
|
#
|
||||||
|
# With one configured pin, ANY_LOW means:
|
||||||
|
# Wake when GPIO4 is pulled LOW.
|
||||||
|
#
|
||||||
|
# An external 10 kΩ pull-up between GPIO4 and 3.3 V is recommended.
|
||||||
|
#
|
||||||
|
|
||||||
|
deep_sleep:
|
||||||
|
id: deep_sleep_control
|
||||||
|
|
||||||
|
esp32_ext1_wakeup:
|
||||||
|
pins:
|
||||||
|
- GPIO${wake_gpio}
|
||||||
|
|
||||||
|
mode: ANY_LOW
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-laundrydrypow.yaml
|
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-laundrydrypow.yaml
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# VERSIONS:
|
# VERSIONS:
|
||||||
|
# V1.2 2026-07-12 Added cumulative Load Energy integration sensor
|
||||||
|
# Added seven-day maximum power sensor
|
||||||
|
# Maximum power resets after 168 powered hours
|
||||||
# V1.1 2026-03-11 Updated yaml to zorruno layout V1.1
|
# V1.1 2026-03-11 Updated yaml to zorruno layout V1.1
|
||||||
# V1.0 2025-05-28 Initial Version
|
# V1.0 2025-05-28 Initial Version
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
@@ -20,7 +23,16 @@
|
|||||||
# - Reference: https://devices.esphome.io/devices/Sonoff-POW-R1
|
# - Reference: https://devices.esphome.io/devices/Sonoff-POW-R1
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# OPERATION NOTES:
|
# OPERATION NOTES:
|
||||||
# - Monitors dryer load current, voltage, and power
|
# - Monitors dryer load current, voltage, power, and cumulative energy
|
||||||
|
# - Load Energy integrates measured power over time and reports kWh
|
||||||
|
# - Load Energy resets to zero if the ESPHome device reboots
|
||||||
|
# - Home Assistant handles the reset because the sensor uses total_increasing
|
||||||
|
# - Load Maximum Power 7 Days records the highest measured power
|
||||||
|
# - Maximum power resets after 168 powered hours
|
||||||
|
# - The maximum power value and elapsed-hour counter are retained across reboots
|
||||||
|
# - The elapsed-hour counter is saved every six hours to reduce flash writes
|
||||||
|
# - An unexpected power loss may extend the reset period by up to approximately six hours
|
||||||
|
# - No accurate clock or SNTP connection is required for the seven-day reset
|
||||||
# - Provides a virtual relay switch that controls the physical relay and LED together
|
# - Provides a virtual relay switch that controls the physical relay and LED together
|
||||||
# - Local button toggles the virtual relay switch
|
# - Local button toggles the virtual relay switch
|
||||||
# - On boot, the virtual relay is turned ON
|
# - On boot, the virtual relay is turned ON
|
||||||
@@ -31,15 +43,18 @@
|
|||||||
# a) Home Assistant offline (network and MQTT online):
|
# a) Home Assistant offline (network and MQTT online):
|
||||||
# - Device continues running normally
|
# - Device continues running normally
|
||||||
# - Local button still works
|
# - Local button still works
|
||||||
# - MQTT remains available if your common MQTT package is enabled and broker is reachable
|
# - Power, energy, running state, and maximum power continue operating locally
|
||||||
|
# - MQTT remains available if the common MQTT package is enabled and broker is reachable
|
||||||
#
|
#
|
||||||
# b) MQTT offline (or HA/MQTT offline):
|
# b) MQTT offline (or HA/MQTT offline):
|
||||||
# - Device continues running locally
|
# - Device continues running locally
|
||||||
# - Local button, relay control, LED control, and power monitoring still work
|
# - Local button, relay control, LED control, and power monitoring still work
|
||||||
|
# - Energy integration and maximum power monitoring continue locally
|
||||||
#
|
#
|
||||||
# c) Entire WiFi/Network offline:
|
# c) Entire WiFi/Network offline:
|
||||||
# - Device continues running locally
|
# - Device continues running locally
|
||||||
# - Local button, relay control, LED control, and power monitoring still work
|
# - Local button, relay control, LED control, and power monitoring still work
|
||||||
|
# - Energy integration and maximum power monitoring continue locally
|
||||||
# - Accurate time is not needed; SNTP is not required for operation
|
# - Accurate time is not needed; SNTP is not required for operation
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
|
|
||||||
@@ -57,7 +72,7 @@ substitutions:
|
|||||||
|
|
||||||
# Project Naming
|
# Project Naming
|
||||||
project_name: "Sonoff Technologies.POW R1"
|
project_name: "Sonoff Technologies.POW R1"
|
||||||
project_version: "v1.1"
|
project_version: "v1.2"
|
||||||
|
|
||||||
# Entity Naming
|
# Entity Naming
|
||||||
entity_prefix: "Load"
|
entity_prefix: "Load"
|
||||||
@@ -121,9 +136,14 @@ esphome:
|
|||||||
friendly_name: "${friendly_name}"
|
friendly_name: "${friendly_name}"
|
||||||
comment: "${description_comment}"
|
comment: "${description_comment}"
|
||||||
area: "${device_area}"
|
area: "${device_area}"
|
||||||
|
|
||||||
|
# Limit parallel compiler processes to reduce host RAM usage.
|
||||||
|
compile_process_limit: 1
|
||||||
|
|
||||||
project:
|
project:
|
||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 200
|
priority: 200
|
||||||
then:
|
then:
|
||||||
@@ -157,6 +177,28 @@ logger:
|
|||||||
# number: GPIO2
|
# number: GPIO2
|
||||||
# inverted: true
|
# inverted: true
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# GLOBALS:
|
||||||
|
# Retained maximum power and elapsed-hour counter
|
||||||
|
# https://esphome.io/components/globals.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
globals:
|
||||||
|
# Highest measured power during the current seven-day period.
|
||||||
|
# The value is checked for changes every five minutes before being saved.
|
||||||
|
- id: max_power_7d
|
||||||
|
type: float
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0.0'
|
||||||
|
update_interval: 5min
|
||||||
|
|
||||||
|
# Number of powered hours elapsed in the current seven-day period.
|
||||||
|
# Saved every six hours to reduce ESP8266 flash writes.
|
||||||
|
- id: max_power_hours_elapsed
|
||||||
|
type: uint16_t
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
update_interval: 6h
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# BINARY SENSORS:
|
# BINARY SENSORS:
|
||||||
# https://esphome.io/components/binary_sensor/
|
# https://esphome.io/components/binary_sensor/
|
||||||
@@ -178,7 +220,7 @@ binary_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (isnan(id(power).state)) {
|
if (isnan(id(power).state)) {
|
||||||
return {};
|
return {};
|
||||||
} else if (id(power).state > 4) {
|
} else if (id(power).state > 4.0f) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@@ -194,13 +236,44 @@ sensor:
|
|||||||
cf_pin: GPIO14
|
cf_pin: GPIO14
|
||||||
cf1_pin: GPIO13
|
cf1_pin: GPIO13
|
||||||
update_interval: 2s
|
update_interval: 2s
|
||||||
|
|
||||||
current:
|
current:
|
||||||
name: "${entity_prefix} Current"
|
name: "${entity_prefix} Current"
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "A"
|
||||||
|
accuracy_decimals: 2
|
||||||
|
|
||||||
voltage:
|
voltage:
|
||||||
name: "${entity_prefix} Voltage"
|
name: "${entity_prefix} Voltage"
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
power:
|
power:
|
||||||
name: "${entity_prefix} Power"
|
name: "${entity_prefix} Power"
|
||||||
id: power
|
id: power
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
# Update the seven-day maximum whenever a new peak is measured.
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (!isnan(x) && x > id(max_power_7d)) {
|
||||||
|
id(max_power_7d) = x;
|
||||||
|
id(max_power_7d_sensor).publish_state(x);
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"max_power",
|
||||||
|
"New seven-day maximum power: %.1f W",
|
||||||
|
x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
# on_value_range:
|
# on_value_range:
|
||||||
# - above: 4.0
|
# - above: 4.0
|
||||||
# then:
|
# then:
|
||||||
@@ -209,6 +282,64 @@ sensor:
|
|||||||
# then:
|
# then:
|
||||||
# - light.turn_off: led
|
# - light.turn_off: led
|
||||||
|
|
||||||
|
# Integrates instantaneous watts over time.
|
||||||
|
# Watts integrated over hours produces Wh, then multiply converts Wh to kWh.
|
||||||
|
- platform: integration
|
||||||
|
name: "${entity_prefix} Energy"
|
||||||
|
id: energy
|
||||||
|
sensor: power
|
||||||
|
time_unit: h
|
||||||
|
integration_method: trapezoid
|
||||||
|
restore: false
|
||||||
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
accuracy_decimals: 3
|
||||||
|
filters:
|
||||||
|
- multiply: 0.001
|
||||||
|
|
||||||
|
# Highest power reading recorded during the current 168 powered-hour period.
|
||||||
|
- platform: template
|
||||||
|
name: "${entity_prefix} Maximum Power 7 Days"
|
||||||
|
id: max_power_7d_sensor
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
accuracy_decimals: 1
|
||||||
|
update_interval: 60s
|
||||||
|
lambda: |-
|
||||||
|
return id(max_power_7d);
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# INTERVAL:
|
||||||
|
# Seven-day maximum power reset
|
||||||
|
# https://esphome.io/components/interval.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
interval:
|
||||||
|
# Count one powered hour at a time.
|
||||||
|
# 168 hours equals seven days.
|
||||||
|
- interval: 1h
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(max_power_hours_elapsed) += 1;
|
||||||
|
|
||||||
|
ESP_LOGD(
|
||||||
|
"max_power",
|
||||||
|
"Seven-day maximum power period: %u of 168 powered hours elapsed",
|
||||||
|
id(max_power_hours_elapsed)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (id(max_power_hours_elapsed) >= 168) {
|
||||||
|
id(max_power_hours_elapsed) = 0;
|
||||||
|
id(max_power_7d) = 0.0f;
|
||||||
|
id(max_power_7d_sensor).publish_state(0.0f);
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"max_power",
|
||||||
|
"Seven-day maximum power value has been reset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# SWITCH:
|
# SWITCH:
|
||||||
# https://esphome.io/components/switch/
|
# https://esphome.io/components/switch/
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-laundrywashpow.yaml
|
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-laundrywashpow.yaml
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# VERSIONS:
|
# VERSIONS:
|
||||||
|
# V1.2 2026-07-12 Added cumulative Load Energy integration sensor
|
||||||
|
# Added seven-day maximum power sensor
|
||||||
|
# Maximum power resets after 168 powered hours
|
||||||
# V1.1 2026-03-11 Updated yaml to zorruno layout V1.1
|
# V1.1 2026-03-11 Updated yaml to zorruno layout V1.1
|
||||||
# V1.0 2025-03-28 Initial Version
|
# V1.0 2025-03-28 Initial Version
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
@@ -20,7 +23,18 @@
|
|||||||
# - Reference: https://devices.esphome.io/devices/Sonoff-POW-R1
|
# - Reference: https://devices.esphome.io/devices/Sonoff-POW-R1
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# OPERATION NOTES:
|
# OPERATION NOTES:
|
||||||
# - Monitors washing machine load current, voltage, and power
|
# - Monitors washing machine load current, voltage, power, and cumulative energy
|
||||||
|
# - Load Energy integrates measured power over time and reports kWh
|
||||||
|
# - Load Energy resets to zero if the ESPHome device reboots
|
||||||
|
# - Home Assistant handles the reset because the sensor uses total_increasing
|
||||||
|
# - Load Maximum Power 7 Days records the highest measured power
|
||||||
|
# - Maximum power resets after 168 powered hours
|
||||||
|
# - The maximum power value and elapsed-hour counter are retained across reboots
|
||||||
|
# - The maximum power value is checked for saving every five minutes
|
||||||
|
# - The elapsed-hour counter is checked for saving every six hours
|
||||||
|
# - Reduced save frequency limits unnecessary ESP8266 flash writes
|
||||||
|
# - An unexpected power loss may extend the reset period by up to approximately six hours
|
||||||
|
# - No accurate clock or SNTP connection is required for the seven-day reset
|
||||||
# - Provides a virtual relay switch that controls the physical relay and LED together
|
# - Provides a virtual relay switch that controls the physical relay and LED together
|
||||||
# - Local button toggles the virtual relay switch
|
# - Local button toggles the virtual relay switch
|
||||||
# - On boot, the virtual relay is turned ON
|
# - On boot, the virtual relay is turned ON
|
||||||
@@ -31,15 +45,18 @@
|
|||||||
# a) Home Assistant offline (network and MQTT online):
|
# a) Home Assistant offline (network and MQTT online):
|
||||||
# - Device continues running normally
|
# - Device continues running normally
|
||||||
# - Local button still works
|
# - Local button still works
|
||||||
# - MQTT remains available if your common MQTT package is enabled and broker is reachable
|
# - Power, energy, running state, and maximum power continue operating locally
|
||||||
|
# - MQTT remains available if the common MQTT package is enabled and broker is reachable
|
||||||
#
|
#
|
||||||
# b) MQTT offline (or HA/MQTT offline):
|
# b) MQTT offline (or HA/MQTT offline):
|
||||||
# - Device continues running locally
|
# - Device continues running locally
|
||||||
# - Local button, relay control, LED control, and power monitoring still work
|
# - Local button, relay control, LED control, and power monitoring still work
|
||||||
|
# - Energy integration and maximum power monitoring continue locally
|
||||||
#
|
#
|
||||||
# c) Entire WiFi/Network offline:
|
# c) Entire WiFi/Network offline:
|
||||||
# - Device continues running locally
|
# - Device continues running locally
|
||||||
# - Local button, relay control, LED control, and power monitoring still work
|
# - Local button, relay control, LED control, and power monitoring still work
|
||||||
|
# - Energy integration and maximum power monitoring continue locally
|
||||||
# - Accurate time is not needed; SNTP is not required for operation
|
# - Accurate time is not needed; SNTP is not required for operation
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
|
|
||||||
@@ -57,7 +74,7 @@ substitutions:
|
|||||||
|
|
||||||
# Project Naming
|
# Project Naming
|
||||||
project_name: "Sonoff Technologies.POW R1"
|
project_name: "Sonoff Technologies.POW R1"
|
||||||
project_version: "v1.1"
|
project_version: "v1.2"
|
||||||
|
|
||||||
# Entity Naming
|
# Entity Naming
|
||||||
entity_prefix: "Load"
|
entity_prefix: "Load"
|
||||||
@@ -121,9 +138,14 @@ esphome:
|
|||||||
friendly_name: "${friendly_name}"
|
friendly_name: "${friendly_name}"
|
||||||
comment: "${description_comment}"
|
comment: "${description_comment}"
|
||||||
area: "${device_area}"
|
area: "${device_area}"
|
||||||
|
|
||||||
|
# Limit parallel compiler processes to reduce host RAM usage.
|
||||||
|
compile_process_limit: 1
|
||||||
|
|
||||||
project:
|
project:
|
||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
|
|
||||||
on_boot:
|
on_boot:
|
||||||
priority: 200
|
priority: 200
|
||||||
then:
|
then:
|
||||||
@@ -157,6 +179,28 @@ logger:
|
|||||||
# number: GPIO2
|
# number: GPIO2
|
||||||
# inverted: true
|
# inverted: true
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# GLOBALS:
|
||||||
|
# Retained maximum power and elapsed-hour counter
|
||||||
|
# https://esphome.io/components/globals.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
globals:
|
||||||
|
# Highest measured power during the current seven-day period.
|
||||||
|
# The value is checked for changes every five minutes before being saved.
|
||||||
|
- id: max_power_7d
|
||||||
|
type: float
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0.0'
|
||||||
|
update_interval: 5min
|
||||||
|
|
||||||
|
# Number of powered hours elapsed in the current seven-day period.
|
||||||
|
# The value is checked for saving every six hours to reduce flash writes.
|
||||||
|
- id: max_power_hours_elapsed
|
||||||
|
type: uint16_t
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
update_interval: 6h
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# BINARY SENSORS:
|
# BINARY SENSORS:
|
||||||
# https://esphome.io/components/binary_sensor/
|
# https://esphome.io/components/binary_sensor/
|
||||||
@@ -178,7 +222,7 @@ binary_sensor:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
if (isnan(id(power).state)) {
|
if (isnan(id(power).state)) {
|
||||||
return {};
|
return {};
|
||||||
} else if (id(power).state > 4) {
|
} else if (id(power).state > 4.0f) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
@@ -194,13 +238,44 @@ sensor:
|
|||||||
cf_pin: GPIO14
|
cf_pin: GPIO14
|
||||||
cf1_pin: GPIO13
|
cf1_pin: GPIO13
|
||||||
update_interval: 2s
|
update_interval: 2s
|
||||||
|
|
||||||
current:
|
current:
|
||||||
name: "${entity_prefix} Current"
|
name: "${entity_prefix} Current"
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "A"
|
||||||
|
accuracy_decimals: 2
|
||||||
|
|
||||||
voltage:
|
voltage:
|
||||||
name: "${entity_prefix} Voltage"
|
name: "${entity_prefix} Voltage"
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
power:
|
power:
|
||||||
name: "${entity_prefix} Power"
|
name: "${entity_prefix} Power"
|
||||||
id: power
|
id: power
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
# Update the seven-day maximum whenever a new peak is measured.
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (!isnan(x) && x > id(max_power_7d)) {
|
||||||
|
id(max_power_7d) = x;
|
||||||
|
id(max_power_7d_sensor).publish_state(x);
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"max_power",
|
||||||
|
"New seven-day maximum power: %.1f W",
|
||||||
|
x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
# on_value_range:
|
# on_value_range:
|
||||||
# - above: 4.0
|
# - above: 4.0
|
||||||
# then:
|
# then:
|
||||||
@@ -209,6 +284,64 @@ sensor:
|
|||||||
# then:
|
# then:
|
||||||
# - light.turn_off: led
|
# - light.turn_off: led
|
||||||
|
|
||||||
|
# Integrates instantaneous watts over time.
|
||||||
|
# Watts integrated over hours produces Wh, then multiply converts Wh to kWh.
|
||||||
|
- platform: integration
|
||||||
|
name: "${entity_prefix} Energy"
|
||||||
|
id: energy
|
||||||
|
sensor: power
|
||||||
|
time_unit: h
|
||||||
|
integration_method: trapezoid
|
||||||
|
restore: false
|
||||||
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
accuracy_decimals: 3
|
||||||
|
filters:
|
||||||
|
- multiply: 0.001
|
||||||
|
|
||||||
|
# Highest power reading recorded during the current 168 powered-hour period.
|
||||||
|
- platform: template
|
||||||
|
name: "${entity_prefix} Maximum Power 7 Days"
|
||||||
|
id: max_power_7d_sensor
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
accuracy_decimals: 1
|
||||||
|
update_interval: 60s
|
||||||
|
lambda: |-
|
||||||
|
return id(max_power_7d);
|
||||||
|
|
||||||
|
#:########################################################################################:#
|
||||||
|
# INTERVAL:
|
||||||
|
# Seven-day maximum power reset
|
||||||
|
# https://esphome.io/components/interval.html
|
||||||
|
#:########################################################################################:#
|
||||||
|
interval:
|
||||||
|
# Count one powered hour at a time.
|
||||||
|
# 168 hours equals seven days.
|
||||||
|
- interval: 1h
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(max_power_hours_elapsed) += 1;
|
||||||
|
|
||||||
|
ESP_LOGD(
|
||||||
|
"max_power",
|
||||||
|
"Seven-day maximum power period: %u of 168 powered hours elapsed",
|
||||||
|
id(max_power_hours_elapsed)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (id(max_power_hours_elapsed) >= 168) {
|
||||||
|
id(max_power_hours_elapsed) = 0;
|
||||||
|
id(max_power_7d) = 0.0f;
|
||||||
|
id(max_power_7d_sensor).publish_state(0.0f);
|
||||||
|
|
||||||
|
ESP_LOGI(
|
||||||
|
"max_power",
|
||||||
|
"Seven-day maximum power value has been reset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# SWITCH:
|
# SWITCH:
|
||||||
# https://esphome.io/components/switch/
|
# https://esphome.io/components/switch/
|
||||||
|
|||||||
@@ -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
@@ -6,6 +6,11 @@
|
|||||||
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-mainpowermonitor.yaml
|
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-mainpowermonitor.yaml
|
||||||
#:########################################################################################:#
|
#:########################################################################################:#
|
||||||
# VERSIONS:
|
# VERSIONS:
|
||||||
|
# v1.4 2026-07-12 Added Home Assistant Energy Dashboard metadata.
|
||||||
|
# Marked cumulative PZEM, mains total, and 32A Wallcharger total energy
|
||||||
|
# sensors as energy / total_increasing.
|
||||||
|
# Marked mains and EV power sensors as power / measurement.
|
||||||
|
# Added single-process compilation limit to reduce Builder memory use.
|
||||||
# v1.3 2026-02-25 Updated yaml to zorruno layout V1.1
|
# v1.3 2026-02-25 Updated yaml to zorruno layout V1.1
|
||||||
# v1.2 2026-02-25 Updated yaml to zorruno layout V1.1. Added EV charging binary sensor
|
# v1.2 2026-02-25 Updated yaml to zorruno layout V1.1. Added EV charging binary sensor
|
||||||
# and EV power rolling average (default 5 min). Clarified EV pulse notes.
|
# and EV power rolling average (default 5 min). Clarified EV pulse notes.
|
||||||
@@ -21,6 +26,11 @@
|
|||||||
# OPERATION NOTES:
|
# OPERATION NOTES:
|
||||||
# - Power Monitor using 3x PZEM-004T V3 (Modbus) for 3-phase monitoring
|
# - Power Monitor using 3x PZEM-004T V3 (Modbus) for 3-phase monitoring
|
||||||
# - Pulse input energy monitoring for 32A EV Charger (2 Wh per pulse)
|
# - Pulse input energy monitoring for 32A EV Charger (2 Wh per pulse)
|
||||||
|
# - 32A Wallcharger Total Energy is cumulative pulse-derived energy in kWh
|
||||||
|
# - Cumulative active-energy sensors are exposed to Home Assistant with:
|
||||||
|
# device_class: energy
|
||||||
|
# state_class: total_increasing
|
||||||
|
# - This allows the cumulative totals to be selected in the HA Energy Dashboard
|
||||||
# - Adds a 5 minute rolling average for EV power to avoid spiky readings at low loads
|
# - Adds a 5 minute rolling average for EV power to avoid spiky readings at low loads
|
||||||
# - Adds a binary sensor to indicate when the EV charger is operating (threshold configurable)
|
# - Adds a binary sensor to indicate when the EV charger is operating (threshold configurable)
|
||||||
#
|
#
|
||||||
@@ -60,7 +70,7 @@ substitutions:
|
|||||||
|
|
||||||
# Project Naming
|
# Project Naming
|
||||||
project_name: "Wemos.D1Mini PZEM-004T V3 3-Phase Power Monitor"
|
project_name: "Wemos.D1Mini PZEM-004T V3 3-Phase Power Monitor"
|
||||||
project_version: "v1.3"
|
project_version: "v1.4"
|
||||||
|
|
||||||
# Passwords
|
# Passwords
|
||||||
api_key: !secret esp-api_key
|
api_key: !secret esp-api_key
|
||||||
@@ -202,6 +212,10 @@ esphome:
|
|||||||
friendly_name: "${friendly_name}"
|
friendly_name: "${friendly_name}"
|
||||||
comment: "${description_comment}"
|
comment: "${description_comment}"
|
||||||
area: "${device_area}"
|
area: "${device_area}"
|
||||||
|
|
||||||
|
# Restrict PlatformIO to one simultaneous compiler process to reduce host RAM use.
|
||||||
|
compile_process_limit: 1
|
||||||
|
|
||||||
project:
|
project:
|
||||||
name: "${project_name}"
|
name: "${project_name}"
|
||||||
version: "${project_version}"
|
version: "${project_version}"
|
||||||
@@ -467,11 +481,16 @@ sensor:
|
|||||||
power:
|
power:
|
||||||
name: "${pzem_red_p_name}"
|
name: "${pzem_red_p_name}"
|
||||||
id: pzem_red_power_w
|
id: pzem_red_power_w
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
energy:
|
energy:
|
||||||
name: "${pzem_red_e_name}"
|
name: "${pzem_red_e_name}"
|
||||||
id: pzem_red_energy_kwh
|
id: pzem_red_energy_kwh
|
||||||
unit_of_measurement: "kWh"
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
accuracy_decimals: 3
|
accuracy_decimals: 3
|
||||||
filters:
|
filters:
|
||||||
- multiply: 0.001
|
- multiply: 0.001
|
||||||
@@ -537,11 +556,16 @@ sensor:
|
|||||||
power:
|
power:
|
||||||
name: "${pzem_yellow_p_name}"
|
name: "${pzem_yellow_p_name}"
|
||||||
id: pzem_yellow_power_w
|
id: pzem_yellow_power_w
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
energy:
|
energy:
|
||||||
name: "${pzem_yellow_e_name}"
|
name: "${pzem_yellow_e_name}"
|
||||||
id: pzem_yellow_energy_kwh
|
id: pzem_yellow_energy_kwh
|
||||||
unit_of_measurement: "kWh"
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
accuracy_decimals: 3
|
accuracy_decimals: 3
|
||||||
filters:
|
filters:
|
||||||
- multiply: 0.001
|
- multiply: 0.001
|
||||||
@@ -607,11 +631,16 @@ sensor:
|
|||||||
power:
|
power:
|
||||||
name: "${pzem_blue_p_name}"
|
name: "${pzem_blue_p_name}"
|
||||||
id: pzem_blue_power_w
|
id: pzem_blue_power_w
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
energy:
|
energy:
|
||||||
name: "${pzem_blue_e_name}"
|
name: "${pzem_blue_e_name}"
|
||||||
id: pzem_blue_energy_kwh
|
id: pzem_blue_energy_kwh
|
||||||
unit_of_measurement: "kWh"
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
accuracy_decimals: 3
|
accuracy_decimals: 3
|
||||||
filters:
|
filters:
|
||||||
- multiply: 0.001
|
- multiply: 0.001
|
||||||
@@ -665,6 +694,8 @@ sensor:
|
|||||||
name: "${mains_total_p_name}"
|
name: "${mains_total_p_name}"
|
||||||
id: mains_total_active_power_w
|
id: mains_total_active_power_w
|
||||||
unit_of_measurement: "W"
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
update_interval: ${pzem_update_interval}
|
update_interval: ${pzem_update_interval}
|
||||||
lambda: |-
|
lambda: |-
|
||||||
@@ -711,6 +742,8 @@ sensor:
|
|||||||
name: "${mains_total_e_name}"
|
name: "${mains_total_e_name}"
|
||||||
id: mains_total_active_energy_kwh
|
id: mains_total_active_energy_kwh
|
||||||
unit_of_measurement: "kWh"
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
accuracy_decimals: 3
|
accuracy_decimals: 3
|
||||||
update_interval: ${pzem_update_interval}
|
update_interval: ${pzem_update_interval}
|
||||||
lambda: |-
|
lambda: |-
|
||||||
@@ -957,6 +990,8 @@ sensor:
|
|||||||
name: "${ev_power_name}"
|
name: "${ev_power_name}"
|
||||||
id: ev_power_avg_w
|
id: ev_power_avg_w
|
||||||
unit_of_measurement: "W"
|
unit_of_measurement: "W"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
update_interval: "${ev_power_avg_sample_interval}"
|
update_interval: "${ev_power_avg_sample_interval}"
|
||||||
lambda: |-
|
lambda: |-
|
||||||
@@ -966,6 +1001,8 @@ sensor:
|
|||||||
name: "${ev_total_energy_name}"
|
name: "${ev_total_energy_name}"
|
||||||
id: ev_total_energy_kwh
|
id: ev_total_energy_kwh
|
||||||
unit_of_measurement: "kWh"
|
unit_of_measurement: "kWh"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
accuracy_decimals: 3
|
accuracy_decimals: 3
|
||||||
update_interval: 10s
|
update_interval: 10s
|
||||||
lambda: |-
|
lambda: |-
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
|||||||
# PACKAGE: Appliance Monitor Processor
|
# PACKAGE: Appliance Monitor Processor
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Version: 1.2
|
# Version: 1.3
|
||||||
# Date: 2026-06-09
|
# Date: 2026-07-11
|
||||||
#
|
#
|
||||||
# Purpose:
|
# Purpose:
|
||||||
# Shared appliance power monitoring processor.
|
# Shared appliance power monitoring processor.
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
# - One MQTT sensor per appliance.
|
# - One MQTT sensor per appliance.
|
||||||
# - Timestamp-based energy and cost calculation.
|
# - Timestamp-based energy and cost calculation.
|
||||||
# - Rolling average state detection.
|
# - Rolling average state detection.
|
||||||
|
# - Completion announcements through script.view_road_announcement.
|
||||||
#
|
#
|
||||||
# Default values:
|
# Default values:
|
||||||
# debug_flow: false
|
# debug_flow: false
|
||||||
@@ -40,6 +41,16 @@
|
|||||||
# Finished
|
# Finished
|
||||||
#
|
#
|
||||||
# Notes:
|
# 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:
|
# Version 1.2:
|
||||||
# - Added min_cycle_notify_seconds.
|
# - Added min_cycle_notify_seconds.
|
||||||
# - Finished cycles shorter than this are still recorded, but do not send
|
# - Finished cycles shorter than this are still recorded, but do not send
|
||||||
@@ -53,17 +64,39 @@
|
|||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
# FINISHED ANNOUNCEMENT
|
# 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
|
# The finishing script calls:
|
||||||
# method there.
|
#
|
||||||
|
# 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:
|
# Announcements can use:
|
||||||
# am_announcement_channels
|
# announcement_enabled
|
||||||
# am_announcement_title
|
# announcement_channels
|
||||||
# am_short_announcement
|
# title
|
||||||
# am_long_announcement
|
# short_announcement
|
||||||
# am_matrix_indicator
|
# long_announcement
|
||||||
|
# announcement_payload
|
||||||
|
# payload
|
||||||
|
# matrix_indicator
|
||||||
|
# mqtt_feed
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
@@ -127,6 +160,59 @@ script:
|
|||||||
step: 1
|
step: 1
|
||||||
unit_of_measurement: s
|
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:
|
sequence:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# CORE DEFAULTS AND PASSED VALUES
|
# CORE DEFAULTS AND PASSED VALUES
|
||||||
@@ -156,10 +242,36 @@ script:
|
|||||||
##########################################################################
|
##########################################################################
|
||||||
# ANNOUNCEMENT DEFAULTS AND PASSED VALUES
|
# 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:
|
- 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: >-
|
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: >-
|
am_short_announcement: >-
|
||||||
{{ short_announcement | default((am_appliance_action | title) ~ ' complete', true) | string }}
|
{{ short_announcement | default((am_appliance_action | title) ~ ' complete', true) | string }}
|
||||||
@@ -170,7 +282,20 @@ script:
|
|||||||
am_announcement_title: >-
|
am_announcement_title: >-
|
||||||
{{ title | default(am_appliance_name ~ ' Notification', true) | string }}
|
{{ 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
|
# 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))
|
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_notification_suppressed: >-
|
||||||
{{
|
{{
|
||||||
am_cycle_finished | bool(false)
|
am_cycle_finished | bool(false)
|
||||||
@@ -631,6 +762,8 @@ script:
|
|||||||
am_cycle_notification_suppressed_reason: >-
|
am_cycle_notification_suppressed_reason: >-
|
||||||
{% if am_cycle_notification_suppressed | bool(false) %}
|
{% if am_cycle_notification_suppressed | bool(false) %}
|
||||||
Cycle was shorter than {{ am_min_cycle_notify_seconds | int(300) }} seconds
|
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 %}
|
{% endif %}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@@ -673,6 +806,10 @@ script:
|
|||||||
|
|
||||||
min_cycle_notify_seconds=am_min_cycle_notify_seconds | int(300),
|
min_cycle_notify_seconds=am_min_cycle_notify_seconds | int(300),
|
||||||
cycle_notify_allowed=am_cycle_notify_allowed | bool(false),
|
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=am_cycle_notification_suppressed | bool(false),
|
||||||
cycle_notification_suppressed_reason=am_cycle_notification_suppressed_reason | trim,
|
cycle_notification_suppressed_reason=am_cycle_notification_suppressed_reason | trim,
|
||||||
|
|
||||||
@@ -740,18 +877,45 @@ script:
|
|||||||
threshold. No announcement was sent.
|
threshold. No announcement was sent.
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# FINISHED ANNOUNCEMENT
|
# ANNOUNCEMENT DISABLED LOG
|
||||||
##########################################################################
|
|
||||||
# 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.
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
- if:
|
- if:
|
||||||
- condition: template
|
- 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:
|
then:
|
||||||
- action: script.view_road_announcement
|
- action: script.view_road_announcement
|
||||||
data:
|
data:
|
||||||
@@ -759,5 +923,6 @@ script:
|
|||||||
title: "{{ am_announcement_title }}"
|
title: "{{ am_announcement_title }}"
|
||||||
short_announcement: "{{ am_short_announcement }}"
|
short_announcement: "{{ am_short_announcement }}"
|
||||||
long_announcement: "{{ am_long_announcement }}"
|
long_announcement: "{{ am_long_announcement }}"
|
||||||
payload: ""
|
payload: "{{ am_announcement_payload }}"
|
||||||
indicator: "{{ am_matrix_indicator }}"
|
indicator: "{{ am_matrix_indicator }}"
|
||||||
|
mqtt_feed: "{{ am_mqtt_feed }}"
|
||||||
|
|||||||
@@ -1,49 +1,85 @@
|
|||||||
################################################################################
|
#:########################################################################################:#
|
||||||
# PACKAGE: Appliances Status Monitoring
|
# Appliances Status Monitoring Package #
|
||||||
################################################################################
|
#:########################################################################################:#
|
||||||
#
|
#
|
||||||
# Version: 1.4
|
# FILE:
|
||||||
# Date: 2026-06-09
|
# appliances_status_monitoring.yaml
|
||||||
#
|
#
|
||||||
# Purpose:
|
# VERSION:
|
||||||
|
# V1.5 2026-07-11
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
# Appliance status monitoring package for multiple appliances.
|
# Appliance status monitoring package for multiple appliances.
|
||||||
#
|
#
|
||||||
# This package defines a central appliance registry and one automation that:
|
# 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.
|
# 2. Samples appliance power sensors once per minute.
|
||||||
# 3. Calls script.appliance_monitor_process_sample.
|
# 3. Calls script.appliance_monitor_process_sample.
|
||||||
|
# 4. Passes per-appliance completion announcement settings to the processor.
|
||||||
#
|
#
|
||||||
# Processor package required:
|
# REQUIRED PACKAGES:
|
||||||
# appliance_monitor_processor.yaml
|
# appliance_monitor_processor.yaml V1.3 or newer
|
||||||
#
|
# view_road_announcement_router.yaml V1.8 or newer
|
||||||
# Announcement router required:
|
|
||||||
# view_road_announcement_router.yaml
|
|
||||||
#
|
#
|
||||||
# MQTT:
|
# MQTT:
|
||||||
# MQTT is used only as retained state storage for appliance monitor sensors.
|
# MQTT is used as retained state storage for appliance monitor sensors.
|
||||||
# Sensor entities are created using MQTT discovery.
|
# Sensor entities are created using MQTT Discovery.
|
||||||
#
|
#
|
||||||
# Adding another appliance:
|
# ADDING AN APPLIANCE:
|
||||||
# Add one entry to the am_appliances list.
|
# Add one entry to the am_appliances registry below.
|
||||||
#
|
#
|
||||||
# Version 1.4:
|
# ANNOUNCEMENT ROUTER CHANNELS:
|
||||||
# Added min_cycle_notify_seconds support.
|
# The following channel IDs are accepted by the current announcement router:
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# Version 1.3:
|
# pushover_zorruno
|
||||||
# Removed the is_number gate before calling the processor. The processor is
|
# pushover_kim
|
||||||
# now always called once per appliance per sample, and power is safely converted
|
# sony_tv_lounge
|
||||||
# to 0 if the source sensor is unavailable or non-numeric.
|
# 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:
|
automation:
|
||||||
- id: appliances_status_monitoring_process_samples
|
- id: appliances_status_monitoring_process_samples
|
||||||
alias: 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.
|
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
|
mode: queued
|
||||||
max: 20
|
max: 20
|
||||||
|
|
||||||
@@ -57,17 +93,17 @@ automation:
|
|||||||
id: periodic_sample
|
id: periodic_sample
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
# MQTT LOCAL CONTROLS
|
# MQTT LOCAL CONTROLS #
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
|
|
||||||
mqtt_device_name: "appliance_monitor"
|
mqtt_device_name: "appliance_monitor"
|
||||||
mqtt_local_status_topic: !secret mqtt_status_main_topic
|
mqtt_local_status_topic: !secret mqtt_status_main_topic
|
||||||
mqtt_discovery_prefix: "homeassistant"
|
mqtt_discovery_prefix: "homeassistant"
|
||||||
|
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
# DEFAULTS
|
# PROCESSOR DEFAULTS #
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
|
|
||||||
am_default_debug_flow: false
|
am_default_debug_flow: false
|
||||||
am_default_resolution: 2
|
am_default_resolution: 2
|
||||||
@@ -75,91 +111,184 @@ automation:
|
|||||||
am_default_operating_power_min: 12
|
am_default_operating_power_min: 12
|
||||||
am_default_min_cycle_notify_seconds: 300
|
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
|
# 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
|
# standby_power: 3
|
||||||
# Below this value, the appliance will show "Off".
|
# Below this value, the appliance shows Off.
|
||||||
# Between this value and operating_power_min, it will show "Standby".
|
# Between standby_power and operating_power_min, it shows Standby.
|
||||||
#
|
#
|
||||||
# operating_power_min: 20
|
# 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
|
# min_cycle_notify_seconds: 600
|
||||||
# Finished cycles shorter than this are recorded, but no notification
|
# Minimum cycle duration before a completion announcement is allowed.
|
||||||
# is sent. Default is 300 seconds.
|
|
||||||
#
|
#
|
||||||
# announcement_channels: "pushover_zorruno"
|
# announcement_enabled: false
|
||||||
# Announcement channels for this appliance.
|
# Disables cycle-completion announcements for this appliance.
|
||||||
#
|
#
|
||||||
# long_announcement: "The device has finished. Please empty it when convenient."
|
# announcement_channels: >-
|
||||||
# Override the long announcement.
|
# 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
|
# title: "Washing Machine Notification"
|
||||||
# Minimum time for an appliance to be operating before a completion announcement generated.
|
# 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:
|
am_appliances:
|
||||||
|
#:##################################################################################:#
|
||||||
|
# Washing Machine #
|
||||||
|
#:##################################################################################:#
|
||||||
|
|
||||||
- appliance_id: washing_machine
|
- appliance_id: washing_machine
|
||||||
appliance_name: "Washing Machine"
|
appliance_name: "Washing Machine"
|
||||||
appliance_action: "washing"
|
appliance_action: "washing"
|
||||||
power_entity: "sensor.esp_laundrywashpow_load_power"
|
power_entity: "sensor.esp_laundrywashpow_load_power"
|
||||||
icon: "mdi:washing-machine"
|
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
|
resolution: 5
|
||||||
standby_power: 3
|
standby_power: 3
|
||||||
operating_power_min: 10
|
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_id: dryer
|
||||||
appliance_name: "Dryer"
|
appliance_name: "Dryer"
|
||||||
appliance_action: "drying"
|
appliance_action: "drying"
|
||||||
power_entity: "sensor.esp_laundrydrypow_load_power"
|
power_entity: "sensor.esp_laundrydrypow_load_power"
|
||||||
icon: "mdi:tumble-dryer"
|
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
|
resolution: 4
|
||||||
standby_power: 3
|
standby_power: 3
|
||||||
operating_power_min: 10
|
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_id: main_dishwasher
|
||||||
appliance_name: "Main Dishwasher"
|
appliance_name: "Main Dishwasher"
|
||||||
appliance_action: "dishwashing"
|
appliance_action: "dishwashing"
|
||||||
power_entity: "sensor.esp_maindishwasherpower_power"
|
power_entity: "sensor.esp_maindishwasherpower_power"
|
||||||
icon: "mdi:dishwasher"
|
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
|
resolution: 5
|
||||||
standby_power: 3
|
standby_power: 3
|
||||||
operating_power_min: 30
|
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_id: downstairs_dishwasher
|
||||||
appliance_name: "Downstairs Dishwasher"
|
appliance_name: "Downstairs Dishwasher"
|
||||||
appliance_action: "dishwashing"
|
appliance_action: "dishwashing"
|
||||||
power_entity: "sensor.esp_downstdishwasher_power"
|
power_entity: "sensor.esp_downstdishwasher_power"
|
||||||
icon: "mdi:dishwasher"
|
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
|
resolution: 5
|
||||||
standby_power: 3
|
standby_power: 3
|
||||||
operating_power_min: 30
|
operating_power_min: 30
|
||||||
|
|
||||||
|
announcement_enabled: true
|
||||||
announcement_channels: "pushover_zorruno"
|
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_id: evcharger_32a
|
||||||
appliance_name: "EV Charger (32A)"
|
appliance_name: "EV Charger (32A)"
|
||||||
@@ -167,88 +296,134 @@ automation:
|
|||||||
power_entity: "sensor.main_house_3_phase_power_monitor_32a_wallcharger_power"
|
power_entity: "sensor.main_house_3_phase_power_monitor_32a_wallcharger_power"
|
||||||
monitor_sensor: "sensor.appliance_monitor_ev_charger_32a_monitor"
|
monitor_sensor: "sensor.appliance_monitor_ev_charger_32a_monitor"
|
||||||
icon: "mdi:ev-plug-type2"
|
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
|
resolution: 6
|
||||||
standby_power: 3
|
standby_power: 3
|
||||||
operating_power_min: 100
|
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_id: evcharger_16a
|
||||||
appliance_name: "EV Charger (16A)"
|
appliance_name: "EV Charger (16A)"
|
||||||
appliance_action: "charging"
|
appliance_action: "charging"
|
||||||
power_entity: "sensor.main_house_3_phase_power_monitor_32a_wallcharger_power"
|
power_entity: "sensor.garage_db_control_16a_wallcharger_power"
|
||||||
monitor_sensor: "sensor.garage_db_control_16a_wallcharger_power"
|
monitor_sensor: "sensor.appliance_monitor_ev_charger_16a_monitor"
|
||||||
icon: "mdi:ev-plug-type1"
|
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
|
resolution: 6
|
||||||
standby_power: 3
|
standby_power: 3
|
||||||
operating_power_min: 100
|
operating_power_min: 100
|
||||||
|
|
||||||
|
announcement_enabled: true
|
||||||
announcement_channels: "pushover_zorruno"
|
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_id: ryobicharger_left
|
||||||
appliance_name: "Ryobi Charger Left"
|
appliance_name: "Ryobi Charger Left"
|
||||||
appliance_action: "charging"
|
appliance_action: "charging"
|
||||||
power_entity: "sensor.ryobi_charger_left_power_monitor_x17pp_power"
|
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"
|
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
|
resolution: 4
|
||||||
standby_power: 1
|
standby_power: 1
|
||||||
operating_power_min: 20
|
operating_power_min: 20
|
||||||
|
|
||||||
|
announcement_enabled: true
|
||||||
announcement_channels: "pushover_zorruno"
|
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_id: ryobicharger_right
|
||||||
appliance_name: "Ryobi Charger Right"
|
appliance_name: "Ryobi Charger Right"
|
||||||
appliance_action: "charging"
|
appliance_action: "charging"
|
||||||
power_entity: "sensor.ryobi_charger_right_power_monitor_x16pp_power"
|
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"
|
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
|
resolution: 4
|
||||||
standby_power: 1
|
standby_power: 1
|
||||||
operating_power_min: 20
|
operating_power_min: 20
|
||||||
|
|
||||||
|
announcement_enabled: true
|
||||||
announcement_channels: "pushover_zorruno"
|
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_id: pool_pump
|
||||||
appliance_name: "Pool Pump"
|
appliance_name: "Pool Pump"
|
||||||
appliance_action: "pumping"
|
appliance_action: "pumping"
|
||||||
power_entity: "sensor.esp_poolpumppower_power"
|
power_entity: "sensor.esp_poolpumppower_power"
|
||||||
#monitor_sensor: "sensor.ryobi_charger_right_power_monitor_x16pp_power"
|
|
||||||
icon: "mdi:pool"
|
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
|
resolution: 4
|
||||||
standby_power: 5
|
standby_power: 5
|
||||||
operating_power_min: 20
|
operating_power_min: 20
|
||||||
|
|
||||||
|
announcement_enabled: true
|
||||||
announcement_channels: "pushover_zorruno"
|
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_id: 3d_printer
|
||||||
appliance_name: "3D Printer"
|
appliance_name: "3D Printer"
|
||||||
appliance_action: "printing"
|
appliance_action: "printing"
|
||||||
power_entity: "sensor.esp_3dprinterpow_load_power"
|
power_entity: "sensor.esp_3dprinterpow_load_power"
|
||||||
#monitor_sensor: "sensor.ryobi_charger_right_power_monitor_x16pp_power"
|
|
||||||
icon: "mdi:printer-3d"
|
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
|
resolution: 4
|
||||||
standby_power: 2
|
standby_power: 2
|
||||||
operating_power_min: 20
|
operating_power_min: 20
|
||||||
|
|
||||||
|
announcement_enabled: true
|
||||||
announcement_channels: "pushover_zorruno"
|
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:
|
action:
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
# PUBLISH / REFRESH MQTT DISCOVERY CONFIG
|
# PUBLISH / REFRESH MQTT DISCOVERY CONFIG #
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
|
|
||||||
- repeat:
|
- repeat:
|
||||||
for_each: "{{ am_appliances }}"
|
for_each: "{{ am_appliances }}"
|
||||||
@@ -285,19 +460,18 @@ automation:
|
|||||||
identifiers=[mqtt_device_name],
|
identifiers=[mqtt_device_name],
|
||||||
name='Appliance Monitor',
|
name='Appliance Monitor',
|
||||||
manufacturer='View Road Home Assistant',
|
manufacturer='View Road Home Assistant',
|
||||||
model='Shared appliance power monitor'
|
model='Shared appliance power monitor',
|
||||||
|
sw_version='1.5'
|
||||||
)
|
)
|
||||||
) | to_json
|
) | to_json
|
||||||
}}
|
}}
|
||||||
|
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
# PROCESS SAMPLES
|
# PROCESS SAMPLES #
|
||||||
##########################################################################
|
#:####################################################################################:#
|
||||||
#
|
#
|
||||||
# Always process every appliance. The processor receives power_w as a safe
|
# 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:
|
- repeat:
|
||||||
for_each: "{{ am_appliances }}"
|
for_each: "{{ am_appliances }}"
|
||||||
@@ -309,36 +483,97 @@ automation:
|
|||||||
am_monitor_sensor: >-
|
am_monitor_sensor: >-
|
||||||
{{
|
{{
|
||||||
repeat.item.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: >-
|
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: >-
|
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: >-
|
am_announcement_payload: >-
|
||||||
{{ repeat.item.debug_flow | default(am_default_debug_flow, true) }}
|
{{
|
||||||
|
repeat.item.announcement_payload
|
||||||
|
| default(am_default_announcement_payload, true)
|
||||||
|
}}
|
||||||
|
|
||||||
am_resolution: >-
|
am_mqtt_feed: >-
|
||||||
{{ repeat.item.resolution | default(am_default_resolution, true) }}
|
{{
|
||||||
|
repeat.item.mqtt_feed
|
||||||
|
| default(repeat.item.appliance_id, true)
|
||||||
|
}}
|
||||||
|
|
||||||
am_standby_power: >-
|
am_short_announcement: >-
|
||||||
{{ repeat.item.standby_power | default(am_default_standby_power, true) }}
|
{{
|
||||||
|
repeat.item.short_announcement
|
||||||
|
| default(
|
||||||
|
(repeat.item.appliance_action | title) ~ ' complete',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
|
||||||
am_operating_power_min: >-
|
am_matrix_indicator: >-
|
||||||
{{ repeat.item.operating_power_min | default(am_default_operating_power_min, true) }}
|
{{ repeat.item.matrix_indicator | default('', 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_long_announcement: >-
|
am_long_announcement: >-
|
||||||
{{ repeat.item.long_announcement | default('', true) }}
|
{{ 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
|
- action: script.appliance_monitor_process_sample
|
||||||
data:
|
data:
|
||||||
appliance_id: "{{ repeat.item.appliance_id }}"
|
appliance_id: "{{ repeat.item.appliance_id }}"
|
||||||
@@ -355,8 +590,11 @@ automation:
|
|||||||
operating_power_min: "{{ am_operating_power_min }}"
|
operating_power_min: "{{ am_operating_power_min }}"
|
||||||
min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}"
|
min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}"
|
||||||
|
|
||||||
|
announcement_enabled: "{{ am_announcement_enabled }}"
|
||||||
announcement_channels: "{{ am_announcement_channels }}"
|
announcement_channels: "{{ am_announcement_channels }}"
|
||||||
short_announcement: "{{ repeat.item.short_announcement }}"
|
|
||||||
long_announcement: "{{ am_long_announcement }}"
|
|
||||||
title: "{{ am_title }}"
|
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:
|
# TITLE:
|
||||||
# Jobs Tracker Processor
|
# Jobs Tracker Processor
|
||||||
@@ -9,20 +9,122 @@
|
|||||||
# jobs_tracker_processor.yaml
|
# jobs_tracker_processor.yaml
|
||||||
#
|
#
|
||||||
# VERSION:
|
# VERSION:
|
||||||
# V1.3.1 2026-06-20
|
# V1.6 2026-07-10
|
||||||
#
|
#
|
||||||
# CHANGELOG:
|
# PURPOSE:
|
||||||
# V1.3.1:
|
# 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
|
# - Corrected script field definitions so Home Assistant reliably creates
|
||||||
# script.jobs_tracker_update_job.
|
# 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
|
# - Daily jobs reset at 00:00 but do not become Due until their configured
|
||||||
# due time.
|
# due time.
|
||||||
# - Added "Not Due" state.
|
# - Added Not Due state.
|
||||||
# - Completion is recalculated from retained last_completed_ts after restart.
|
# - Completion is recalculated from retained last_completed_ts after restart.
|
||||||
#
|
#
|
||||||
################################################################################
|
#:########################################################################################:#
|
||||||
|
|
||||||
script:
|
script:
|
||||||
jobs_tracker_update_job:
|
jobs_tracker_update_job:
|
||||||
@@ -59,6 +161,12 @@ script:
|
|||||||
mark_complete:
|
mark_complete:
|
||||||
description: "Set true when a human has completed the job."
|
description: "Set true when a human has completed the job."
|
||||||
example: true
|
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:
|
sequence:
|
||||||
- variables:
|
- variables:
|
||||||
@@ -73,6 +181,17 @@ script:
|
|||||||
now_ts: "{{ as_timestamp(now()) }}"
|
now_ts: "{{ as_timestamp(now()) }}"
|
||||||
today_midnight_ts: "{{ as_timestamp(today_at('00:00')) }}"
|
today_midnight_ts: "{{ as_timestamp(today_at('00:00')) }}"
|
||||||
topic_safe: "{{ mqtt_topic | default('viewroad-status/jobs_tracker/' ~ job_id) }}"
|
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:
|
- variables:
|
||||||
current_period_start_ts: >-
|
current_period_start_ts: >-
|
||||||
@@ -98,6 +217,28 @@ script:
|
|||||||
{{ (today_midnight_ts | float(0)) + 86400 }}
|
{{ (today_midnight_ts | float(0)) + 86400 }}
|
||||||
{% endif %}
|
{% 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:
|
- variables:
|
||||||
current_due_ts: >-
|
current_due_ts: >-
|
||||||
{% if repeat_type_safe == 'daily' %}
|
{% if repeat_type_safe == 'daily' %}
|
||||||
@@ -110,6 +251,19 @@ script:
|
|||||||
{{ (current_period_start_ts | float(0)) + (due_seconds | int) }}
|
{{ (current_period_start_ts | float(0)) + (due_seconds | int) }}
|
||||||
{% endif %}
|
{% 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: >-
|
next_due_ts: >-
|
||||||
{% if repeat_type_safe == 'daily' %}
|
{% if repeat_type_safe == 'daily' %}
|
||||||
{{ (next_period_start_ts | float(0)) + (due_seconds | int) }}
|
{{ (next_period_start_ts | float(0)) + (due_seconds | int) }}
|
||||||
@@ -133,6 +287,60 @@ script:
|
|||||||
120
|
120
|
||||||
{% endif %}
|
{% 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:
|
- variables:
|
||||||
stored_last_completed_ts: >-
|
stored_last_completed_ts: >-
|
||||||
{% set v = state_attr(state_entity, 'last_completed_ts') %}
|
{% set v = state_attr(state_entity, 'last_completed_ts') %}
|
||||||
@@ -141,20 +349,52 @@ script:
|
|||||||
last_completed_ts: >-
|
last_completed_ts: >-
|
||||||
{{ now_ts if mark_complete_bool else stored_last_completed_ts }}
|
{{ now_ts if mark_complete_bool else stored_last_completed_ts }}
|
||||||
|
|
||||||
|
#:####################################################################################:#
|
||||||
|
# Completion and timing state #
|
||||||
|
#:####################################################################################:#
|
||||||
|
|
||||||
- variables:
|
- variables:
|
||||||
complete_bool: >-
|
complete_bool: >-
|
||||||
{{ (last_completed_ts | float(0)) >= (current_period_start_ts | float(0)) }}
|
{{ (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: >-
|
due_started_bool: >-
|
||||||
{% if repeat_type_safe == 'daily' %}
|
|
||||||
{{ (now_ts | float(0)) >= (current_due_ts | float(0)) }}
|
{{ (now_ts | float(0)) >= (current_due_ts | float(0)) }}
|
||||||
{% else %}
|
|
||||||
true
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
overdue_started_bool: >-
|
overdue_started_bool: >-
|
||||||
{{ (now_ts | float(0)) >= ((current_due_ts | float(0)) + ((overdue_after_minutes | int(0)) * 60)) }}
|
{{ (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: >-
|
minutes_since_required: >-
|
||||||
{% if (now_ts | float(0)) <= (current_due_ts | float(0)) %}
|
{% if (now_ts | float(0)) <= (current_due_ts | float(0)) %}
|
||||||
0
|
0
|
||||||
@@ -169,10 +409,16 @@ script:
|
|||||||
{{ (((now_ts | float(0)) - (current_due_ts | float(0))) / 3600) | round(0) | int }}
|
{{ (((now_ts | float(0)) - (current_due_ts | float(0))) / 3600) | round(0) | int }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
#:####################################################################################:#
|
||||||
|
# Display state #
|
||||||
|
#:####################################################################################:#
|
||||||
|
|
||||||
- variables:
|
- variables:
|
||||||
job_status: >-
|
job_status: >-
|
||||||
{% if complete_bool | bool %}
|
{% if complete_bool | bool %}
|
||||||
Complete
|
Complete
|
||||||
|
{% elif missed_bool | bool %}
|
||||||
|
Missed
|
||||||
{% elif not (due_started_bool | bool) %}
|
{% elif not (due_started_bool | bool) %}
|
||||||
Not Due
|
Not Due
|
||||||
{% elif overdue_started_bool | bool %}
|
{% elif overdue_started_bool | bool %}
|
||||||
@@ -265,52 +511,64 @@ script:
|
|||||||
{{ ts | timestamp_custom('%Y-%m-%d @ %H:%M', true) }}
|
{{ ts | timestamp_custom('%Y-%m-%d @ %H:%M', true) }}
|
||||||
{% endif %}
|
{% 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
|
- action: mqtt.publish
|
||||||
data:
|
data:
|
||||||
topic: "{{ topic_safe }}"
|
topic: "{{ topic_safe }}"
|
||||||
retain: true
|
retain: true
|
||||||
qos: 1
|
qos: 1
|
||||||
payload: >-
|
payload: >-
|
||||||
{{
|
|
||||||
{
|
{
|
||||||
"job_id": job_id,
|
"job_id": {{ job_id | tojson }},
|
||||||
"job_name": job_name,
|
"job_name": {{ job_name | tojson }},
|
||||||
"repeat": repeat_type_safe,
|
"repeat": {{ repeat_type_safe | tojson }},
|
||||||
"due_time": due_time_safe,
|
"due_time": {{ due_time_safe | tojson }},
|
||||||
"due_weekday": due_weekday_safe | int,
|
"due_weekday": {{ due_weekday_safe | int }},
|
||||||
"due_day": due_day_safe | int,
|
"due_day": {{ due_day_safe | int }},
|
||||||
|
"state": {{ job_status | trim | tojson }},
|
||||||
"state": job_status | trim,
|
"complete": {{ complete_bool | bool | tojson }},
|
||||||
"complete": complete_bool | bool,
|
"last_completed_ts": {{ last_completed_ts | float(0) }},
|
||||||
|
"last_completed_time": {{ last_completed_time | tojson }},
|
||||||
"last_completed_ts": last_completed_ts | float(0),
|
"last_completed_human": {{ last_completed_human | tojson }},
|
||||||
"last_completed_time": last_completed_time,
|
"previous_period_start_ts": {{ previous_period_start_ts | float(0) }},
|
||||||
"last_completed_human": last_completed_human,
|
"previous_due_ts": {{ previous_due_ts | float(0) }},
|
||||||
|
"previous_completed": {{ previous_completed_bool | bool | tojson }},
|
||||||
"completed_for_period_ts": current_period_start_ts | float(0) if complete_bool | bool else 0,
|
"previous_missed": {{ previous_missed_bool | bool | tojson }},
|
||||||
"completed_for_due_ts": current_period_start_ts | float(0) if complete_bool | bool else 0,
|
"previous_missed_active": {{ previous_missed_active_bool | bool | tojson }},
|
||||||
|
"missed": {{ missed_bool | bool | tojson }},
|
||||||
"current_period_start_ts": current_period_start_ts | float(0),
|
"completion_window_start": {{ completion_window_start_safe | tojson }},
|
||||||
"next_period_start_ts": next_period_start_ts | float(0),
|
"completion_window_end": {{ completion_window_end_safe | tojson }},
|
||||||
|
"has_completion_window": {{ has_completion_window | bool | tojson }},
|
||||||
"current_due_ts": current_due_ts | float(0),
|
"completion_window_missed": {{ completion_window_missed_bool | bool | tojson }},
|
||||||
"next_due_ts": next_due_ts | float(0),
|
"current_completion_window_start_ts": {{ current_completion_window_start_ts | float(0) }},
|
||||||
"display_due_ts": display_due_ts | float(0),
|
"current_completion_window_end_ts": {{ current_completion_window_end_ts | float(0) }},
|
||||||
"next_due_human": next_due_human,
|
"completed_for_period_ts": {{ completed_for_period_ts_safe | float(0) }},
|
||||||
|
"completed_for_due_ts": {{ completed_for_period_ts_safe | float(0) }},
|
||||||
"due_started": due_started_bool | bool,
|
"current_period_start_ts": {{ current_period_start_ts | float(0) }},
|
||||||
"overdue_started": overdue_started_bool | bool,
|
"next_period_start_ts": {{ next_period_start_ts | float(0) }},
|
||||||
|
"current_due_ts": {{ current_due_ts | float(0) }},
|
||||||
"minutes_since_required": minutes_since_required | int(0),
|
"next_due_ts": {{ next_due_ts | float(0) }},
|
||||||
"hours_since_required": hours_since_required | int(0),
|
"display_due_ts": {{ display_due_ts | float(0) }},
|
||||||
|
"next_due_human": {{ next_due_human | tojson }},
|
||||||
"overdue_after_minutes": overdue_after_minutes | int(0),
|
"due_started": {{ due_started_bool | bool | tojson }},
|
||||||
"overdue_minutes": overdue_minutes | int(0),
|
"overdue_started": {{ overdue_started_bool | bool | tojson }},
|
||||||
"overdue_hours": overdue_hours | int(0),
|
"minutes_since_required": {{ minutes_since_required | int(0) }},
|
||||||
|
"hours_since_required": {{ hours_since_required | int(0) }},
|
||||||
"hours_since_completed": hours_since_completed,
|
"overdue_after_minutes": {{ overdue_after_minutes | int(0) }},
|
||||||
|
"overdue_minutes": {{ overdue_minutes | int(0) }},
|
||||||
"last_updated_local": (now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true),
|
"overdue_hours": {{ overdue_hours | int(0) }},
|
||||||
"mqtt_topic": topic_safe
|
"hours_since_completed": {{ hours_since_completed | tojson }},
|
||||||
} | tojson
|
"last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }},
|
||||||
}}
|
"mqtt_topic": {{ topic_safe | tojson }}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ mqtt:
|
|||||||
name: "Dishwasher Finished"
|
name: "Dishwasher Finished"
|
||||||
state_topic: "viewroad-status/activityfeed/Dishwasher_complete"
|
state_topic: "viewroad-status/activityfeed/Dishwasher_complete"
|
||||||
icon: mdi:dishwasher
|
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
|
- unique_id: 16Acharger_finished
|
||||||
name: "Leaf Done"
|
name: "Leaf Done"
|
||||||
state_topic: "viewroad-status/activityfeed/Leaf_Charger_complete"
|
state_topic: "viewroad-status/activityfeed/Leaf_Charger_complete"
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ automation:
|
|||||||
|
|
||||||
variables:
|
variables:
|
||||||
bedtime_delayed_off_targets:
|
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
|
- entity_id: switch.esp_mainkitchenlights_relay_1_main_lights
|
||||||
delay_seconds: 4
|
delay_seconds: 4
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
# PACKAGE: View Road Announcement Router
|
# 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:
|
# 1.7:
|
||||||
# - Updated Lounge Google Home Voice to speak short_announcement only.
|
# - Updated Lounge Google Home Voice to speak short_announcement only.
|
||||||
# - This allows long_announcement to remain available for detailed channels
|
# - This allows long_announcement to remain available for detailed channels
|
||||||
@@ -14,7 +19,7 @@
|
|||||||
# Central announcement router for general Home Assistant announcements.
|
# Central announcement router for general Home Assistant announcements.
|
||||||
# Other scripts and automations can call script.view_road_announcement and pass
|
# Other scripts and automations can call script.view_road_announcement and pass
|
||||||
# values such as channels, title, short_announcement, long_announcement,
|
# values such as channels, title, short_announcement, long_announcement,
|
||||||
# payload, and indicator.
|
# payload, indicator, and mqtt_feed.
|
||||||
#
|
#
|
||||||
# How to call this script:
|
# How to call this script:
|
||||||
#
|
#
|
||||||
@@ -62,7 +67,13 @@
|
|||||||
# Blocked by default when input_boolean.quiet_time is on.
|
# Blocked by default when input_boolean.quiet_time is on.
|
||||||
#
|
#
|
||||||
# mqtt_view_rd_feed:
|
# 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:
|
# led_matrix_1:
|
||||||
# Uses:
|
# Uses:
|
||||||
@@ -285,6 +296,13 @@ script:
|
|||||||
selector:
|
selector:
|
||||||
text:
|
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:
|
sequence:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# PACKAGE SETTINGS / SUBSTITUTIONS
|
# PACKAGE SETTINGS / SUBSTITUTIONS
|
||||||
@@ -305,6 +323,7 @@ script:
|
|||||||
default_long_announcement: "Home automation announcement"
|
default_long_announcement: "Home automation announcement"
|
||||||
default_payload: ""
|
default_payload: ""
|
||||||
default_indicator: ""
|
default_indicator: ""
|
||||||
|
default_mqtt_feed: ""
|
||||||
|
|
||||||
quiet_time_blocked_channels:
|
quiet_time_blocked_channels:
|
||||||
- "google_home_voice"
|
- "google_home_voice"
|
||||||
@@ -322,6 +341,11 @@ script:
|
|||||||
led_matrix_mqtt_qos: 0
|
led_matrix_mqtt_qos: 0
|
||||||
led_matrix_mqtt_retain: false
|
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
|
# RESOLVE PASSED VALUES
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@@ -333,6 +357,7 @@ script:
|
|||||||
# long_announcement
|
# long_announcement
|
||||||
# payload
|
# payload
|
||||||
# indicator
|
# indicator
|
||||||
|
# mqtt_feed
|
||||||
#
|
#
|
||||||
# Some uppercase variants are also accepted for tolerance.
|
# Some uppercase variants are also accepted for tolerance.
|
||||||
#
|
#
|
||||||
@@ -395,6 +420,17 @@ script:
|
|||||||
{{ default_indicator }}
|
{{ default_indicator }}
|
||||||
{% endif %}
|
{% 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
|
# NORMALISE CHANNEL LISTS AND MESSAGE TEXT
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@@ -406,6 +442,15 @@ script:
|
|||||||
ann_quiet_time_blocked_channels_normalised: >-
|
ann_quiet_time_blocked_channels_normalised: >-
|
||||||
{{ ',' ~ (quiet_time_blocked_channels | join(',') | lower | replace(' ', '') | replace('\n', '') | trim(',')) ~ ',' }}
|
{{ ',' ~ (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: >-
|
ann_short_announcement_raw: >-
|
||||||
{% if short_announcement is defined %}
|
{% if short_announcement is defined %}
|
||||||
{{ short_announcement }}
|
{{ short_announcement }}
|
||||||
@@ -575,10 +620,43 @@ script:
|
|||||||
# CHANNEL: MQTT VIEW RD FEED
|
# 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
|
# CHANNEL: LOUNGE LED MATRIX DISPLAY - MATRIX 1
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
Reference in New Issue
Block a user