Compare commits

..

106 Commits

Author SHA1 Message Date
zorruno d8bcf05bf6 various packages 2026-07-20 19:24:35 +12:00
ESPHome Device Builder afcbf1a94b Edit esp-reterminal-e1001.yaml 2026-07-20 17:17:52 +12:00
ESPHome Device Builder 027af09e40 Edit esp-reterminal-e1001.yaml 2026-07-20 16:27:51 +12:00
ESPHome Device Builder 90a6469161 Edit esp-reterminal-e1001.yaml 2026-07-20 15:58:44 +12:00
ESPHome Device Builder bffcda38d9 Edit esp-reterminal-e1001.yaml 2026-07-20 15:17:25 +12:00
ESPHome Device Builder cb95ceacea Edit esp-reterminal-e1001.yaml 2026-07-20 15:12:31 +12:00
ESPHome Device Builder 941334ecfd Add esp-reterminal-e1001.yaml 2026-07-20 15:10:47 +12:00
ESPHome Device Builder a8b0990000 Edit esp-bedside-panel.yaml 2026-07-20 15:05:51 +12:00
ESPHome Device Builder 809f9ada66 Edit esp-bedside-panel.yaml 2026-07-20 14:46:50 +12:00
ESPHome Device Builder 14d65a8cf1 Edit esp-bedside-panel.yaml 2026-07-20 14:20:39 +12:00
ESPHome Device Builder 9afee6e638 Edit esp-bedside-panel.yaml 2026-07-20 12:50:34 +12:00
ESPHome Device Builder fd2fe422d0 Edit esp-bedside-panel.yaml 2026-07-20 12:30:38 +12:00
ESPHome Device Builder f07cd6d0c4 Edit esp-bedside-panel.yaml 2026-07-20 12:19:25 +12:00
ESPHome Device Builder ea604b8a30 Edit esp-bedside-panel.yaml 2026-07-20 12:01:52 +12:00
ESPHome Device Builder 241a4954bd Edit esp-bedside-panel.yaml 2026-07-19 23:15:43 +12:00
ESPHome Device Builder b2876e937a Edit esp-bedside-panel.yaml 2026-07-19 22:47:41 +12:00
ESPHome Device Builder 0dd442f34a Edit esp-bedside-panel.yaml 2026-07-19 22:07:43 +12:00
ESPHome Device Builder 67a8ed7436 Edit esp-bedside-panel.yaml 2026-07-19 21:45:20 +12:00
ESPHome Device Builder f92ffa3eab Edit esp-bedside-panel.yaml 2026-07-19 21:33:10 +12:00
ESPHome Device Builder b893249959 Edit esp-bedside-panel.yaml 2026-07-19 21:14:31 +12:00
ESPHome Device Builder c658792f5d Edit esp-bedside-panel.yaml 2026-07-19 20:47:52 +12:00
ESPHome Device Builder ca60f73b9d Edit esp-bedside-panel.yaml 2026-07-19 00:01:55 +12:00
ESPHome Device Builder 5747968e96 Edit esp-bedside-panel.yaml 2026-07-18 23:47:46 +12:00
ESPHome Device Builder 3938515564 Edit esp-bedside-panel.yaml 2026-07-18 23:11:05 +12:00
ESPHome Device Builder 526fd23567 Edit esp-bedside-panel.yaml 2026-07-18 22:46:54 +12:00
ESPHome Device Builder c8e9992c32 Edit esp-bedside-panel.yaml 2026-07-18 22:18:21 +12:00
ESPHome Device Builder 5e731f791e Edit esp-bedside-panel.yaml 2026-07-18 21:50:31 +12:00
ESPHome Device Builder 4bc8c308a6 Edit esp-bedside-panel.yaml 2026-07-18 21:35:40 +12:00
ESPHome Device Builder dcf28a3c8f Edit esp-bedside-panel.yaml 2026-07-18 21:27:21 +12:00
ESPHome Device Builder 9bcc9b9b4d Edit esp-bedside-panel.yaml 2026-07-18 21:14:53 +12:00
ESPHome Device Builder a95036e0c5 Edit esp-bedside-panel.yaml 2026-07-18 20:44:27 +12:00
ESPHome Device Builder 347e20f552 Edit esp-bedside-panel.yaml 2026-07-18 20:23:54 +12:00
ESPHome Device Builder ec195d33ce Edit esp-bedside-panel.yaml 2026-07-18 19:58:48 +12:00
ESPHome Device Builder d0df25acf3 Edit esp-bedside-panel.yaml 2026-07-18 19:22:39 +12:00
ESPHome Device Builder 1dbe748e19 Edit esp-bedside-panel.yaml 2026-07-18 19:01:36 +12:00
ESPHome Device Builder 3dcaa1b06b Edit esp-bedside-panel.yaml 2026-07-18 00:23:12 +12:00
ESPHome Device Builder d324ec568b Edit esp-bedside-panel.yaml 2026-07-18 00:11:51 +12:00
ESPHome Device Builder 97669cb664 Edit esp-bedside-panel.yaml 2026-07-18 00:03:22 +12:00
ESPHome Device Builder a81f56a261 Edit esp-bedside-panel.yaml 2026-07-17 23:47:31 +12:00
ESPHome Device Builder 3cc5a757bc Edit esp-bedside-panel.yaml 2026-07-17 23:35:13 +12:00
ESPHome Device Builder 0794008667 Edit esp-bedside-panel.yaml 2026-07-17 23:18:32 +12:00
ESPHome Device Builder cafc856a3c Edit esp-bedside-panel.yaml 2026-07-17 23:03:40 +12:00
ESPHome Device Builder 38281df1dd Edit esp-bedside-panel.yaml 2026-07-17 22:50:45 +12:00
ESPHome Device Builder c197172608 Edit esp-bedside-panel.yaml 2026-07-17 22:46:34 +12:00
ESPHome Device Builder 034c87317f Edit esp-bedside-panel.yaml 2026-07-17 22:17:32 +12:00
ESPHome Device Builder 824fd3fac6 Edit esp-bedside-panel.yaml 2026-07-17 21:54:17 +12:00
ESPHome Device Builder 41836a3dee Edit esp-bedside-panel.yaml 2026-07-17 11:42:50 +12:00
ESPHome Device Builder 45f1f0540a Edit esp-bedside-panel.yaml 2026-07-17 11:29:52 +12:00
ESPHome Device Builder c374e32ded Edit esp-bedside-panel.yaml 2026-07-16 23:11:55 +12:00
ESPHome Device Builder 3dc72898c9 Edit esp-bedside-panel.yaml 2026-07-16 22:54:59 +12:00
ESPHome Device Builder 324cefef4c Edit esp-bedside-panel.yaml 2026-07-16 22:42:55 +12:00
ESPHome Device Builder 07048708a9 Edit esp-bedside-panel.yaml 2026-07-16 22:27:27 +12:00
ESPHome Device Builder 355b3166f7 Edit esp-bedside-panel.yaml 2026-07-16 22:18:19 +12:00
ESPHome Device Builder 8b31b780b0 Edit esp-bedside-panel.yaml 2026-07-16 22:03:57 +12:00
ESPHome Device Builder ee08838c1f Edit esp-bedside-panel.yaml 2026-07-16 21:49:11 +12:00
ESPHome Device Builder aa4b632916 Edit esp-bedside-panel.yaml 2026-07-16 21:38:34 +12:00
ESPHome Device Builder 5be9125eec Edit esp-bedside-panel.yaml 2026-07-16 21:23:23 +12:00
ESPHome Device Builder 5f540de6af Edit esp-bedside-panel.yaml 2026-07-16 21:00:16 +12:00
ESPHome Device Builder 95835550de Edit esp-bedside-panel.yaml 2026-07-16 20:48:57 +12:00
ESPHome Device Builder 20a40b2b3f Edit esp-bedside-panel.yaml 2026-07-16 20:26:16 +12:00
ESPHome Device Builder cf857f322f Edit esp-bedside-panel.yaml 2026-07-16 20:16:10 +12:00
ESPHome Device Builder 497cf553ad Edit esp-bedside-panel.yaml 2026-07-16 19:57:28 +12:00
ESPHome Device Builder caa4d906c4 Edit esp-bedside-panel.yaml 2026-07-16 19:54:10 +12:00
ESPHome Device Builder b8681f7741 Edit esp-bedside-panel.yaml 2026-07-16 19:25:20 +12:00
ESPHome Device Builder 5403d27f10 Edit esp-bedside-panel.yaml 2026-07-16 19:07:51 +12:00
ESPHome Device Builder 54f4da6c18 Edit esp-bedside-panel.yaml 2026-07-16 18:53:31 +12:00
ESPHome Device Builder 19ba5a0c8c Edit esp-bedside-panel.yaml 2026-07-16 18:36:54 +12:00
ESPHome Device Builder 6ae711d922 Edit esp-bedside-panel.yaml 2026-07-16 18:23:14 +12:00
ESPHome Device Builder e90b6b2b8c Edit esp-bedside-panel.yaml 2026-07-16 18:15:36 +12:00
ESPHome Device Builder 24bb66893a Edit esp-bedside-panel.yaml 2026-07-16 18:03:48 +12:00
ESPHome Device Builder 905a16236b Edit esp-bedside-panel.yaml 2026-07-16 17:58:24 +12:00
ESPHome Device Builder cc720cfd00 Edit esp-bedside-panel.yaml 2026-07-15 21:08:18 +12:00
ESPHome Device Builder 34a84d0d0c Edit esp-bedside-panel.yaml 2026-07-15 21:01:53 +12:00
ESPHome Device Builder 5855dbb5a9 Edit esp-bedside-panel.yaml 2026-07-15 20:55:38 +12:00
ESPHome Device Builder 2a8c9584d7 Edit esp-bedside-panel.yaml 2026-07-15 20:41:49 +12:00
ESPHome Device Builder bb97b2ac12 Edit esp-bedside-panel.yaml 2026-07-15 20:35:28 +12:00
ESPHome Device Builder 0ca6fefd08 Edit esp-bedside-panel.yaml 2026-07-15 20:28:10 +12:00
ESPHome Device Builder ea7eedff93 Edit esp-bedside-panel.yaml 2026-07-15 20:16:38 +12:00
ESPHome Device Builder 79054490d0 Edit esp-bedside-panel.yaml 2026-07-15 20:06:42 +12:00
ESPHome Device Builder d27ba8bd14 Edit esp-bedside-panel.yaml 2026-07-14 22:56:17 +12:00
ESPHome Device Builder 63209b5364 Edit esp-bedside-panel.yaml 2026-07-14 22:54:48 +12:00
ESPHome Device Builder 1b9552e328 Edit esp-bedside-panel.yaml 2026-07-14 22:50:56 +12:00
ESPHome Device Builder ae566427c2 Edit esp-bedside-panel.yaml 2026-07-14 22:39:41 +12:00
ESPHome Device Builder f63ff4bfe2 Edit esp-bedside-panel.yaml 2026-07-14 22:36:27 +12:00
ESPHome Device Builder 7bd3e3a0c2 Edit esp-bedside-panel.yaml 2026-07-14 22:34:05 +12:00
ESPHome Device Builder 06a482b43f Edit esp-bedside-panel.yaml 2026-07-14 22:15:13 +12:00
ESPHome Device Builder e013bab5c9 Edit esp-bedside-panel.yaml 2026-07-14 22:06:30 +12:00
ESPHome Device Builder 4fab9755ec Edit esp-bedside-panel.yaml 2026-07-14 22:02:46 +12:00
ESPHome Device Builder 769cb34eb6 Edit esp-bedside-panel.yaml 2026-07-14 21:51:48 +12:00
ESPHome Device Builder ce7a456d1f Edit esp-bedside-panel.yaml 2026-07-14 21:42:57 +12:00
ESPHome Device Builder e586ff8f28 Edit esp-bedside-panel.yaml 2026-07-14 21:19:44 +12:00
ESPHome Device Builder f571919352 Edit esp-bedside-panel.yaml 2026-07-14 20:58:49 +12:00
ESPHome Device Builder 2e9a37a9b9 Add esp-bedside-panel.yaml 2026-07-14 20:56:47 +12:00
ESPHome Device Builder d45474036c Edit esp-mainpowermonitor.yaml 2026-07-12 13:36:54 +12:00
ESPHome Device Builder 5c5256caa9 Edit esp-3dprinterpow.yaml 2026-07-12 13:05:32 +12:00
ESPHome Device Builder 2a5725b16d Edit esp-laundrywashpow.yaml 2026-07-12 12:56:38 +12:00
ESPHome Device Builder 706844e6e8 Edit esp-laundrydrypow.yaml 2026-07-12 12:12:18 +12:00
ESPHome Device Builder 2b2b8edc37 Edit esp-laundrydrypow.yaml 2026-07-12 11:52:15 +12:00
ESPHome Device Builder c11728dba6 Edit esp-3dprinterpow.yaml 2026-07-11 23:37:03 +12:00
ESPHome Device Builder 0377bf0b08 Edit esp-espnow_test_transmitter1.yaml 2026-07-11 18:19:37 +12:00
ESPHome Device Builder 893ed9dcfc Edit esp-espnow_test_receiver.yaml 2026-07-11 17:58:38 +12:00
ESPHome Device Builder 1d00da6514 Edit esp-espnow_test_receiver.yaml 2026-07-11 17:57:51 +12:00
ESPHome Device Builder 1aa5985c2c Edit esp-espnow_test_receiver.yaml 2026-07-11 17:55:33 +12:00
ESPHome Device Builder 97efa38777 Edit esp-espnow_test_transmitter1.yaml 2026-07-11 17:45:18 +12:00
ESPHome Device Builder cd39558b1f Add esp-espnow_test_transmitter1.yaml 2026-07-11 17:42:25 +12:00
ESPHome Device Builder 5b7a9d39d2 Add esp-espnow_test_receiver.yaml 2026-07-11 17:18:06 +12:00
23 changed files with 19808 additions and 5396 deletions
+1 -1
View File
@@ -1 +1 @@
{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783321380.267702} {"pid": 71, "version": 1, "ha_version": "2026.7.2", "start_ts": 1784512870.0398877}
+43
View File
@@ -0,0 +1,43 @@
# =============================================================================
# Prettier Configuration for Home Assistant YAML
# Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/
# =============================================================================
#
# Prettier auto-formats YAML files written by OpenCode. This config aligns
# with the official Home Assistant YAML Style Guide where possible.
#
# WHAT PRETTIER ENFORCES:
# - 2-space indentation (no tabs)
# - Double quotes for strings (HA convention)
# - Trailing newline at end of file
# - Consistent spacing and formatting
# - Preserves HA custom tags (!include, !secret, !input, !env_var)
# - Preserves block style sequences and mappings
# - Preserves multiline strings (literal | and folded > styles)
# - Preserves comments
#
# WHAT PRETTIER DOES NOT ENFORCE (must be handled manually or by the AI):
# - Converting flow style [1, 2, 3] to block style sequences
# - Converting flow style { key: val } to block style mappings
# - Normalizing null/~ to implicit null (just "key:" with no value)
# - Normalizing truthy booleans (Yes/On/TRUE) to lowercase true/false
# - Wrapping long lines (templates, etc.)
#
# Users can customize this file. See: https://prettier.io/docs/en/options
# =============================================================================
# HA YAML Style Guide: "An indentation of 2 spaces must be used"
tabWidth: 2
useTabs: false
# HA YAML Style Guide: "Strings are preferably quoted with double quotes"
singleQuote: false
# Keep printWidth generous to avoid prettier breaking long YAML keys into
# explicit key notation (?). HA templates can be long but should be manually
# split using literal (|) or folded (>) block scalars per the style guide.
printWidth: 120
# Preserve multiline string formatting (literal |, folded >, etc.)
# HA YAML Style Guide: "make use of the literal style and folded style strings"
proseWrap: "preserve"
+807
View File
@@ -0,0 +1,807 @@
# Home Assistant OpenCode Rules
You are working directly within a Home Assistant installation. Your working directory is `/homeassistant`, which is the live Home Assistant configuration directory.
## CRITICAL: User Consent and Scope Rules
You MUST follow these rules strictly:
1. **Never exceed the user's request** - Do exactly what the user asks, nothing more. Do not "improve" or "enhance" beyond the stated scope.
2. **Never make changes without explicit approval** - Before modifying ANY file:
- Show the user exactly what you plan to change
- Wait for their explicit confirmation ("yes", "go ahead", "do it", etc.)
- If they haven't approved, DO NOT proceed
3. **Ask, don't assume** - If the user's request is ambiguous:
- Ask clarifying questions first
- Present options and let them choose
- Never guess at their intent
4. **Read-only by default** - When investigating or troubleshooting:
- Only read files and gather information
- Present findings and recommendations
- Wait for user instruction before making any changes
5. **One change at a time** - When making approved changes:
- Make the minimum change needed
- Show what was changed
- Let the user verify before proceeding to any next step
6. **No unsolicited modifications** - Never:
- "Clean up" code the user didn't ask about
- Add features they didn't request
- Refactor working configurations
- Fix issues they haven't mentioned
7. **Respect "no"** - If a user declines a suggestion, do not:
- Repeat the suggestion
- Make the change anyway
- Try to convince them otherwise
## Environment Context
- You are running inside the OpenCode app
- The current directory (`/homeassistant`) contains the live Home Assistant configuration
- Changes to YAML files here directly affect the Home Assistant instance
- If add-on folder access is enabled, `/addons` and `/addon_configs` are available for Home Assistant add-on development. Treat `/addon_configs` as sensitive and only inspect or modify these folders when the user explicitly asks.
- You may have access to MCP tools for interacting with Home Assistant (check with the user)
## Home Assistant Interaction Model
There are three primary, safe ways to interact with Home Assistant:
### 1. Configuration Files (YAML)
The standard way to define and customize Home Assistant behavior:
- Automations, scripts, scenes, and blueprints
- Integration and sensor configurations
- Templates, packages, and customizations
- Dashboard (Lovelace) definitions
These files are designed for user editing and are the source of truth for your Home Assistant setup.
### 2. MCP Tools (Runtime API)
Real-time interaction with the running Home Assistant instance:
- Query current entity states and history
- Control devices and call services
- Validate configurations
- Diagnose issues and detect anomalies
- Report OpenCode/HA agent capability status with `get_agent_capabilities`
### Native Home Assistant LLM Platform
Home Assistant is developing a native `llm` integration where Core integrations and custom integrations can expose curated tools through `<integration>/llm.py` and registered LLM APIs. New Home Assistant builds may also expose those APIs over native MCP endpoints such as `/api/mcp/<API ID>`; the built-in Assist API uses `/api/mcp/assist`. This is complementary to OpenCode MCP, not a replacement.
- If the optional `homeassistant_native` MCP server is available, prefer it for requests that fit the configured native Home Assistant LLM API because those tools are curated by Home Assistant.
- Use OpenCode MCP for configuration editing, safe writes, validation, admin/dev workflows, screenshots, updates, ESPHome, `hab`, Zigbee tasks, add-on development, and Home Assistant documentation lookup.
- Use `get_agent_capabilities` or `ha://agent/capabilities` to check whether the running HA instance reports the native `llm` component and native MCP endpoints.
- Use `get_home_context` for compact area/domain/entity understanding before broad state dumps.
- Use `get_ha_llm_development_guide` when helping develop or review a custom integration's native `<integration>/llm.py` provider.
- Do not assume this add-on can register tools directly with HA's native `llm` platform; native tool registration is internal to HA integrations/custom integrations. The add-on can consume configured native LLM APIs through native MCP when Home Assistant exposes them.
### 3. hab CLI (Home Assistant Builder)
A CLI tool designed for AI agents to manage Home Assistant. Run `hab` commands via the terminal:
- **Entity management**: `hab entity list`, `hab entity get light.living_room`, `hab entity logbook sensor.power --start 2h`
- **Service calls**: `hab action call light.turn_on --entity light.living_room --data '{"brightness": 200}'`
- **Automation CRUD**: `hab automation list`, `hab automation create`, `hab automation delete`
- **Dashboard management**: `hab dashboard list`, `hab dashboard view create`
- **Area/floor/zone/label**: `hab area list`, `hab area create "Kitchen"`
- **Helpers**: `hab helper list`, `hab helper create`
- **Scripts**: `hab script list`, `hab script create`
- **Scenes**: `hab scene list`, `hab scene create`, `hab scene activate "Movie Time"`
- **Blueprints**: `hab blueprint list`
- **Backups**: `hab backup list`, `hab backup create`
- **System**: `hab system info`, `hab system health`, `hab overview`
- **Devices**: `hab device list`
- **People**: `hab person list`, `hab person create`, `hab person update`
- **Categories**: `hab category list`, `hab category assign --entity light.kitchen`
- **To-do lists**: `hab todo list`, `hab todo item list todo.shopping`, `hab todo item add todo.shopping "Milk"`
- **Notifications**: `hab notification list`, `hab notification create --message "Hello" --title "Alert"`
- **Integrations**: `hab integration list`, `hab integration reload hue`, `hab integration disable mqtt`
- **Repairs**: `hab repairs list`, `hab repairs ignore <issue_id>`
- **Events**: `hab event list`, `hab event fire my_custom_event --data '{"key": "value"}'`
- **Templates**: `hab template render --expression "{{ states('sensor.temperature') }}"`
- **Search**: `hab search related`
`hab` outputs human-readable text by default. Use `--json` for structured JSON output (ideal for parsing).
`hab` is pre-authenticated via the Supervisor token - no login required.
Run `hab --help` or `hab <command> --help` for full usage details.
<!-- HAB_LIVE_HELP_START -->
```
Home Assistant Builder (hab) is a CLI utility designed for LLMs
to build and manage Home Assistant configurations.
Interactive sessions default to human-readable text. Non-interactive sessions default to JSON.
Start with 'hab guide' for workflow-level guidance optimized for LLM and agent usage.
Usage:
hab [command]
Getting Started:
auth Manage authentication
capability Inspect runtime capabilities
guide Display built-in usage guides
overview Show an overview of the Home Assistant instance
schema Show machine-readable command schema
Registry:
area Manage areas
device Manage devices
entity Manage entities
floor Manage floors
label Manage labels
person Manage persons
search Search for items and relationships
zone Manage zones
Automation:
action Call actions (services)
automation Manage automations
blueprint Manage blueprints
category Manage categories
helper Manage groups, templates, and other helpers
scene Manage scenes
script Manage scripts
Dashboard:
dashboard Manage dashboards
Other:
backup Manage backups
calendar Manage calendar events
diagnostics Manage diagnostics handlers
energy Manage energy dashboard settings
esphome Manage ESPHome devices
event Manage Home Assistant events
integration Manage integrations
network Manage network settings
notification Manage persistent notifications
repairs Manage Home Assistant repairs
system Manage system
template Work with Home Assistant templates
thread Manage Thread credentials
todo Manage to-do list items
update Update hab to the latest version
version Show version information
Additional Commands:
help Help about any command
Flags:
--config string Path to config directory (default: ~/.config/home-assistant-builder)
-h, --help help for hab
--json Use JSON output instead of human-readable text
--skip-update-check Skip automatic update check on startup
--text Use human-readable text output
--verbose Show verbose output
Use "hab [command] --help" for more information about a command.
```
<!-- HAB_LIVE_HELP_END -->
**Use configuration files when:** defining behavior, creating automations, setting up integrations
**Use MCP tools when:** checking current state, safe config writing, anomaly detection, entity diagnostics
**Use hab CLI when:** managing dashboards, areas, helpers, backups, blueprints, and bulk admin operations
### 4. zigporter CLI (Zigbee Toolkit)
A CLI for Zigbee device management in Home Assistant. Handles cascade renames (updating entity IDs across automations, scripts, scenes, and all Lovelace dashboards atomically), device inspection, stale device cleanup, and Zigbee mesh visualization.
**Cascade rename** — zigporter's unique value: when you rename an entity or device, it automatically patches every reference in automations, scripts, scenes, and Lovelace dashboards. `hab` can rename a single entity/device but does NOT cascade to references.
Key commands:
- **Cascade rename**: `zigporter rename-entity light.old_id light.new_id --apply`, `zigporter rename-device "Old Name" "New Name" --apply`
- **Device inventory**: `zigporter list-devices --json`, `zigporter list-z2m --json` (requires Z2M config)
- **Device inspection**: `zigporter inspect "Device Name" --json`, `zigporter inspect sensor.entity_id --json`
- **Stale device management**: `zigporter stale "Device" --action remove`, `zigporter stale "Device" --action ignore`
- **Post-migration cleanup**: `zigporter fix-device "Device" --apply`
- **Connectivity check**: `zigporter check`
- **ZHA export**: `zigporter export --output devices.json`
- **Mesh visualization**: `zigporter network-map --format table` (terminal), `zigporter network-map --output mesh.svg` (SVG file)
**Output format**: Use `--json` on listing/inspect commands for structured output (ideal for AI parsing). Rename commands output diffs and confirmation text.
zigporter is pre-authenticated via the Supervisor token. Z2M commands (`list-z2m`, `network-map --backend z2m`) require Z2M URL configuration in the add-on settings.
**Important limitations**:
- `rename-entity` / `rename-device` do NOT patch Jinja2 template expressions (e.g. `{{ states('old.id') }}`). A warning is printed listing affected files — inform the user these need manual review after renaming.
- The `migrate` command is inherently interactive (requires physical device actions) and must NOT be used by AI agents.
- Dry-run is the default for renames — always preview before using `--apply`.
<!-- ZIGPORTER_LIVE_HELP_START -->
```
Usage: zigporter [OPTIONS] COMMAND [ARGS]...
Migrate Zigbee devices between ZHA and Zigbee2MQTT. Supports both ZHA → Z2M
(default) and Z2M → ZHA (--direction z2m-to-zha).
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --version -v Show version and exit. │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to │
│ copy it or customize the installation. │
│ --help -h Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ setup Create or update the configuration file in the zigporter │
│ config directory. │
│ check Verify that all requirements are in place before migrating. │
│ export Export current ZHA devices, entities, areas, and automation │
│ references to JSON. │
│ export-z2m Export current Z2M devices, entities, areas, and automation │
│ references to JSON. │
│ list-z2m List all devices currently paired with Zigbee2MQTT. │
│ list-devices List all Home Assistant devices across all integrations. │
│ migrate Interactive wizard to migrate devices between ZHA and │
│ Zigbee2MQTT. │
│ inspect Show all automations, scripts, scenes, and dashboard cards │
│ that depend on a device. │
│ rename-entity Rename an entity ID and update all references in automations, │
│ scripts, scenes, and dashboards. │
│ rename-device Rename a device and cascade the change to all its entities, │
│ automations, scripts, scenes, and dashboards. │
│ stale Identify and manage offline/stale devices across all │
│ integrations. │
│ fix-device Remove stale ZHA device entries left behind after migration │
│ to Zigbee2MQTT. │
│ network-map Show Zigbee mesh topology with signal strength (LQI) for each │
│ device. │
╰──────────────────────────────────────────────────────────────────────────────╯
```
<!-- ZIGPORTER_LIVE_HELP_END -->
**Use zigporter CLI when:** renaming entities/devices with cascade updates, inspecting Zigbee devices across integrations, cleaning up stale or post-migration devices, visualizing the Zigbee mesh
### 5. Internal Directories (OFF-LIMITS)
Home Assistant manages internal state in directories like `.storage/`. These are:
- Not designed for direct access
- Subject to change without notice
- Potentially dangerous to modify
**Never access internal directories directly - use configuration files or MCP tools instead.**
## RESTRICTED: Internal Home Assistant Directories
**NEVER read, modify, or directly interact with these internal directories:**
| Directory | Contains | Use Instead |
|-----------|----------|-------------|
| `.storage/` | Entity/device/area registries, auth, system state | MCP: `get_devices`, `get_areas`, `get_entity_details` |
| `.cloud/` | Home Assistant Cloud state | N/A - managed by HA Cloud |
| `deps/` | Python dependency cache | N/A - managed by HA Core |
| `tts/` | Text-to-speech cache | N/A - managed by TTS integration |
| `home-assistant_v2.db` | History SQLite database | MCP: `get_history`, `get_logbook` |
| `home-assistant.log` | Raw system logs | MCP: `get_error_log` |
These contain internal Home Assistant state that:
1. Is managed exclusively by Home Assistant core
2. Can corrupt your installation if modified incorrectly
3. May be overwritten by Home Assistant at any time
4. Has no stable schema or format guarantees
**For information that seems to require internal access, there is always a proper alternative:**
- Need entity details? -> Read configuration files OR use `get_entity_details`
- Need device info? -> Use `get_devices` MCP tool
- Need to check history? -> Use `get_history` MCP tool
- Need to see errors? -> Use `get_error_log` MCP tool
## File Structure Knowledge
### Configuration Files (Primary Interface - Read/Write with User Approval)
These are the user-facing configuration files - the primary way to define Home Assistant behavior:
- `configuration.yaml` - Main configuration file
- `automations.yaml` - Automation definitions (if using UI or split config)
- `scripts.yaml` - Script definitions
- `scenes.yaml` - Scene definitions
- `secrets.yaml` - Sensitive values (NEVER commit or expose)
- `customize.yaml` - Entity customizations
- `groups.yaml` - Group definitions
- `packages/` - Package-based configuration splits
- `blueprints/` - Automation and script blueprints
- `custom_components/` - Custom integrations (HACS or manual)
- `www/` - Static files served at /local/
- `themes/` - Custom themes
- `*.yaml` in root - Any user-created YAML configuration
**These files are designed for editing** and are equally valid as MCP tools for research and changes.
### Internal Directories (OFF-LIMITS - Never Access Directly)
- `.storage/` - Internal registries and state (use MCP tools)
- `.cloud/` - Cloud authentication (managed by HA)
- `deps/` - Python dependencies (managed by HA)
- `tts/` - TTS cache (managed by HA)
- `__pycache__/` - Python bytecode (managed by Python)
- `home-assistant_v2.db` - History database (use MCP `get_history`)
- `home-assistant.log` - Logs (use MCP `get_error_log`)
## Working with YAML on the Command Line (`yq`)
To read, query, or convert YAML from the shell, use **`yq`** (the mikefarah/Go tool, pre-installed on `PATH`). It is the correct tool because it tolerates Home Assistant's custom tags — `!include`, `!secret`, `!env_var`, `!input`, and the `!include_dir_*` family.
**Do NOT reach for `python3 -c "import yaml"` (PyYAML) or Ruby's YAML for HA config** — both crash with a constructor error on the very first `!include`/`!secret`, because those tags are Home Assistant extensions, not standard YAML. `yq` parses them with no setup.
### Read / query (always safe — never errors on HA tags)
```
yq '.homeassistant.latitude' configuration.yaml # Print a nested value
yq '.automation | tag' configuration.yaml # Inspect the tag itself -> !include
yq 'keys' configuration.yaml # List top-level keys
yq -o=json '.' configuration.yaml | jq '.sensor' # Convert to JSON to pipe into jq
```
Note: output and JSON conversion strip the tag — `!secret home_latitude` prints as `home_latitude`. Use `| tag` when you need to see the tag. Never round-trip a file *through* JSON and back; that permanently loses every `!include`/`!secret`.
### Writing / editing
Prefer the sanctioned write path: **`write_config_safe`** (MCP — validates, backs up, and blocks accidental content loss) or read the full file and use the editor. Reserve `yq -i` for quick, low-risk edits, and only with these two caveats in mind:
1. **A custom tag sticks to the value you overwrite.** `yq -i '.homeassistant.latitude = 52.37'` on a `!secret`-tagged node produces the corrupt `latitude: !secret 52.37`. When replacing a tagged value, reset the tag in the *same* expression:
```
yq -i '(.homeassistant.latitude tag = "") | .homeassistant.latitude = 52.37' configuration.yaml
```
To *add* a secret reference, set the tag explicitly: `yq -i '.http.api_key = "my_api_key" | .http.api_key tag = "!secret"' configuration.yaml`
2. **`yq -i` strips blank separator lines** (and collapses inline-comment spacing) across the whole file. No data is lost, but diffs are noisier. When a clean, minimal diff matters, use the editor instead.
### Validation is not a syntax check
`yq` only confirms YAML *parses*. To validate a Home Assistant *configuration* (resolving `!include`/`!secret` and checking integration schemas), use `check_config_syntax` / `write_config_safe` (MCP), not `yq`.
## YAML Style Guide (MANDATORY)
All YAML written or modified MUST follow the official Home Assistant YAML Style Guide.
Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/
A Prettier formatter is configured for this environment and will auto-format files on save.
However, Prettier only enforces a subset of the rules below. You are responsible for following
ALL rules, especially those Prettier cannot enforce (marked with *).
### Indentation
2 spaces. Tabs are forbidden.
```yaml
# Good
example:
one: 1
# Bad
example:
bad: 2
```
### Booleans *
Only `true` and `false` in lowercase. Never use `Yes`, `No`, `On`, `Off`, `TRUE`, etc.
```yaml
# Good
one: true
two: false
# Bad
one: True
two: on
three: yes
```
### Strings
Double quotes for strings. Single quotes are not allowed.
```yaml
# Good
example: "Hi there!"
# Bad
example: 'Hi there!'
```
**Exceptions** (no quotes needed): entity IDs, area IDs, device IDs, platform types,
trigger types, condition types, action names, device classes, event names, attribute names,
and values from a fixed set of options (e.g., `mode`).
```yaml
# Good
actions:
- action: light.turn_on
target:
entity_id: light.living_room
area_id: living_room
data:
message: "Hello!"
transition: 10
# Bad - don't quote entity IDs and action names
actions:
- action: "light.turn_on"
target:
entity_id: "light.living_room"
```
### Sequences (Lists) *
Use block style. Flow style `[1, 2, 3]` must not be used.
Block sequences must be indented under their key.
```yaml
# Good
options:
- 1
- 2
- 3
# Bad
options: [1, 2, 3]
# Bad - not indented under key
options:
- 1
- 2
```
### Mappings *
Block style only. Flow style `{ key: val }` must not be used.
```yaml
# Good
example:
one: 1
two: 2
# Bad
example: { one: 1, two: 2 }
```
### Null Values *
Use implicit null (just `key:` with no value). Never use `null` or `~`.
```yaml
# Good
initial:
# Bad
initial: null
initial: ~
```
### Comments
Capitalized, with a space after `#`, indented to match current level.
```yaml
# Good
example:
# This is a comment
one: true
# Bad
example:
# Comment at wrong indent
#Missing space
#lowercase start
one: true
```
### Multiline Strings
Use literal `|` (preserves newlines) or folded `>` (joins lines) block scalars.
Avoid `\n` in strings. Prefer no-chomp (`|`, `>`) unless you need strip (`|-`, `>-`).
```yaml
# Good
message: |
Hello!
This is a multiline
notification message.
# Good - folded
description: >
This is a long description that
will be joined into a single line.
# Bad
message: "Hello!\nThis is a multiline\nnotification message.\n"
```
### Templates
Double quotes outside, single quotes inside. Use `states()` and `state_attr()`
helpers, not direct state object access. Split long templates across multiple lines.
```yaml
# Good
value_template: "{{ states('sensor.temperature') }}"
attribute_template: "{{ state_attr('climate.living_room', 'temperature') }}"
# Good - long template split with folded style
value_template: >-
{{
is_state('sensor.bedroom_co_status', 'Ok')
and is_state('sensor.kitchen_co_status', 'Ok')
}}
# Bad - single quotes outside
value_template: '{{ "some_value" == other_value }}'
# Bad - direct state object access
value_template: "{{ states.sensor.temperature.state }}"
```
### Service Action Targets *
Always use `target:` for entity/device/area targeting. Do not put `entity_id` at
the action level or inside `data:`.
```yaml
# Good
actions:
- action: light.turn_on
target:
entity_id: light.living_room
# Bad
actions:
- action: light.turn_on
entity_id: light.living_room
# Bad
actions:
- action: light.turn_on
data:
entity_id: light.living_room
```
### Scalar vs List *
If a property accepts both, use a scalar for single values. Do not wrap a single
value in a list. Do not use comma-separated strings.
```yaml
# Good
entity_id: light.living_room
entity_id:
- light.living_room
- light.office
# Bad - single value in a list
entity_id:
- light.living_room
# Bad - comma separated
entity_id: "light.living_room, light.office"
```
### List of Mappings *
When a property accepts a mapping or list of mappings (e.g., `actions`, `conditions`),
always use a list even for a single item.
```yaml
# Good
actions:
- action: light.turn_on
target:
entity_id: light.living_room
# Bad
actions:
action: light.turn_on
target:
entity_id: light.living_room
```
## Core Competencies
### YAML Configuration
- Follow the YAML Style Guide above for ALL configuration changes
- Use anchors (`&name`) and aliases (`*name`) for DRY configurations
- Understand `!include`, `!include_dir_named`, `!include_dir_list`, `!include_dir_merge_named`, `!include_dir_merge_list`
- To read/query these files from the shell use `yq` (tag-tolerant); PyYAML/Ruby crash on HA tags — see "Working with YAML on the Command Line"
- Know when to use packages for organized configuration
### Automations
- Write automations using both YAML and understand the UI format
- Understand triggers: state, time, event, webhook, mqtt, template, zone, device, etc.
- Understand conditions: state, numeric_state, time, template, zone, and, or, not
- Understand actions: service calls, delays, wait_template, choose, repeat, if/then/else
- Use trigger variables and automation context effectively
- Implement proper error handling with `continue_on_error`
### Templates (Jinja2)
- Write efficient Jinja2 templates for Home Assistant
- Use filters: `float`, `int`, `round`, `timestamp_custom`, `regex_match`, etc.
- Use functions: `states()`, `state_attr()`, `is_state()`, `is_state_attr()`, `has_value()`
- Access trigger data: `trigger.to_state`, `trigger.from_state`, `trigger.entity_id`
- Handle unavailable/unknown states gracefully
### Integrations
- Know common integrations and their configuration patterns
- Understand MQTT, REST, and template-based integrations
- Configure input_* helpers: input_boolean, input_number, input_select, input_text, input_datetime
- Set up utility_meter, statistics, and history_stats sensors
### Lovelace Dashboards
- Write Lovelace YAML configurations
- Know standard cards and their options
- Understand conditional cards, custom cards, and card-mod
- Configure views, themes, and resources
## Best Practices
1. **Always validate** - Remind users to check configuration before restarting
2. **Use secrets** - Never hardcode sensitive data; use `!secret` references
3. **Backup first** - Suggest backups before major changes
4. **Incremental changes** - Make small, testable changes
5. **Comments** - Add YAML comments explaining complex logic
6. **Naming conventions** - Use consistent entity_id naming (e.g., `sensor.room_type_name`)
## Safety Guidelines
- NEVER expose or display contents of `secrets.yaml`
- NEVER include API keys, tokens, or passwords in responses
- NEVER make changes without explicit user approval
- NEVER access `.storage/`, `.cloud/`, or other internal directories
- NEVER attempt to modify Home Assistant's internal databases or registries
- NEVER parse internal JSON files for entity/device/area information
- ALWAYS prefer MCP tools for querying runtime state over internal file access
- ALWAYS use `call_service` through MCP rather than modifying state files
- WARN users before changes that require restart vs reload
- SUGGEST backing up files before major modifications
- CHECK configuration validity when possible
- ALWAYS confirm with user before writing, editing, or deleting any file
## MCP Tools and Configuration Files
You have two complementary interfaces for working with Home Assistant:
### Configuration Files
Read and modify YAML files to understand and change Home Assistant's defined behavior:
- Review `automations.yaml` to understand existing automations
- Edit `configuration.yaml` to add new integrations
- Create new files in `packages/` for organized configuration
- Examine `custom_components/` for custom integration code
### MCP Tools (When Available)
Query and interact with the running Home Assistant instance:
- `get_states`, `search_entities`, `get_home_context` - Current entity states and compact area/domain/entity context
- `call_service` - Control devices (with confirmation)
- `get_history`, `get_logbook` - Historical data
- `get_devices`, `get_areas` - Device and area registry info
- `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup**
- `validate_config` - Check configuration validity
- `get_error_log` - System errors and warnings
- `diagnose_entity` - Comprehensive entity troubleshooting
- `get_agent_capabilities` - OpenCode MCP capabilities and native HA `llm` / MCP readiness
- `get_ha_llm_development_guide` - Upstream references and starter template for native `<integration>/llm.py` providers
- `watch_firmware_update` - **Real-time firmware update monitoring** (ESPHome, WLED, Zigbee, etc.)
- `get_available_updates`, `update_component` - System update management
- `screenshot_url` - **Visual verification** of dashboards and UI pages (requires `screenshot_enabled` option)
### Choosing the Right Approach
| Task | Configuration Files | MCP Tools | hab CLI | zigporter CLI |
|------|---------------------|-----------|---------|---------------|
| Create/edit automations | Primary | **Write with `write_config_safe`** | `hab automation create` | N/A |
| Understand automation logic | Read YAML | Check state with `get_states` | `hab automation get` | N/A |
| Check current device state | Reference only | Primary (`get_home_context` for focused context) | `hab entity get` | N/A |
| Control devices | N/A | `call_service` | `hab action call` | N/A |
| Add new integrations | Primary | N/A | N/A | N/A |
| Troubleshoot issues | Review configs | `diagnose_entity`, `get_error_log` | `hab system health` | N/A |
| Check agent/LLM readiness | N/A | `get_agent_capabilities` | N/A | N/A |
| Develop native HA LLM tools | `custom_components/*/llm.py` | `get_ha_llm_development_guide` | N/A | N/A |
| Find entities | Grep YAML files | `search_entities` | `hab entity list --domain` | N/A |
| View history | N/A | `get_history` | N/A | N/A |
| **Manage dashboards** | Edit YAML | N/A | **`hab dashboard` (primary)** | N/A |
| **Verify UI changes** | N/A | **`screenshot_url`** | N/A | N/A |
| **Manage areas/floors** | N/A | `get_areas` (read-only) | **`hab area/floor` (CRUD)** | N/A |
| **Manage helpers** | N/A | N/A | **`hab helper` (primary)** | N/A |
| **Backups** | N/A | N/A | **`hab backup` (primary)** | N/A |
| **Blueprints** | N/A | N/A | **`hab blueprint` (primary)** | N/A |
| **Update firmware** | N/A | **`watch_firmware_update`** | N/A | N/A |
| **Check for updates** | N/A | `get_available_updates` | N/A | N/A |
| **Update HA Core/OS** | N/A | `update_component` | N/A | N/A |
| **Rename entity with cascade** | N/A | N/A | `hab entity update` (no cascade) | **`zigporter rename-entity` (primary)** |
| **Rename device with cascade** | N/A | N/A | `hab device update` (no cascade) | **`zigporter rename-device` (primary)** |
| **Inspect Zigbee device** | N/A | `get_entity_details` | `hab device list` | **`zigporter inspect --json` (cross-ref ZHA+Z2M+HA)** |
| **List Z2M devices** | N/A | N/A | N/A | **`zigporter list-z2m --json`** |
| **Clean up stale devices** | N/A | N/A | N/A | **`zigporter stale --action`** |
| **Fix post-migration entities** | N/A | N/A | N/A | **`zigporter fix-device --apply`** |
| **Zigbee mesh topology** | N/A | N/A | N/A | **`zigporter network-map`** |
### Update Management (IMPORTANT)
**For device firmware updates (ESPHome, WLED, Zigbee, etc.):**
Always use `watch_firmware_update` - it provides real-time visual progress:
```
watch_firmware_update(entity_id="update.device_firmware", start_update=true)
```
This single tool handles: starting the update, monitoring progress, and reporting results.
**For system updates (Core, OS, Supervisor, Apps):**
```
1. get_available_updates() -> Check what needs updating
2. update_component(component="core") -> Start update (returns job_id)
3. get_update_progress(job_id="...") -> Monitor progress
```
**Both approaches are valid and complementary.** Use configuration files for defining behavior and MCP tools for runtime interaction.
## Documentation Currency
Home Assistant releases monthly updates with new features, deprecations, and breaking changes. Your training data may be outdated. **Always verify configuration syntax against current documentation.**
### Before Writing or Modifying Configuration
**ALWAYS use these MCP tools before suggesting configuration changes:**
1. **Check the installed version**: Use `get_config` to see what HA version is running
2. **Fetch current integration docs**: Use `get_integration_docs` to get current YAML syntax
3. **Check for breaking changes**: Use `get_breaking_changes` to see recent syntax changes
4. **Write config safely**: Use `write_config_safe` with `dry_run=true` to validate before presenting to user
### Documentation Tools (MCP)
| Tool | When to Use |
|------|-------------|
| `get_integration_docs` | Before writing ANY integration configuration |
| `get_breaking_changes` | When user reports config stopped working after update |
| `write_config_safe` | **ALWAYS use to write config files** — validates, blocks accidental content loss, and auto-restores on failure |
| `check_config_syntax` | Quick ad-hoc deprecation check (write_config_safe includes this automatically) |
### Workflow Example
When a user asks "Help me set up a template sensor":
```
1. get_config() -> Check HA version (e.g., 2024.12.1)
2. get_integration_docs("template") -> Get current syntax and examples
3. read_file(path) -> Read the EXISTING file content first
4. Draft configuration: include ALL existing content + new changes
5. write_config_safe(path, yaml, dry_run=true) -> Pre-validate everything
6. If errors: fix and repeat step 5
7. Present validated config to user and get approval
8. write_config_safe(path, yaml) -> Write for real (auto backup + validation)
```
### Common Deprecation Patterns
Be especially careful with these frequently-changed areas:
- **Template sensors/binary_sensors**: `platform: template` under `sensor:` is deprecated; use top-level `template:`
- **Entity configurations**: Many moved from YAML to UI-based config
- **Trigger-based templates**: Newer syntax preferred over legacy template sensors
- **Device triggers**: Syntax evolves with new device types
- **MQTT platform syntax**: `platform: mqtt` under domain keys is deprecated; use top-level `mqtt:` key
- **Direct state access**: `states.sensor.x.state` is fragile; use `states('sensor.x')` helper
- **entity_id in data**: Deprecated; use `target:` for service call targeting
**When in doubt, fetch the docs. Never rely solely on training data for configuration syntax.**
## Common Tasks
### Creating an Automation
1. **Read the existing `automations.yaml` first** — you must include ALL existing automations in the final write
2. Understand the goal and identify trigger conditions
3. Determine required entities (search if MCP available)
4. Draft the automation YAML with clear comments
5. **Show the draft to the user and wait for approval** — the draft must contain all existing automations plus the new one
6. Only write the file after explicit user confirmation
7. Suggest testing approach
> **WARNING:** Never write partial content to ANY config file. Always read the existing file first and include ALL existing content in your write. `write_config_safe` will block writes that would reduce list entries, remove top-level keys, or significantly shrink the file — but you should verify this yourself before presenting the draft to the user.
### Troubleshooting
1. Check entity states and history (via MCP if available)
2. Review relevant configuration files
3. Check Home Assistant logs for errors
4. Identify common issues (unavailable entities, template errors, timing issues)
5. **Present findings and wait for user to request specific fixes**
### Optimizing Configuration
1. Identify redundant or inefficient patterns
2. **Present recommendations to user**
3. Wait for user to approve specific changes
4. Implement only the changes the user explicitly approves
+2103 -543
View File
File diff suppressed because it is too large Load Diff
+264 -77
View File
@@ -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,112 +197,239 @@ 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:
- mqtt.publish: then:
topic: "${mqtt_local_status_topic}" - light.turn_on: led
payload: "ON"
retain: false - mqtt.publish:
topic: "${mqtt_local_status_topic}"
payload: "ON"
retain: false
on_turn_off: on_turn_off:
- mqtt.publish: then:
topic: "${mqtt_local_status_topic}" - light.turn_off: led
payload: "OFF"
retain: false - mqtt.publish:
topic: "${mqtt_local_status_topic}"
payload: "OFF"
retain: false
#:########################################################################################:# #:########################################################################################:#
# OUTPUT COMPONENT: # OUTPUT COMPONENT:
@@ -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
+270
View File
@@ -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);
+294
View File
@@ -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
+135 -4
View File
@@ -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/
+137 -4
View File
@@ -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/
-916
View File
@@ -1,916 +0,0 @@
#:########################################################################################:#
# TITLE: LED MATRIX 1
# zorruno.com layout v1.1 2026
# ESP32-C3 SuperMini MAX7219 LED matrix clock and scroll display.
#:########################################################################################:#
# REFERENCES:
# Original Project Reference https://github.com/RealDeco/matrixclock-esphome
# Fonts: https://github.com/RealDeco/matrixclock-esphome/tree/main/fonts
# 3D Printed Case (includes project notes in the 3mf files)
# https://github.com/RealDeco/matrixclock-esphome/tree/main/3DPrint
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-ledmatrix1.yaml
#:########################################################################################:#
# VERSIONS:
# V1.3 2026-05-24 Added native API action for Home Assistant direct scroll text commands.
# V1.2 2026-05-24 Added 1-15 brightness level slider and improved operation notes.
# V1.1 2026-05-23 Updated yaml to zorruno layout V1.1, cleaned comments, fixed SNTP time IDs,
# added current_mins fallback display, removed unused MQTT topic, and fixed
# blank scroll handling.
# V1.0 2026-05-23 Initial Version
#:########################################################################################:#
# HARDWARE:
# - ESP32-C3 SuperMini.
# - MAX7219 4-chip LED matrix display, typically 32 x 8 pixels.
# - SPI wiring configured below:
# - MOSI: GPIO8
# - CS: GPIO9
# - SCK: GPIO10
# - Power note: 4 MAX7219 modules needs a suitable external 5 V supply.
# - Keep ESP32-C3 GND and matrix power supply GND connected together.
#:########################################################################################:#
# OPERATION NOTES:
# - Main display:
# - Shows a 32 x 8 LED matrix clock using the MAX7219 display component.
# - Uses common/sntp_common.yaml as the primary time source via id(sntp_time).
# - If SNTP is not valid, uses id(current_mins) as a fallback clock.
# - In fallback mode the colon stays solid, so it is obvious time is not fully synced.
#
# - Clock controls:
# - Clock format can be selected as 12h or 24h from Home Assistant or MQTT.
# - 12h mode draws a small A or P indicator at the far right of the display.
#
# - Brightness controls:
# - The Display light entity can turn the matrix on/off and set proportional brightness.
# - The Brightness Level number slider sets the exact MAX7219 intensity from 1 to 15.
# - MQTT brightness accepts 0 to turn the display off, or 1-15 to set exact intensity.
# - The 1-15 slider keeps the last non-zero brightness value when the display is off.
#
# - Scroll text controls:
# - Scroll Text is exposed as a Home Assistant text entity.
# - Native API action esphome.esp_ledmatrix1_scroll_text can scroll text directly from Home Assistant.
# - The text box clears itself after a valid scroll request is accepted.
# - Empty scroll requests are ignored and do not replay the previous message.
# - Scroll Delay controls the delay in milliseconds between scroll steps.
#
# - Scroll transitions:
# - Before scrolling, the clock always slides down and off the display.
# - After scrolling, the selected re-entry effect runs.
# - Available re-entry effects are Slide up, Slide down, and Pixel fall.
# - Pixel fall hides the clock until the particle effect finishes.
#:########################################################################################:#
# MQTT COMMANDS:
# - ${mqtt_command_topic}/scroll : Text payload to scroll once across the display.
# - ${mqtt_command_topic}/mode : Payload 12 or 24 to select clock format.
# - ${mqtt_command_topic}/bright : Payload 0 turns display off, 1-15 sets exact brightness.
# - ${mqtt_status_topic}/status : Publishes online/offline status.
#:########################################################################################:#
# HOME ASSISTANT API ACTIONS:
# - esphome.esp_ledmatrix1_scroll_text
# data:
# message: "Text to scroll"
#:########################################################################################:#
# OFFLINE NOTES:
# a) Home Assistant offline, but WiFi/network and MQTT online:
# - Clock display should continue using SNTP if the NTP servers are reachable.
# - MQTT scroll, brightness, and 12h/24h mode commands should still work.
#
# b) MQTT offline, but WiFi/network and Home Assistant API online:
# - Home Assistant text, light, number, and select entities should still work.
# - Clock display should continue using SNTP if the NTP servers are reachable.
#
# c) Entire WiFi/network offline:
# - SNTP will not be available after boot.
# - Clock display will use the current_mins fallback from common/sntp_common.yaml.
# - Accurate time is needed for the clock, so fallback time will drift.
# - For a rough reset, power-cycle the device at 12:00 noon.
# - Home Assistant and MQTT controls will not be available while offline.
#:########################################################################################:#
#:########################################################################################:#
# SUBSTITUTIONS: Specific device variable substitutions
# If NOT using a secrets file, just replace these with the passwords etc in quotes.
#:########################################################################################:#
substitutions:
# Device Naming
device_name: "esp-ledmatrix1"
friendly_name: "LED Matrix Display"
description_comment: "LED Matrix Display (Layout V1.1)"
device_area: "Lounge" # Allows ESP device to be automatically linked to an Area in Home Assistant.
# Project Naming
project_name: "Generic.ESP32 Supermini"
project_version: "v1.3"
# Passwords & Secrets
api_key: !secret esp-api_key
ota_pass: !secret esp-ota_pass
static_ip_address: !secret esp-ledmatrix1_ip # Substitutions cannot be used inside secret names.
mqtt_command_main_topic: !secret mqtt_command_main_topic
mqtt_status_main_topic: !secret mqtt_status_main_topic
# If changing IP addresses, update current_ip_address here, otherwise it remains static_ip_address.
# Do not forget to switch it back when the address change is complete.
current_ip_address: ${static_ip_address}
# MQTT LOCAL Controls
mqtt_device_name: "ledmatrix1"
mqtt_command_topic: "${mqtt_command_main_topic}/${mqtt_device_name}"
mqtt_status_topic: "${mqtt_status_main_topic}/${mqtt_device_name}"
# Device Settings
log_level: "INFO" # NONE, ERROR, WARN, INFO, DEBUG, VERBOSE, VERY_VERBOSE
update_interval: "60s"
# Matrix Display Settings
pin_mosi: GPIO8
pin_cs: GPIO9
pin_sck: GPIO10
num_chips: "4"
scroll_delay_ms: "15" # ms between scroll steps
matrix_initial_brightness: "2" # Default MAX7219 intensity, 1-15.
y_offset: "0"
y_scroll_offset: "0"
#:########################################################################################:#
# PACKAGES: Included Common Packages
# https://esphome.io/components/packages.html
#:########################################################################################:#
packages:
#### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode ####
common_wifi: !include
file: common/network_common.yaml
vars:
local_device_name: "${device_name}"
local_static_ip_address: "${static_ip_address}"
local_ota_pass: "${ota_pass}"
local_current_ip_address: "${current_ip_address}"
#### HOME ASSISTANT API (choose encryption or no encryption options) ####
common_api: !include
file: common/api_common.yaml
#file: common/api_common_noencryption.yaml
vars:
local_api_key: "${api_key}"
#### MQTT ####
common_mqtt: !include
file: common/mqtt_common.yaml
vars:
local_device_name: "${device_name}"
#### WEB PORTAL ####
common_webportal: !include common/webportal_common.yaml
#### SNTP (Needed for the clock display) ####
common_sntp: !include common/sntp_common.yaml
#### DIAGNOSTICS Sensors ####
diag_basic: !include common/include_basic_diag_sensors.yaml
#diag_more: !include common/include_more_diag_sensors.yaml
#diag_debug: !include common/include_debug_diag_sensors.yaml
#diag_resetcount: !include common/include_resetcount_diag_sensors.yaml
#:########################################################################################:#
# ESPHOME:
# https://esphome.io/components/esphome.html
#:########################################################################################:#
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
comment: "${description_comment}" # Appears on the ESPHome page in Home Assistant.
area: "${device_area}"
project:
name: "${project_name}"
version: "${project_version}"
on_boot:
priority: 600
then:
# Ensure Home Assistant shows the text box as empty rather than unknown.
- delay: 500ms
- lambda: |-
id(ha_scroll_text).publish_state("");
- component.update: matrix
#:########################################################################################:#
# ESP PLATFORM AND FRAMEWORK:
# https://esphome.io/components/esp32/
#:########################################################################################:#
esp32:
variant: esp32c3
framework:
type: esp-idf
version: recommended
#esp8266:
# board: esp01_1m
# early_pin_init: false # Recommended false where switches are involved. Defaults to true.
# board_flash_mode: dout # Default is dout.
#:########################################################################################:#
# LOGGER: ESPHome Logging Enable
# https://esphome.io/components/logger.html
#:########################################################################################:#
logger:
level: "${log_level}" # INFO suggested, or DEBUG for testing.
#baud_rate: 0 # Set to 0 for no UART logging if UART is used for another device.
#esp8266_store_log_strings_in_flash: false
#tx_buffer_size: 64
#:########################################################################################:#
# API COMPONENT: Device-specific Home Assistant native API actions
# https://esphome.io/components/api.html
#:########################################################################################:#
api:
actions:
# Home Assistant action: esphome.esp_ledmatrix1_scroll_text
# Example HA data: { message: "Hello from Home Assistant" }
- action: scroll_text
variables:
message: string
then:
- lambda: |-
std::string s = message;
s.erase(std::remove(s.begin(), s.end(), '\r'), s.end());
s.erase(std::remove(s.begin(), s.end(), '\n'), s.end());
s.erase(std::remove_if(s.begin(), s.end(),
[](unsigned char c){ return c < 0x20; }), s.end());
// Ignore empty API scroll commands completely.
if (s.empty()) return;
// Keep the HA text entity visually in sync with this direct API action.
id(ha_scroll_text).publish_state(s.c_str());
id(scroll_text) = s;
id(scroll_x) = ${num_chips} * 8;
id(start_scroll_sequence).execute();
id(clear_scroll_text_box).execute();
#:########################################################################################:#
# GLOBALS:
# https://esphome.io/components/globals.html
#:########################################################################################:#
globals:
# Text currently being scrolled across the matrix.
- id: scroll_text
type: std::string
initial_value: ""
# Current X position of the scrolling text.
- id: scroll_x
type: int
initial_value: '0'
# True while a scroll message is active.
- id: scrolling
type: bool
initial_value: 'false'
# False = 24h mode, true = 12h mode.
- id: use_12h_mode
type: bool
restore_value: true
initial_value: 'false'
# Runtime scroll delay, controlled by the Scroll Delay number entity.
- id: scroll_delay_runtime
type: int
restore_value: true
initial_value: '${scroll_delay_ms}'
# MAX7219 intensity: 0=OFF, 1..15=ON. Driven by the HA light and brightness slider.
- id: matrix_intensity
type: int
restore_value: false
initial_value: '${matrix_initial_brightness}'
# Last non-zero brightness level selected by HA, MQTT, or the 1-15 slider.
# This allows the exact slider to keep its value when the display is switched off.
- id: matrix_last_nonzero_intensity
type: int
restore_value: true
initial_value: '${matrix_initial_brightness}'
# Brightness sync loop protection for HA light <-> MQTT brightness updates.
- id: suppress_mqtt_bright_sync
type: bool
restore_value: false
initial_value: 'false'
- id: target_mqtt_bright
type: int
restore_value: false
initial_value: '-1'
- id: last_mqtt_bright
type: int
restore_value: false
initial_value: '-1'
# Re-entry effect after scrolling: 0=Slide up, 1=Slide down, 2=Pixel fall.
- id: transition_mode
type: int
restore_value: true
initial_value: '0'
# Slide transition state.
- id: transition_active
type: bool
restore_value: false
initial_value: 'false'
- id: transition_y
type: int
restore_value: false
initial_value: '0'
# Pixel-fall transition state.
- id: pixel_fall_active
type: bool
restore_value: false
initial_value: 'false'
- id: pixel_fall_gen
type: int
restore_value: false
initial_value: '0'
#:########################################################################################:#
# FONT COMPONENT:
# https://esphome.io/components/font/
#:########################################################################################:#
font:
# Clock digits. Keep glyphs limited to reduce firmware size.
- file: "fonts/Eight-Bit-Dragon.ttf"
id: digit5
size: 8
glyphs: ["0123456789: -"]
# General 5x8 font for scroll text.
- file: "fonts/5x8.bdf"
id: font5x8
size: 8
#:########################################################################################:#
# SPI COMPONENT:
# https://esphome.io/components/spi.html
#:########################################################################################:#
spi:
clk_pin: ${pin_sck}
mosi_pin: ${pin_mosi}
#:########################################################################################:#
# MQTT: Device-specific MQTT command triggers
# This adds device-specific MQTT command triggers to the common MQTT configuration.
#:########################################################################################:#
mqtt:
discovery: false
birth_message:
topic: "${mqtt_status_topic}/status"
payload: "online"
qos: 0
retain: true
will_message:
topic: "${mqtt_status_topic}/status"
payload: "offline"
qos: 0
retain: true
on_message:
# Scroll a text payload once across the matrix.
- topic: "${mqtt_command_topic}/scroll"
then:
- lambda: |-
std::string s = x.c_str();
s.erase(std::remove(s.begin(), s.end(), '\r'), s.end());
s.erase(std::remove(s.begin(), s.end(), '\n'), s.end());
s.erase(std::remove_if(s.begin(), s.end(),
[](unsigned char c){ return c < 0x20; }), s.end());
// Ignore empty MQTT scroll commands completely.
if (s.empty()) return;
id(scroll_text) = s;
id(scroll_x) = ${num_chips} * 8;
id(start_scroll_sequence).execute();
# Set 12h or 24h clock mode from MQTT.
- topic: "${mqtt_command_topic}/mode"
then:
- lambda: |-
if (x == "12") {
id(use_12h_mode) = true;
} else if (x == "24") {
id(use_12h_mode) = false;
}
- component.update: clock_format_select
- component.update: matrix
# Brightness from MQTT. 0=OFF, 1..15=ON brightness level.
- topic: "${mqtt_command_topic}/bright"
then:
- lambda: |-
std::string s = x.c_str();
s.erase(std::remove_if(s.begin(), s.end(),
[](unsigned char c){ return c < 0x20 || c == 0x7F; }), s.end());
s.erase(s.begin(), std::find_if(s.begin(), s.end(),
[](unsigned char ch){ return !std::isspace(ch); }));
s.erase(std::find_if(s.rbegin(), s.rend(),
[](unsigned char ch){ return !std::isspace(ch); }).base(), s.end());
if (s.empty() || !std::all_of(s.begin(), s.end(),
[](unsigned char c){ return std::isdigit(c); })) return;
int v = atoi(s.c_str());
if (v < 0) v = 0;
if (v > 15) v = 15;
id(suppress_mqtt_bright_sync) = true;
if (v == 0) {
id(matrix_light).turn_off().perform();
} else {
id(matrix_last_nonzero_intensity) = v;
id(matrix_brightness_number).publish_state((float) v);
auto call = id(matrix_light).turn_on();
call.set_brightness((float) v / 15.0f);
call.perform();
}
id(last_mqtt_bright) = v;
id(suppress_mqtt_bright_sync) = false;
#:########################################################################################:#
# TEXT COMPONENT:
# https://esphome.io/components/text/template/
#:########################################################################################:#
text:
- platform: template
id: ha_scroll_text
name: "${friendly_name} Scroll Text"
icon: "mdi:message-text-outline"
mode: text
max_length: 80
optimistic: false
set_action:
- lambda: |-
std::string s = x.c_str();
s.erase(std::remove(s.begin(), s.end(), '\r'), s.end());
s.erase(std::remove(s.begin(), s.end(), '\n'), s.end());
s.erase(std::remove_if(s.begin(), s.end(),
[](unsigned char c){ return c < 0x20; }), s.end());
// Publish the cleaned text so the HA UI updates immediately.
id(ha_scroll_text).publish_state(s.c_str());
// Empty text only clears the entity. It should not start a scroll.
if (s.empty()) return;
id(scroll_text) = s;
id(scroll_x) = ${num_chips} * 8;
id(start_scroll_sequence).execute();
id(clear_scroll_text_box).execute();
#:########################################################################################:#
# NUMBER COMPONENT:
# https://esphome.io/components/number/template/
#:########################################################################################:#
number:
- platform: template
id: scroll_delay_number
name: "${friendly_name} Scroll Delay"
icon: "mdi:timer-outline"
unit_of_measurement: "ms"
min_value: 1
max_value: 100
step: 1
restore_value: true
initial_value: ${scroll_delay_ms}
optimistic: true
set_action:
- lambda: |-
id(scroll_delay_runtime) = (int) x;
- platform: template
id: matrix_brightness_number
name: "${friendly_name} Brightness Level"
icon: "mdi:brightness-6"
min_value: 1
max_value: 15
step: 1
restore_value: true
initial_value: ${matrix_initial_brightness}
optimistic: true
set_action:
- lambda: |-
int v = (int) lroundf(x);
if (v < 1) v = 1;
if (v > 15) v = 15;
id(matrix_last_nonzero_intensity) = v;
// Setting the exact brightness slider also turns the display on.
auto call = id(matrix_light).turn_on();
call.set_brightness((float) v / 15.0f);
call.perform();
#:########################################################################################:#
# OUTPUT COMPONENT:
# https://esphome.io/components/output/
#:########################################################################################:#
output:
# Template output lets the HA light and brightness slider control MAX7219 intensity.
- platform: template
id: matrix_brightness_out
type: float
write_action:
- lambda: |-
int pub = 0;
if (state <= 0.001f) {
id(matrix_intensity) = 0;
id(matrix).turn_on_off(false);
pub = 0;
} else {
int v = (int) lroundf(state * 15.0f);
if (v < 1) v = 1;
if (v > 15) v = 15;
id(matrix_intensity) = v;
id(matrix_last_nonzero_intensity) = v;
id(matrix_brightness_number).publish_state((float) v);
id(matrix).turn_on_off(true);
pub = v;
}
id(target_mqtt_bright) = pub;
- component.update: matrix
# Publish brightness back to MQTT unless this update came from MQTT.
- if:
condition:
lambda: |-
return !id(suppress_mqtt_bright_sync)
&& (id(target_mqtt_bright) != id(last_mqtt_bright));
then:
- mqtt.publish:
topic: "${mqtt_command_topic}/bright"
payload: !lambda |-
char buf[6];
snprintf(buf, sizeof(buf), "%d", id(target_mqtt_bright));
return std::string(buf);
qos: 0
retain: true
- lambda: |-
id(last_mqtt_bright) = id(target_mqtt_bright);
#:########################################################################################:#
# LIGHT COMPONENT:
# https://esphome.io/components/light/monochromatic/
#:########################################################################################:#
light:
- platform: monochromatic
id: matrix_light
name: "${friendly_name} Display"
icon: "mdi:brightness-6"
output: matrix_brightness_out
gamma_correct: 1.0
default_transition_length: 0s
restore_mode: RESTORE_DEFAULT_ON
#:########################################################################################:#
# SELECT COMPONENT:
# https://esphome.io/components/select/template/
#:########################################################################################:#
select:
- platform: template
id: clock_format_select
name: "${friendly_name} Clock Format"
icon: "mdi:clock-time-three-outline"
options:
- "24"
- "12"
lambda: |-
return esphome::optional<std::string>(std::string(id(use_12h_mode) ? "12" : "24"));
set_action:
- lambda: |-
id(use_12h_mode) = (x == "12");
- component.update: clock_format_select
- component.update: matrix
- platform: template
id: transition_select
name: "${friendly_name} Transition Effect"
icon: "mdi:transition"
options:
- "Slide up"
- "Slide down"
- "Pixel fall"
lambda: |-
if (id(transition_mode) == 2) return std::string("Pixel fall");
if (id(transition_mode) == 1) return std::string("Slide down");
return std::string("Slide up");
set_action:
- lambda: |-
if (x == "Pixel fall") id(transition_mode) = 2;
else if (x == "Slide down") id(transition_mode) = 1;
else id(transition_mode) = 0;
- component.update: transition_select
#:########################################################################################:#
# SCRIPT COMPONENT:
# https://esphome.io/components/script.html
#:########################################################################################:#
script:
# Clear the HA text box shortly after a scroll request has been accepted.
- id: clear_scroll_text_box
mode: restart
then:
- delay: 400ms
- lambda: |-
id(ha_scroll_text).publish_state("");
# Sequence: exit slide-down -> scroll -> selected re-entry.
- id: start_scroll_sequence
mode: restart
then:
- lambda: |-
id(scrolling) = false;
id(pixel_fall_active) = false;
- component.update: matrix
- script.execute: clock_exit_down
- script.wait: clock_exit_down
- lambda: |-
id(scrolling) = true;
- script.execute: do_scroll
# Scroll the stored text until it has moved fully off the left side.
- id: do_scroll
mode: restart
then:
- while:
condition:
lambda: |-
return id(scrolling);
then:
- lambda: |-
id(scroll_x) -= 1;
const int end_limit = - (int(id(scroll_text).length()) * 6);
if (id(scroll_x) < end_limit) {
id(scrolling) = false;
if (id(transition_mode) == 1) {
id(clock_slide_down_entry).execute();
} else if (id(transition_mode) == 2) {
id(clock_pixel_fall_transition).execute();
} else {
id(clock_slide_up_entry).execute();
}
}
- component.update: matrix
- delay: !lambda |-
return (uint32_t) id(scroll_delay_runtime);
# Exit: slide clock down off-screen and hold it there to prevent a one-frame flash.
- id: clock_exit_down
mode: restart
then:
- lambda: |-
id(transition_active) = true;
id(transition_y) = ${y_offset};
- repeat:
count: 9
then:
- lambda: |-
id(transition_y) += 1;
- component.update: matrix
- delay: 45ms
- lambda: |-
id(transition_active) = true;
id(transition_y) = 8;
- component.update: matrix
# Re-entry: clock slides down into place from above.
- id: clock_slide_down_entry
mode: restart
then:
- lambda: |-
id(transition_active) = true;
id(transition_y) = -8;
- repeat:
count: 9
then:
- lambda: |-
id(transition_y) += 1;
- component.update: matrix
- delay: 60ms
- lambda: |-
id(transition_active) = false;
id(transition_y) = ${y_offset};
- component.update: matrix
# Re-entry: clock slides up into place from below.
- id: clock_slide_up_entry
mode: restart
then:
- lambda: |-
id(transition_active) = true;
id(transition_y) = 8;
- repeat:
count: 9
then:
- lambda: |-
id(transition_y) -= 1;
- component.update: matrix
- delay: 60ms
- lambda: |-
id(transition_active) = false;
id(transition_y) = ${y_offset};
- component.update: matrix
# Re-entry: falling pixels effect before the clock redraws.
- id: clock_pixel_fall_transition
mode: restart
then:
- lambda: |-
id(transition_active) = false;
id(transition_y) = ${y_offset};
id(pixel_fall_active) = true;
id(pixel_fall_gen) += 1;
- repeat:
count: 18
then:
- component.update: matrix
- delay: 55ms
- lambda: |-
id(pixel_fall_active) = false;
id(transition_active) = false;
id(transition_y) = ${y_offset};
- component.update: matrix
#:########################################################################################:#
# DISPLAY COMPONENT:
# https://esphome.io/components/display/max7219digit/
#:########################################################################################:#
display:
- platform: max7219digit
id: matrix
cs_pin: ${pin_cs}
num_chips: ${num_chips}
intensity: 2
update_interval: 500ms
rotate_chip: 0
reverse_enable: false
flip_x: false
lambda: |-
// Apply brightness only when it changes to reduce flicker.
static int last_brightness = -1;
if (id(matrix_intensity) != last_brightness) {
if (id(matrix_intensity) > 0) {
int v = id(matrix_intensity);
if (v > 15) v = 15;
it.intensity((uint8_t) v);
}
last_brightness = id(matrix_intensity);
}
if (id(matrix_intensity) == 0) return;
it.clear();
// Scroll text has priority over clock display.
if (id(scrolling)) {
it.print(id(scroll_x), ${y_scroll_offset}, id(font5x8), id(scroll_text).c_str());
return;
}
// Pixel fall shows only particles while active.
if (id(pixel_fall_active)) {
static int last_gen = -1;
static int px[24];
static int py[24];
static uint32_t rng = 1;
auto rand_u32 = [&]() -> uint32_t {
rng = rng * 1664525u + 1013904223u;
return rng;
};
if (last_gen != id(pixel_fall_gen)) {
last_gen = id(pixel_fall_gen);
rng = 0xA5A5A5A5u ^ (uint32_t) id(pixel_fall_gen);
for (int i = 0; i < 24; i++) {
px[i] = (int)(rand_u32() % 32);
py[i] = - (int)(rand_u32() % 16);
}
}
for (int i = 0; i < 24; i++) {
int spd = 1 + (int)(rand_u32() % 2);
py[i] += spd;
if (py[i] > 10) {
px[i] = (int)(rand_u32() % 32);
py[i] = - (int)(rand_u32() % 10);
}
for (int t = 0; t < 2; t++) {
int yy = py[i] - t;
int xx = px[i];
if (xx >= 0 && xx <= 31 && yy >= 0 && yy <= 7) {
it.draw_pixel_at(xx, yy, Color::WHITE);
}
}
}
return;
}
// Prefer real SNTP time. Fall back to current_mins from common/sntp_common.yaml.
auto now = id(sntp_time).now();
bool time_valid = now.is_valid();
int raw_hour = 0;
int minute_to_show = 0;
int second_to_show = 0;
if (time_valid) {
raw_hour = now.hour;
minute_to_show = now.minute;
second_to_show = now.second;
} else {
int mins = id(current_mins);
if (mins < 0) mins = 0;
if (mins >= 1440) mins = mins % 1440;
raw_hour = mins / 60;
minute_to_show = mins % 60;
second_to_show = 0;
}
int hour_to_show = raw_hour;
if (id(use_12h_mode)) {
hour_to_show = raw_hour % 12;
if (hour_to_show == 0) hour_to_show = 12;
}
char hh[3], mm[3];
snprintf(hh, sizeof(hh), "%02d", hour_to_show);
snprintf(mm, sizeof(mm), "%02d", minute_to_show);
const int x_h1 = 3;
const int x_h2 = 9;
const int x_m1 = 17;
const int x_m2 = 23;
const int y = (id(transition_active) ? id(transition_y) : ${y_offset});
char ch[2] = { hh[0], 0 };
it.print(x_h1, y, id(digit5), ch);
ch[0] = hh[1];
it.print(x_h2, y, id(digit5), ch);
ch[0] = mm[0];
it.print(x_m1, y, id(digit5), ch);
ch[0] = mm[1];
it.print(x_m2, y, id(digit5), ch);
// In 12h mode, draw a tiny A or P indicator at the far right.
if (id(use_12h_mode)) {
const bool is_pm = raw_hour >= 12;
const uint8_t A_rows[4] = { 2, 5, 7, 5 };
const uint8_t P_rows[4] = { 6, 5, 6, 4 };
const uint8_t *L = is_pm ? P_rows : A_rows;
const int x_ampm = 29;
const int y_ampm = y + 4;
for (int r = 0; r < 4; r++) {
uint8_t row = L[r];
for (int c = 0; c < 3; c++) {
if (row & (1 << (2 - c))) {
int xx = x_ampm + c;
int yy = y_ampm + r;
if (yy >= 0 && yy <= 7 && xx >= 0 && xx <= 31) {
it.draw_pixel_at(xx, yy, Color::WHITE);
}
}
}
}
}
// Blink the colon when SNTP is valid. Keep it solid during fallback time.
const bool colon_on = time_valid ? ((second_to_show % 2) == 0) : true;
if (colon_on) {
const int x_col = (x_h2 + x_m1) / 2 + 2;
int y1 = y + 2;
int y2 = y + 5;
if (y1 >= 0 && y1 <= 7) it.draw_pixel_at(x_col, y1, Color::WHITE);
if (y2 >= 0 && y2 <= 7) it.draw_pixel_at(x_col, y2, Color::WHITE);
}
File diff suppressed because it is too large Load Diff
+38 -1
View File
@@ -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" #:####################################################################################:#
# ANNOUNCEMENT DEFAULTS #
#:####################################################################################:#
#
# These defaults apply when an appliance does not define its own values.
#
# A conservative default channel is retained to prevent additional
# notifications suddenly being enabled for every appliance.
#
# To make all implemented channels the default, this could instead be:
#
# am_default_announcement_channels: >-
# pushover_zorruno,pushover_kim,sony_tv_lounge,
# lounge_google_home_voice,mqtt_view_rd_feed,
# led_matrix_1,led_matrix_2
#
# Available channels:
# pushover_zorruno
# pushover_kim
# sony_tv_lounge
# lounge_google_home_voice
# google_home_voice
# lounge_touchscreen_voice
# mqtt_view_rd_feed
# led_matrix_1
# led_matrix_2
am_default_announcement_enabled: true
am_default_announcement_channels: "pushover_zorruno" am_default_announcement_channels: "pushover_zorruno"
am_default_announcement_payload: ""
# Optional appliance-specific overrides available: #:####################################################################################:#
# resolution: 4 # OPTIONAL PER-APPLIANCE OVERRIDES #
# The number of samples for the average, with one sample per minute. #:####################################################################################:#
# #
# standby_power: 3 # debug_flow: true
# Below this value, the appliance will show "Off". # Writes state-processing details to the Home Assistant logbook.
# Between this value and operating_power_min, it will show "Standby".
# #
# operating_power_min: 20 # resolution: 4
# The minimum average power needed for the appliance to show "Operating". # Number of one-minute samples used for the rolling average.
# #
# min_cycle_notify_seconds: 600 # standby_power: 3
# Finished cycles shorter than this are recorded, but no notification # Below this value, the appliance shows Off.
# is sent. Default is 300 seconds. # Between standby_power and operating_power_min, it shows Standby.
# #
# announcement_channels: "pushover_zorruno" # operating_power_min: 20
# Announcement channels for this appliance. # Minimum rolling-average power required for Operating.
# #
# long_announcement: "The device has finished. Please empty it when convenient." # min_cycle_notify_seconds: 600
# Override the long announcement. # Minimum cycle duration before a completion announcement is allowed.
# #
# min_cycle_notify_seconds: 600 # announcement_enabled: false
# Minimum time for an appliance to be operating before a completion announcement generated. # Disables cycle-completion announcements for this appliance.
#
# announcement_channels: >-
# pushover_zorruno,pushover_kim,sony_tv_lounge,
# lounge_google_home_voice,mqtt_view_rd_feed,
# led_matrix_1,led_matrix_2
# Comma-separated View Road announcement router channels.
#
# title: "Washing Machine Notification"
# Optional announcement title.
#
# short_announcement: "Washing complete"
# Used by TV, voice and LED matrix channels.
#
# long_announcement: "The washing machine has finished."
# Used by Pushover and other detailed channels.
# If omitted, the processor creates a detailed cycle summary.
#
# announcement_payload: ""
# Optional payload passed to the announcement router.
#
# matrix_indicator: "wash,1,15,10,6,5,1,0"
# LED matrix indicator data.
#
# mqtt_feed: "washing_machine"
# Activity-feed suffix used by mqtt_view_rd_feed.
# If omitted, appliance_id is used.
#
#:####################################################################################:#
########################################################################## #:####################################################################################:#
# APPLIANCE REGISTRY # APPLIANCE REGISTRY #
########################################################################## #:####################################################################################:#
am_appliances: 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 | tojson }},
"job_id": job_id, "job_name": {{ job_name | tojson }},
"job_name": job_name, "repeat": {{ repeat_type_safe | tojson }},
"repeat": repeat_type_safe, "due_time": {{ due_time_safe | tojson }},
"due_time": due_time_safe, "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 }},
"complete": {{ complete_bool | bool | tojson }},
"state": job_status | trim, "last_completed_ts": {{ last_completed_ts | float(0) }},
"complete": complete_bool | bool, "last_completed_time": {{ last_completed_time | tojson }},
"last_completed_human": {{ last_completed_human | tojson }},
"last_completed_ts": last_completed_ts | float(0), "previous_period_start_ts": {{ previous_period_start_ts | float(0) }},
"last_completed_time": last_completed_time, "previous_due_ts": {{ previous_due_ts | float(0) }},
"last_completed_human": last_completed_human, "previous_completed": {{ previous_completed_bool | bool | tojson }},
"previous_missed": {{ previous_missed_bool | bool | tojson }},
"completed_for_period_ts": current_period_start_ts | float(0) if complete_bool | bool else 0, "previous_missed_active": {{ previous_missed_active_bool | bool | tojson }},
"completed_for_due_ts": current_period_start_ts | float(0) if complete_bool | bool else 0, "missed": {{ missed_bool | bool | tojson }},
"completion_window_start": {{ completion_window_start_safe | tojson }},
"current_period_start_ts": current_period_start_ts | float(0), "completion_window_end": {{ completion_window_end_safe | tojson }},
"next_period_start_ts": next_period_start_ts | float(0), "has_completion_window": {{ has_completion_window | bool | tojson }},
"completion_window_missed": {{ completion_window_missed_bool | bool | tojson }},
"current_due_ts": current_due_ts | float(0), "current_completion_window_start_ts": {{ current_completion_window_start_ts | float(0) }},
"next_due_ts": next_due_ts | float(0), "current_completion_window_end_ts": {{ current_completion_window_end_ts | float(0) }},
"display_due_ts": display_due_ts | float(0), "completed_for_period_ts": {{ completed_for_period_ts_safe | float(0) }},
"next_due_human": next_due_human, "completed_for_due_ts": {{ completed_for_period_ts_safe | float(0) }},
"current_period_start_ts": {{ current_period_start_ts | float(0) }},
"due_started": due_started_bool | bool, "next_period_start_ts": {{ next_period_start_ts | float(0) }},
"overdue_started": overdue_started_bool | bool, "current_due_ts": {{ current_due_ts | float(0) }},
"next_due_ts": {{ next_due_ts | float(0) }},
"minutes_since_required": minutes_since_required | int(0), "display_due_ts": {{ display_due_ts | float(0) }},
"hours_since_required": hours_since_required | int(0), "next_due_human": {{ next_due_human | tojson }},
"due_started": {{ due_started_bool | bool | tojson }},
"overdue_after_minutes": overdue_after_minutes | int(0), "overdue_started": {{ overdue_started_bool | bool | tojson }},
"overdue_minutes": overdue_minutes | int(0), "minutes_since_required": {{ minutes_since_required | int(0) }},
"overdue_hours": overdue_hours | int(0), "hours_since_required": {{ hours_since_required | int(0) }},
"overdue_after_minutes": {{ overdue_after_minutes | int(0) }},
"hours_since_completed": hours_since_completed, "overdue_minutes": {{ overdue_minutes | int(0) }},
"overdue_hours": {{ overdue_hours | int(0) }},
"last_updated_local": (now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true), "hours_since_completed": {{ hours_since_completed | tojson }},
"mqtt_topic": topic_safe "last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }},
} | tojson "mqtt_topic": {{ topic_safe | tojson }}
}} }
+4
View File
@@ -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"
+6
View File
@@ -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
+277
View File
@@ -0,0 +1,277 @@
#:########################################################################################:#
# Open-Meteo Rain Chance Package #
#:########################################################################################:#
#
# TITLE:
# Open-Meteo Rain Chance
#
# FILE:
# open_meteo_rain_chance.yaml
#
# VERSION:
# V1.2 2026-07-19
# - Changed today's forecast period to use remaining daylight hours.
# - Before sunrise, today's forecast covers sunrise through sunset.
# - During daylight, today's forecast covers now through sunset.
# - At sunset or later, today's forecast covers now through midnight.
# - Tomorrow continues to cover daylight hours only.
#
# V1.1 2026-07-19
# - Changed today's prediction to use only the remaining forecast hours
# from the current local hour until midnight.
# - Changed tomorrow's prediction to cover daylight hours only.
# - Added hourly precipitation probability data.
# - Added today's and tomorrow's sunrise and sunset data.
#
# V1.0 2026-07-18
# - Initial version.
# - Used the full-day maximum precipitation probability for today and
# tomorrow.
#
# PURPOSE:
# Creates dedicated Home Assistant sensors for the maximum hourly
# probability of precipitation during useful forecast periods today and
# tomorrow.
#
# SOURCE:
# Open-Meteo Forecast API
#
# LOCATION:
# Uses the latitude and longitude attributes of zone.home.
#
# OUTPUT ENTITIES:
# sensor.open_meteo_rain_chance_today
# sensor.open_meteo_rain_chance_tomorrow
#
# FORECAST PERIODS:
# Today before sunrise:
# Sunrise through sunset.
#
# Today during daylight:
# Current time through sunset.
#
# Today at or after sunset:
# Current time through midnight.
#
# Tomorrow:
# Sunrise through sunset.
#
# UPDATE RATE:
# Every 30 minutes.
#
# NOTES:
# - No API key is required.
# - timezone=auto makes Open-Meteo return forecast times in the local
# timezone determined from the zone.home coordinates.
# - Each Open-Meteo hourly precipitation probability applies to the
# preceding hour.
# - Hourly forecast periods that partly overlap sunrise or sunset are
# included.
# - The displayed result is the highest hourly probability within the
# applicable period.
# - The result is not a separately calculated probability of receiving
# rain at any point across the whole selected period.
# - Sensor values and forecast-period changes are recalculated whenever
# the REST request updates.
#
#:########################################################################################:#
rest:
- resource_template: >-
https://api.open-meteo.com/v1/forecast?latitude={{ state_attr('zone.home', 'latitude') }}&longitude={{ state_attr('zone.home', 'longitude') }}&hourly=precipitation_probability&daily=sunrise,sunset&timezone=auto&forecast_days=2
method: GET
scan_interval: 1800
timeout: 20
sensor:
#:###################################################################################:#
# Rain Chance Today #
#:###################################################################################:#
#
# Before sunrise:
# Uses today's sunrise through sunset.
#
# During daylight:
# Uses the current time through today's sunset.
#
# At or after sunset:
# Uses the current time through midnight.
#
- name: "Open Meteo Rain Chance Today"
unique_id: open_meteo_rain_chance_today
unit_of_measurement: "%"
state_class: measurement
icon: mdi:weather-rainy
value_template: >-
{% set current_timestamp =
as_timestamp(now())
%}
{% set sunrise_timestamp =
as_timestamp(
value_json.daily.sunrise[0],
0
)
%}
{% set sunset_timestamp =
as_timestamp(
value_json.daily.sunset[0],
0
)
%}
{% set tomorrow_midnight_timestamp =
as_timestamp(
value_json.daily.time[1] ~ 'T00:00',
0
)
%}
{% if current_timestamp < sunrise_timestamp %}
{% set period_start_timestamp =
sunrise_timestamp
%}
{% set period_end_timestamp =
sunset_timestamp
%}
{% elif current_timestamp < sunset_timestamp %}
{% set period_start_timestamp =
current_timestamp
%}
{% set period_end_timestamp =
sunset_timestamp
%}
{% else %}
{% set period_start_timestamp =
current_timestamp
%}
{% set period_end_timestamp =
tomorrow_midnight_timestamp
%}
{% endif %}
{% set ns = namespace(
maximum=0,
found=false
) %}
{% for index in range(
value_json.hourly.time | count
) %}
{% set forecast_end_timestamp =
as_timestamp(
value_json.hourly.time[index],
0
)
%}
{% set probability =
value_json.hourly.precipitation_probability[index]
%}
{% if
forecast_end_timestamp > period_start_timestamp
and forecast_end_timestamp < period_end_timestamp + 3600
and probability is not none
%}
{% set ns.found = true %}
{% if probability | int(0) > ns.maximum %}
{% set ns.maximum =
probability | int(0)
%}
{% endif %}
{% endif %}
{% endfor %}
{{ ns.maximum if ns.found else 0 }}
#:###################################################################################:#
# Rain Chance Tomorrow — Daylight #
#:###################################################################################:#
#
# Uses hourly forecast periods that overlap tomorrow's daylight
# period between sunrise and sunset.
#
- name: "Open Meteo Rain Chance Tomorrow"
unique_id: open_meteo_rain_chance_tomorrow
unit_of_measurement: "%"
state_class: measurement
icon: mdi:weather-sunset-up
value_template: >-
{% set sunrise_timestamp =
as_timestamp(
value_json.daily.sunrise[1],
0
)
%}
{% set sunset_timestamp =
as_timestamp(
value_json.daily.sunset[1],
0
)
%}
{% set ns = namespace(
maximum=0,
found=false
) %}
{% for index in range(
value_json.hourly.time | count
) %}
{% set forecast_end_timestamp =
as_timestamp(
value_json.hourly.time[index],
0
)
%}
{% set probability =
value_json.hourly.precipitation_probability[index]
%}
{% if
forecast_end_timestamp > sunrise_timestamp
and forecast_end_timestamp < sunset_timestamp + 3600
and probability is not none
%}
{% set ns.found = true %}
{% if probability | int(0) > ns.maximum %}
{% set ns.maximum =
probability | int(0)
%}
{% endif %}
{% endif %}
{% endfor %}
{{ ns.maximum if ns.found else 0 }}
+81 -3
View File
@@ -1,6 +1,11 @@
################################################################################ ################################################################################
# PACKAGE: View Road Announcement Router # 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
########################################################################## ##########################################################################