Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ed1687503 | ||
|
|
87ffe648a5 | ||
|
|
0c420e445f | ||
|
|
26dcabbf8a | ||
|
|
a0c3271743 | ||
|
|
28a8cb98f6 | ||
|
|
92c5268dc8 | ||
|
|
010e828e9c | ||
|
|
6cc7212cef | ||
|
|
0f7585754d | ||
|
|
fefc2c8b5c | ||
|
|
3b1f4bbd75 | ||
|
|
887feaa50a | ||
|
|
2fe9bf0dd6 | ||
|
|
9b6eaa084a | ||
|
|
dcc2524785 | ||
|
|
16c1da4289 | ||
|
|
ee8f221d39 |
@@ -1 +1 @@
|
|||||||
2026.6.2
|
2026.7.2
|
||||||
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 924 B |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
{"timestamp": "2026-07-16T01:51:24.833520+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "input_boolean", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 265.3963565826416, "success": true, "error_message": null, "response_size_bytes": 2297, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T01:51:24.844401+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "input_select", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 234.4343662261963, "success": true, "error_message": null, "response_size_bytes": 472, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T01:51:24.849852+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "input_number", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 281.5418243408203, "success": true, "error_message": null, "response_size_bytes": 640, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T01:51:25.011117+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "timer", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 184.76414680480957, "success": true, "error_message": null, "response_size_bytes": 465, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T01:51:25.012519+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "counter", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 186.4025592803955, "success": true, "error_message": null, "response_size_bytes": 826, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T01:51:27.603591+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "input_boolean", "area_filter": null, "search_types": null, "limit": 10, "offset": 10, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 78.72748374938965, "success": true, "error_message": null, "response_size_bytes": 818, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T14:58:34.806520+00:00", "tool_name": "ha_list_floors_areas", "parameters": {"fields": null, "area_fields": null}, "execution_time_ms": 34.44170951843262, "success": true, "error_message": null, "response_size_bytes": 3757, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T15:17:46.049602+00:00", "tool_name": "ha_get_overview", "parameters": {"detail_level": "minimal", "domains": null, "limit": null, "offset": 0, "max_entities_per_domain": null, "include_state": null, "include_entity_id": null, "include_notifications": true, "include_dismissed_repairs": false, "fields": null}, "execution_time_ms": 1456.7010402679443, "success": true, "error_message": null, "response_size_bytes": 14815, "user_context": null}
|
||||||
|
{"timestamp": "2026-07-16T15:20:28.060870+00:00", "tool_name": "ha_search", "parameters": {"query": null, "domain_filter": "automation", "area_filter": null, "search_types": null, "limit": 10, "offset": 0, "exact_match": true, "include_hidden": true, "include_config": false, "group_by_domain": false, "per_domain_limit": null, "state_filter": null, "result_fields": null, "fields": null, "config_time_budget": null, "ctx": {"_STATE_TTL_SECONDS": 86400}}, "execution_time_ms": 113.4181022644043, "success": true, "error_message": null, "response_size_bytes": 2476, "user_context": null}
|
||||||
@@ -1 +1 @@
|
|||||||
{"pid": 71, "version": 1, "ha_version": "2026.6.2", "start_ts": 1781274194.4665778}
|
{"pid": 71, "version": 1, "ha_version": "2026.7.2", "start_ts": 1784644362.8406641}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Prettier Configuration for Home Assistant YAML
|
||||||
|
# Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# Prettier auto-formats YAML files written by OpenCode. This config aligns
|
||||||
|
# with the official Home Assistant YAML Style Guide where possible.
|
||||||
|
#
|
||||||
|
# WHAT PRETTIER ENFORCES:
|
||||||
|
# - 2-space indentation (no tabs)
|
||||||
|
# - Double quotes for strings (HA convention)
|
||||||
|
# - Trailing newline at end of file
|
||||||
|
# - Consistent spacing and formatting
|
||||||
|
# - Preserves HA custom tags (!include, !secret, !input, !env_var)
|
||||||
|
# - Preserves block style sequences and mappings
|
||||||
|
# - Preserves multiline strings (literal | and folded > styles)
|
||||||
|
# - Preserves comments
|
||||||
|
#
|
||||||
|
# WHAT PRETTIER DOES NOT ENFORCE (must be handled manually or by the AI):
|
||||||
|
# - Converting flow style [1, 2, 3] to block style sequences
|
||||||
|
# - Converting flow style { key: val } to block style mappings
|
||||||
|
# - Normalizing null/~ to implicit null (just "key:" with no value)
|
||||||
|
# - Normalizing truthy booleans (Yes/On/TRUE) to lowercase true/false
|
||||||
|
# - Wrapping long lines (templates, etc.)
|
||||||
|
#
|
||||||
|
# Users can customize this file. See: https://prettier.io/docs/en/options
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# HA YAML Style Guide: "An indentation of 2 spaces must be used"
|
||||||
|
tabWidth: 2
|
||||||
|
useTabs: false
|
||||||
|
|
||||||
|
# HA YAML Style Guide: "Strings are preferably quoted with double quotes"
|
||||||
|
singleQuote: false
|
||||||
|
|
||||||
|
# Keep printWidth generous to avoid prettier breaking long YAML keys into
|
||||||
|
# explicit key notation (?). HA templates can be long but should be manually
|
||||||
|
# split using literal (|) or folded (>) block scalars per the style guide.
|
||||||
|
printWidth: 120
|
||||||
|
|
||||||
|
# Preserve multiline string formatting (literal |, folded >, etc.)
|
||||||
|
# HA YAML Style Guide: "make use of the literal style and folded style strings"
|
||||||
|
proseWrap: "preserve"
|
||||||
@@ -0,0 +1,807 @@
|
|||||||
|
# Home Assistant OpenCode Rules
|
||||||
|
|
||||||
|
You are working directly within a Home Assistant installation. Your working directory is `/homeassistant`, which is the live Home Assistant configuration directory.
|
||||||
|
|
||||||
|
## CRITICAL: User Consent and Scope Rules
|
||||||
|
|
||||||
|
You MUST follow these rules strictly:
|
||||||
|
|
||||||
|
1. **Never exceed the user's request** - Do exactly what the user asks, nothing more. Do not "improve" or "enhance" beyond the stated scope.
|
||||||
|
|
||||||
|
2. **Never make changes without explicit approval** - Before modifying ANY file:
|
||||||
|
- Show the user exactly what you plan to change
|
||||||
|
- Wait for their explicit confirmation ("yes", "go ahead", "do it", etc.)
|
||||||
|
- If they haven't approved, DO NOT proceed
|
||||||
|
|
||||||
|
3. **Ask, don't assume** - If the user's request is ambiguous:
|
||||||
|
- Ask clarifying questions first
|
||||||
|
- Present options and let them choose
|
||||||
|
- Never guess at their intent
|
||||||
|
|
||||||
|
4. **Read-only by default** - When investigating or troubleshooting:
|
||||||
|
- Only read files and gather information
|
||||||
|
- Present findings and recommendations
|
||||||
|
- Wait for user instruction before making any changes
|
||||||
|
|
||||||
|
5. **One change at a time** - When making approved changes:
|
||||||
|
- Make the minimum change needed
|
||||||
|
- Show what was changed
|
||||||
|
- Let the user verify before proceeding to any next step
|
||||||
|
|
||||||
|
6. **No unsolicited modifications** - Never:
|
||||||
|
- "Clean up" code the user didn't ask about
|
||||||
|
- Add features they didn't request
|
||||||
|
- Refactor working configurations
|
||||||
|
- Fix issues they haven't mentioned
|
||||||
|
|
||||||
|
7. **Respect "no"** - If a user declines a suggestion, do not:
|
||||||
|
- Repeat the suggestion
|
||||||
|
- Make the change anyway
|
||||||
|
- Try to convince them otherwise
|
||||||
|
|
||||||
|
## Environment Context
|
||||||
|
|
||||||
|
- You are running inside the OpenCode app
|
||||||
|
- The current directory (`/homeassistant`) contains the live Home Assistant configuration
|
||||||
|
- Changes to YAML files here directly affect the Home Assistant instance
|
||||||
|
- If add-on folder access is enabled, `/addons` and `/addon_configs` are available for Home Assistant add-on development. Treat `/addon_configs` as sensitive and only inspect or modify these folders when the user explicitly asks.
|
||||||
|
- You may have access to MCP tools for interacting with Home Assistant (check with the user)
|
||||||
|
|
||||||
|
## Home Assistant Interaction Model
|
||||||
|
|
||||||
|
There are three primary, safe ways to interact with Home Assistant:
|
||||||
|
|
||||||
|
### 1. Configuration Files (YAML)
|
||||||
|
The standard way to define and customize Home Assistant behavior:
|
||||||
|
- Automations, scripts, scenes, and blueprints
|
||||||
|
- Integration and sensor configurations
|
||||||
|
- Templates, packages, and customizations
|
||||||
|
- Dashboard (Lovelace) definitions
|
||||||
|
|
||||||
|
These files are designed for user editing and are the source of truth for your Home Assistant setup.
|
||||||
|
|
||||||
|
### 2. MCP Tools (Runtime API)
|
||||||
|
Real-time interaction with the running Home Assistant instance:
|
||||||
|
- Query current entity states and history
|
||||||
|
- Control devices and call services
|
||||||
|
- Validate configurations
|
||||||
|
- Diagnose issues and detect anomalies
|
||||||
|
- Report OpenCode/HA agent capability status with `get_agent_capabilities`
|
||||||
|
|
||||||
|
### Native Home Assistant LLM Platform
|
||||||
|
Home Assistant is developing a native `llm` integration where Core integrations and custom integrations can expose curated tools through `<integration>/llm.py` and registered LLM APIs. New Home Assistant builds may also expose those APIs over native MCP endpoints such as `/api/mcp/<API ID>`; the built-in Assist API uses `/api/mcp/assist`. This is complementary to OpenCode MCP, not a replacement.
|
||||||
|
|
||||||
|
- If the optional `homeassistant_native` MCP server is available, prefer it for requests that fit the configured native Home Assistant LLM API because those tools are curated by Home Assistant.
|
||||||
|
- Use OpenCode MCP for configuration editing, safe writes, validation, admin/dev workflows, screenshots, updates, ESPHome, `hab`, Zigbee tasks, add-on development, and Home Assistant documentation lookup.
|
||||||
|
- Use `get_agent_capabilities` or `ha://agent/capabilities` to check whether the running HA instance reports the native `llm` component and native MCP endpoints.
|
||||||
|
- Use `get_home_context` for compact area/domain/entity understanding before broad state dumps.
|
||||||
|
- Use `get_ha_llm_development_guide` when helping develop or review a custom integration's native `<integration>/llm.py` provider.
|
||||||
|
- Do not assume this add-on can register tools directly with HA's native `llm` platform; native tool registration is internal to HA integrations/custom integrations. The add-on can consume configured native LLM APIs through native MCP when Home Assistant exposes them.
|
||||||
|
|
||||||
|
### 3. hab CLI (Home Assistant Builder)
|
||||||
|
A CLI tool designed for AI agents to manage Home Assistant. Run `hab` commands via the terminal:
|
||||||
|
- **Entity management**: `hab entity list`, `hab entity get light.living_room`, `hab entity logbook sensor.power --start 2h`
|
||||||
|
- **Service calls**: `hab action call light.turn_on --entity light.living_room --data '{"brightness": 200}'`
|
||||||
|
- **Automation CRUD**: `hab automation list`, `hab automation create`, `hab automation delete`
|
||||||
|
- **Dashboard management**: `hab dashboard list`, `hab dashboard view create`
|
||||||
|
- **Area/floor/zone/label**: `hab area list`, `hab area create "Kitchen"`
|
||||||
|
- **Helpers**: `hab helper list`, `hab helper create`
|
||||||
|
- **Scripts**: `hab script list`, `hab script create`
|
||||||
|
- **Scenes**: `hab scene list`, `hab scene create`, `hab scene activate "Movie Time"`
|
||||||
|
- **Blueprints**: `hab blueprint list`
|
||||||
|
- **Backups**: `hab backup list`, `hab backup create`
|
||||||
|
- **System**: `hab system info`, `hab system health`, `hab overview`
|
||||||
|
- **Devices**: `hab device list`
|
||||||
|
- **People**: `hab person list`, `hab person create`, `hab person update`
|
||||||
|
- **Categories**: `hab category list`, `hab category assign --entity light.kitchen`
|
||||||
|
- **To-do lists**: `hab todo list`, `hab todo item list todo.shopping`, `hab todo item add todo.shopping "Milk"`
|
||||||
|
- **Notifications**: `hab notification list`, `hab notification create --message "Hello" --title "Alert"`
|
||||||
|
- **Integrations**: `hab integration list`, `hab integration reload hue`, `hab integration disable mqtt`
|
||||||
|
- **Repairs**: `hab repairs list`, `hab repairs ignore <issue_id>`
|
||||||
|
- **Events**: `hab event list`, `hab event fire my_custom_event --data '{"key": "value"}'`
|
||||||
|
- **Templates**: `hab template render --expression "{{ states('sensor.temperature') }}"`
|
||||||
|
- **Search**: `hab search related`
|
||||||
|
|
||||||
|
`hab` outputs human-readable text by default. Use `--json` for structured JSON output (ideal for parsing).
|
||||||
|
`hab` is pre-authenticated via the Supervisor token - no login required.
|
||||||
|
Run `hab --help` or `hab <command> --help` for full usage details.
|
||||||
|
|
||||||
|
<!-- HAB_LIVE_HELP_START -->
|
||||||
|
```
|
||||||
|
Home Assistant Builder (hab) is a CLI utility designed for LLMs
|
||||||
|
to build and manage Home Assistant configurations.
|
||||||
|
|
||||||
|
Interactive sessions default to human-readable text. Non-interactive sessions default to JSON.
|
||||||
|
|
||||||
|
Start with 'hab guide' for workflow-level guidance optimized for LLM and agent usage.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
hab [command]
|
||||||
|
|
||||||
|
Getting Started:
|
||||||
|
auth Manage authentication
|
||||||
|
capability Inspect runtime capabilities
|
||||||
|
guide Display built-in usage guides
|
||||||
|
overview Show an overview of the Home Assistant instance
|
||||||
|
schema Show machine-readable command schema
|
||||||
|
|
||||||
|
Registry:
|
||||||
|
area Manage areas
|
||||||
|
device Manage devices
|
||||||
|
entity Manage entities
|
||||||
|
floor Manage floors
|
||||||
|
label Manage labels
|
||||||
|
person Manage persons
|
||||||
|
search Search for items and relationships
|
||||||
|
zone Manage zones
|
||||||
|
|
||||||
|
Automation:
|
||||||
|
action Call actions (services)
|
||||||
|
automation Manage automations
|
||||||
|
blueprint Manage blueprints
|
||||||
|
category Manage categories
|
||||||
|
helper Manage groups, templates, and other helpers
|
||||||
|
scene Manage scenes
|
||||||
|
script Manage scripts
|
||||||
|
|
||||||
|
Dashboard:
|
||||||
|
dashboard Manage dashboards
|
||||||
|
|
||||||
|
Other:
|
||||||
|
backup Manage backups
|
||||||
|
calendar Manage calendar events
|
||||||
|
diagnostics Manage diagnostics handlers
|
||||||
|
energy Manage energy dashboard settings
|
||||||
|
esphome Manage ESPHome devices
|
||||||
|
event Manage Home Assistant events
|
||||||
|
integration Manage integrations
|
||||||
|
network Manage network settings
|
||||||
|
notification Manage persistent notifications
|
||||||
|
repairs Manage Home Assistant repairs
|
||||||
|
system Manage system
|
||||||
|
template Work with Home Assistant templates
|
||||||
|
thread Manage Thread credentials
|
||||||
|
todo Manage to-do list items
|
||||||
|
update Update hab to the latest version
|
||||||
|
version Show version information
|
||||||
|
|
||||||
|
Additional Commands:
|
||||||
|
help Help about any command
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--config string Path to config directory (default: ~/.config/home-assistant-builder)
|
||||||
|
-h, --help help for hab
|
||||||
|
--json Use JSON output instead of human-readable text
|
||||||
|
--skip-update-check Skip automatic update check on startup
|
||||||
|
--text Use human-readable text output
|
||||||
|
--verbose Show verbose output
|
||||||
|
|
||||||
|
Use "hab [command] --help" for more information about a command.
|
||||||
|
```
|
||||||
|
<!-- HAB_LIVE_HELP_END -->
|
||||||
|
|
||||||
|
**Use configuration files when:** defining behavior, creating automations, setting up integrations
|
||||||
|
**Use MCP tools when:** checking current state, safe config writing, anomaly detection, entity diagnostics
|
||||||
|
**Use hab CLI when:** managing dashboards, areas, helpers, backups, blueprints, and bulk admin operations
|
||||||
|
|
||||||
|
### 4. zigporter CLI (Zigbee Toolkit)
|
||||||
|
A CLI for Zigbee device management in Home Assistant. Handles cascade renames (updating entity IDs across automations, scripts, scenes, and all Lovelace dashboards atomically), device inspection, stale device cleanup, and Zigbee mesh visualization.
|
||||||
|
|
||||||
|
**Cascade rename** — zigporter's unique value: when you rename an entity or device, it automatically patches every reference in automations, scripts, scenes, and Lovelace dashboards. `hab` can rename a single entity/device but does NOT cascade to references.
|
||||||
|
|
||||||
|
Key commands:
|
||||||
|
- **Cascade rename**: `zigporter rename-entity light.old_id light.new_id --apply`, `zigporter rename-device "Old Name" "New Name" --apply`
|
||||||
|
- **Device inventory**: `zigporter list-devices --json`, `zigporter list-z2m --json` (requires Z2M config)
|
||||||
|
- **Device inspection**: `zigporter inspect "Device Name" --json`, `zigporter inspect sensor.entity_id --json`
|
||||||
|
- **Stale device management**: `zigporter stale "Device" --action remove`, `zigporter stale "Device" --action ignore`
|
||||||
|
- **Post-migration cleanup**: `zigporter fix-device "Device" --apply`
|
||||||
|
- **Connectivity check**: `zigporter check`
|
||||||
|
- **ZHA export**: `zigporter export --output devices.json`
|
||||||
|
- **Mesh visualization**: `zigporter network-map --format table` (terminal), `zigporter network-map --output mesh.svg` (SVG file)
|
||||||
|
|
||||||
|
**Output format**: Use `--json` on listing/inspect commands for structured output (ideal for AI parsing). Rename commands output diffs and confirmation text.
|
||||||
|
|
||||||
|
zigporter is pre-authenticated via the Supervisor token. Z2M commands (`list-z2m`, `network-map --backend z2m`) require Z2M URL configuration in the add-on settings.
|
||||||
|
|
||||||
|
**Important limitations**:
|
||||||
|
- `rename-entity` / `rename-device` do NOT patch Jinja2 template expressions (e.g. `{{ states('old.id') }}`). A warning is printed listing affected files — inform the user these need manual review after renaming.
|
||||||
|
- The `migrate` command is inherently interactive (requires physical device actions) and must NOT be used by AI agents.
|
||||||
|
- Dry-run is the default for renames — always preview before using `--apply`.
|
||||||
|
|
||||||
|
<!-- ZIGPORTER_LIVE_HELP_START -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage: zigporter [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Migrate Zigbee devices between ZHA and Zigbee2MQTT. Supports both ZHA → Z2M
|
||||||
|
(default) and Z2M → ZHA (--direction z2m-to-zha).
|
||||||
|
|
||||||
|
╭─ Options ────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ --version -v Show version and exit. │
|
||||||
|
│ --install-completion Install completion for the current shell. │
|
||||||
|
│ --show-completion Show completion for the current shell, to │
|
||||||
|
│ copy it or customize the installation. │
|
||||||
|
│ --help -h Show this message and exit. │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
╭─ Commands ───────────────────────────────────────────────────────────────────╮
|
||||||
|
│ setup Create or update the configuration file in the zigporter │
|
||||||
|
│ config directory. │
|
||||||
|
│ check Verify that all requirements are in place before migrating. │
|
||||||
|
│ export Export current ZHA devices, entities, areas, and automation │
|
||||||
|
│ references to JSON. │
|
||||||
|
│ export-z2m Export current Z2M devices, entities, areas, and automation │
|
||||||
|
│ references to JSON. │
|
||||||
|
│ list-z2m List all devices currently paired with Zigbee2MQTT. │
|
||||||
|
│ list-devices List all Home Assistant devices across all integrations. │
|
||||||
|
│ migrate Interactive wizard to migrate devices between ZHA and │
|
||||||
|
│ Zigbee2MQTT. │
|
||||||
|
│ inspect Show all automations, scripts, scenes, and dashboard cards │
|
||||||
|
│ that depend on a device. │
|
||||||
|
│ rename-entity Rename an entity ID and update all references in automations, │
|
||||||
|
│ scripts, scenes, and dashboards. │
|
||||||
|
│ rename-device Rename a device and cascade the change to all its entities, │
|
||||||
|
│ automations, scripts, scenes, and dashboards. │
|
||||||
|
│ stale Identify and manage offline/stale devices across all │
|
||||||
|
│ integrations. │
|
||||||
|
│ fix-device Remove stale ZHA device entries left behind after migration │
|
||||||
|
│ to Zigbee2MQTT. │
|
||||||
|
│ network-map Show Zigbee mesh topology with signal strength (LQI) for each │
|
||||||
|
│ device. │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
```
|
||||||
|
<!-- ZIGPORTER_LIVE_HELP_END -->
|
||||||
|
|
||||||
|
**Use zigporter CLI when:** renaming entities/devices with cascade updates, inspecting Zigbee devices across integrations, cleaning up stale or post-migration devices, visualizing the Zigbee mesh
|
||||||
|
|
||||||
|
### 5. Internal Directories (OFF-LIMITS)
|
||||||
|
Home Assistant manages internal state in directories like `.storage/`. These are:
|
||||||
|
- Not designed for direct access
|
||||||
|
- Subject to change without notice
|
||||||
|
- Potentially dangerous to modify
|
||||||
|
|
||||||
|
**Never access internal directories directly - use configuration files or MCP tools instead.**
|
||||||
|
|
||||||
|
## RESTRICTED: Internal Home Assistant Directories
|
||||||
|
|
||||||
|
**NEVER read, modify, or directly interact with these internal directories:**
|
||||||
|
|
||||||
|
| Directory | Contains | Use Instead |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| `.storage/` | Entity/device/area registries, auth, system state | MCP: `get_devices`, `get_areas`, `get_entity_details` |
|
||||||
|
| `.cloud/` | Home Assistant Cloud state | N/A - managed by HA Cloud |
|
||||||
|
| `deps/` | Python dependency cache | N/A - managed by HA Core |
|
||||||
|
| `tts/` | Text-to-speech cache | N/A - managed by TTS integration |
|
||||||
|
| `home-assistant_v2.db` | History SQLite database | MCP: `get_history`, `get_logbook` |
|
||||||
|
| `home-assistant.log` | Raw system logs | MCP: `get_error_log` |
|
||||||
|
|
||||||
|
These contain internal Home Assistant state that:
|
||||||
|
1. Is managed exclusively by Home Assistant core
|
||||||
|
2. Can corrupt your installation if modified incorrectly
|
||||||
|
3. May be overwritten by Home Assistant at any time
|
||||||
|
4. Has no stable schema or format guarantees
|
||||||
|
|
||||||
|
**For information that seems to require internal access, there is always a proper alternative:**
|
||||||
|
- Need entity details? -> Read configuration files OR use `get_entity_details`
|
||||||
|
- Need device info? -> Use `get_devices` MCP tool
|
||||||
|
- Need to check history? -> Use `get_history` MCP tool
|
||||||
|
- Need to see errors? -> Use `get_error_log` MCP tool
|
||||||
|
|
||||||
|
## File Structure Knowledge
|
||||||
|
|
||||||
|
### Configuration Files (Primary Interface - Read/Write with User Approval)
|
||||||
|
These are the user-facing configuration files - the primary way to define Home Assistant behavior:
|
||||||
|
|
||||||
|
- `configuration.yaml` - Main configuration file
|
||||||
|
- `automations.yaml` - Automation definitions (if using UI or split config)
|
||||||
|
- `scripts.yaml` - Script definitions
|
||||||
|
- `scenes.yaml` - Scene definitions
|
||||||
|
- `secrets.yaml` - Sensitive values (NEVER commit or expose)
|
||||||
|
- `customize.yaml` - Entity customizations
|
||||||
|
- `groups.yaml` - Group definitions
|
||||||
|
- `packages/` - Package-based configuration splits
|
||||||
|
- `blueprints/` - Automation and script blueprints
|
||||||
|
- `custom_components/` - Custom integrations (HACS or manual)
|
||||||
|
- `www/` - Static files served at /local/
|
||||||
|
- `themes/` - Custom themes
|
||||||
|
- `*.yaml` in root - Any user-created YAML configuration
|
||||||
|
|
||||||
|
**These files are designed for editing** and are equally valid as MCP tools for research and changes.
|
||||||
|
|
||||||
|
### Internal Directories (OFF-LIMITS - Never Access Directly)
|
||||||
|
- `.storage/` - Internal registries and state (use MCP tools)
|
||||||
|
- `.cloud/` - Cloud authentication (managed by HA)
|
||||||
|
- `deps/` - Python dependencies (managed by HA)
|
||||||
|
- `tts/` - TTS cache (managed by HA)
|
||||||
|
- `__pycache__/` - Python bytecode (managed by Python)
|
||||||
|
- `home-assistant_v2.db` - History database (use MCP `get_history`)
|
||||||
|
- `home-assistant.log` - Logs (use MCP `get_error_log`)
|
||||||
|
|
||||||
|
## Working with YAML on the Command Line (`yq`)
|
||||||
|
|
||||||
|
To read, query, or convert YAML from the shell, use **`yq`** (the mikefarah/Go tool, pre-installed on `PATH`). It is the correct tool because it tolerates Home Assistant's custom tags — `!include`, `!secret`, `!env_var`, `!input`, and the `!include_dir_*` family.
|
||||||
|
|
||||||
|
**Do NOT reach for `python3 -c "import yaml"` (PyYAML) or Ruby's YAML for HA config** — both crash with a constructor error on the very first `!include`/`!secret`, because those tags are Home Assistant extensions, not standard YAML. `yq` parses them with no setup.
|
||||||
|
|
||||||
|
### Read / query (always safe — never errors on HA tags)
|
||||||
|
|
||||||
|
```
|
||||||
|
yq '.homeassistant.latitude' configuration.yaml # Print a nested value
|
||||||
|
yq '.automation | tag' configuration.yaml # Inspect the tag itself -> !include
|
||||||
|
yq 'keys' configuration.yaml # List top-level keys
|
||||||
|
yq -o=json '.' configuration.yaml | jq '.sensor' # Convert to JSON to pipe into jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: output and JSON conversion strip the tag — `!secret home_latitude` prints as `home_latitude`. Use `| tag` when you need to see the tag. Never round-trip a file *through* JSON and back; that permanently loses every `!include`/`!secret`.
|
||||||
|
|
||||||
|
### Writing / editing
|
||||||
|
|
||||||
|
Prefer the sanctioned write path: **`write_config_safe`** (MCP — validates, backs up, and blocks accidental content loss) or read the full file and use the editor. Reserve `yq -i` for quick, low-risk edits, and only with these two caveats in mind:
|
||||||
|
|
||||||
|
1. **A custom tag sticks to the value you overwrite.** `yq -i '.homeassistant.latitude = 52.37'` on a `!secret`-tagged node produces the corrupt `latitude: !secret 52.37`. When replacing a tagged value, reset the tag in the *same* expression:
|
||||||
|
```
|
||||||
|
yq -i '(.homeassistant.latitude tag = "") | .homeassistant.latitude = 52.37' configuration.yaml
|
||||||
|
```
|
||||||
|
To *add* a secret reference, set the tag explicitly: `yq -i '.http.api_key = "my_api_key" | .http.api_key tag = "!secret"' configuration.yaml`
|
||||||
|
2. **`yq -i` strips blank separator lines** (and collapses inline-comment spacing) across the whole file. No data is lost, but diffs are noisier. When a clean, minimal diff matters, use the editor instead.
|
||||||
|
|
||||||
|
### Validation is not a syntax check
|
||||||
|
|
||||||
|
`yq` only confirms YAML *parses*. To validate a Home Assistant *configuration* (resolving `!include`/`!secret` and checking integration schemas), use `check_config_syntax` / `write_config_safe` (MCP), not `yq`.
|
||||||
|
|
||||||
|
## YAML Style Guide (MANDATORY)
|
||||||
|
|
||||||
|
All YAML written or modified MUST follow the official Home Assistant YAML Style Guide.
|
||||||
|
Reference: https://developers.home-assistant.io/docs/documenting/yaml-style-guide/
|
||||||
|
|
||||||
|
A Prettier formatter is configured for this environment and will auto-format files on save.
|
||||||
|
However, Prettier only enforces a subset of the rules below. You are responsible for following
|
||||||
|
ALL rules, especially those Prettier cannot enforce (marked with *).
|
||||||
|
|
||||||
|
### Indentation
|
||||||
|
|
||||||
|
2 spaces. Tabs are forbidden.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
example:
|
||||||
|
one: 1
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
example:
|
||||||
|
bad: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Booleans *
|
||||||
|
|
||||||
|
Only `true` and `false` in lowercase. Never use `Yes`, `No`, `On`, `Off`, `TRUE`, etc.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
one: true
|
||||||
|
two: false
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
one: True
|
||||||
|
two: on
|
||||||
|
three: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Strings
|
||||||
|
|
||||||
|
Double quotes for strings. Single quotes are not allowed.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
example: "Hi there!"
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
example: 'Hi there!'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exceptions** (no quotes needed): entity IDs, area IDs, device IDs, platform types,
|
||||||
|
trigger types, condition types, action names, device classes, event names, attribute names,
|
||||||
|
and values from a fixed set of options (e.g., `mode`).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
actions:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.living_room
|
||||||
|
area_id: living_room
|
||||||
|
data:
|
||||||
|
message: "Hello!"
|
||||||
|
transition: 10
|
||||||
|
|
||||||
|
# Bad - don't quote entity IDs and action names
|
||||||
|
actions:
|
||||||
|
- action: "light.turn_on"
|
||||||
|
target:
|
||||||
|
entity_id: "light.living_room"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequences (Lists) *
|
||||||
|
|
||||||
|
Use block style. Flow style `[1, 2, 3]` must not be used.
|
||||||
|
Block sequences must be indented under their key.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
options:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
options: [1, 2, 3]
|
||||||
|
|
||||||
|
# Bad - not indented under key
|
||||||
|
options:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mappings *
|
||||||
|
|
||||||
|
Block style only. Flow style `{ key: val }` must not be used.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
example:
|
||||||
|
one: 1
|
||||||
|
two: 2
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
example: { one: 1, two: 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Null Values *
|
||||||
|
|
||||||
|
Use implicit null (just `key:` with no value). Never use `null` or `~`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
initial:
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
initial: null
|
||||||
|
initial: ~
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
Capitalized, with a space after `#`, indented to match current level.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
example:
|
||||||
|
# This is a comment
|
||||||
|
one: true
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
example:
|
||||||
|
# Comment at wrong indent
|
||||||
|
#Missing space
|
||||||
|
#lowercase start
|
||||||
|
one: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiline Strings
|
||||||
|
|
||||||
|
Use literal `|` (preserves newlines) or folded `>` (joins lines) block scalars.
|
||||||
|
Avoid `\n` in strings. Prefer no-chomp (`|`, `>`) unless you need strip (`|-`, `>-`).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
message: |
|
||||||
|
Hello!
|
||||||
|
This is a multiline
|
||||||
|
notification message.
|
||||||
|
|
||||||
|
# Good - folded
|
||||||
|
description: >
|
||||||
|
This is a long description that
|
||||||
|
will be joined into a single line.
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
message: "Hello!\nThis is a multiline\nnotification message.\n"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
Double quotes outside, single quotes inside. Use `states()` and `state_attr()`
|
||||||
|
helpers, not direct state object access. Split long templates across multiple lines.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
value_template: "{{ states('sensor.temperature') }}"
|
||||||
|
attribute_template: "{{ state_attr('climate.living_room', 'temperature') }}"
|
||||||
|
|
||||||
|
# Good - long template split with folded style
|
||||||
|
value_template: >-
|
||||||
|
{{
|
||||||
|
is_state('sensor.bedroom_co_status', 'Ok')
|
||||||
|
and is_state('sensor.kitchen_co_status', 'Ok')
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Bad - single quotes outside
|
||||||
|
value_template: '{{ "some_value" == other_value }}'
|
||||||
|
|
||||||
|
# Bad - direct state object access
|
||||||
|
value_template: "{{ states.sensor.temperature.state }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Action Targets *
|
||||||
|
|
||||||
|
Always use `target:` for entity/device/area targeting. Do not put `entity_id` at
|
||||||
|
the action level or inside `data:`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
actions:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.living_room
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
actions:
|
||||||
|
- action: light.turn_on
|
||||||
|
entity_id: light.living_room
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
actions:
|
||||||
|
- action: light.turn_on
|
||||||
|
data:
|
||||||
|
entity_id: light.living_room
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scalar vs List *
|
||||||
|
|
||||||
|
If a property accepts both, use a scalar for single values. Do not wrap a single
|
||||||
|
value in a list. Do not use comma-separated strings.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
entity_id: light.living_room
|
||||||
|
entity_id:
|
||||||
|
- light.living_room
|
||||||
|
- light.office
|
||||||
|
|
||||||
|
# Bad - single value in a list
|
||||||
|
entity_id:
|
||||||
|
- light.living_room
|
||||||
|
|
||||||
|
# Bad - comma separated
|
||||||
|
entity_id: "light.living_room, light.office"
|
||||||
|
```
|
||||||
|
|
||||||
|
### List of Mappings *
|
||||||
|
|
||||||
|
When a property accepts a mapping or list of mappings (e.g., `actions`, `conditions`),
|
||||||
|
always use a list even for a single item.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Good
|
||||||
|
actions:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.living_room
|
||||||
|
|
||||||
|
# Bad
|
||||||
|
actions:
|
||||||
|
action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.living_room
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Competencies
|
||||||
|
|
||||||
|
### YAML Configuration
|
||||||
|
- Follow the YAML Style Guide above for ALL configuration changes
|
||||||
|
- Use anchors (`&name`) and aliases (`*name`) for DRY configurations
|
||||||
|
- Understand `!include`, `!include_dir_named`, `!include_dir_list`, `!include_dir_merge_named`, `!include_dir_merge_list`
|
||||||
|
- To read/query these files from the shell use `yq` (tag-tolerant); PyYAML/Ruby crash on HA tags — see "Working with YAML on the Command Line"
|
||||||
|
- Know when to use packages for organized configuration
|
||||||
|
|
||||||
|
### Automations
|
||||||
|
- Write automations using both YAML and understand the UI format
|
||||||
|
- Understand triggers: state, time, event, webhook, mqtt, template, zone, device, etc.
|
||||||
|
- Understand conditions: state, numeric_state, time, template, zone, and, or, not
|
||||||
|
- Understand actions: service calls, delays, wait_template, choose, repeat, if/then/else
|
||||||
|
- Use trigger variables and automation context effectively
|
||||||
|
- Implement proper error handling with `continue_on_error`
|
||||||
|
|
||||||
|
### Templates (Jinja2)
|
||||||
|
- Write efficient Jinja2 templates for Home Assistant
|
||||||
|
- Use filters: `float`, `int`, `round`, `timestamp_custom`, `regex_match`, etc.
|
||||||
|
- Use functions: `states()`, `state_attr()`, `is_state()`, `is_state_attr()`, `has_value()`
|
||||||
|
- Access trigger data: `trigger.to_state`, `trigger.from_state`, `trigger.entity_id`
|
||||||
|
- Handle unavailable/unknown states gracefully
|
||||||
|
|
||||||
|
### Integrations
|
||||||
|
- Know common integrations and their configuration patterns
|
||||||
|
- Understand MQTT, REST, and template-based integrations
|
||||||
|
- Configure input_* helpers: input_boolean, input_number, input_select, input_text, input_datetime
|
||||||
|
- Set up utility_meter, statistics, and history_stats sensors
|
||||||
|
|
||||||
|
### Lovelace Dashboards
|
||||||
|
- Write Lovelace YAML configurations
|
||||||
|
- Know standard cards and their options
|
||||||
|
- Understand conditional cards, custom cards, and card-mod
|
||||||
|
- Configure views, themes, and resources
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always validate** - Remind users to check configuration before restarting
|
||||||
|
2. **Use secrets** - Never hardcode sensitive data; use `!secret` references
|
||||||
|
3. **Backup first** - Suggest backups before major changes
|
||||||
|
4. **Incremental changes** - Make small, testable changes
|
||||||
|
5. **Comments** - Add YAML comments explaining complex logic
|
||||||
|
6. **Naming conventions** - Use consistent entity_id naming (e.g., `sensor.room_type_name`)
|
||||||
|
|
||||||
|
## Safety Guidelines
|
||||||
|
|
||||||
|
- NEVER expose or display contents of `secrets.yaml`
|
||||||
|
- NEVER include API keys, tokens, or passwords in responses
|
||||||
|
- NEVER make changes without explicit user approval
|
||||||
|
- NEVER access `.storage/`, `.cloud/`, or other internal directories
|
||||||
|
- NEVER attempt to modify Home Assistant's internal databases or registries
|
||||||
|
- NEVER parse internal JSON files for entity/device/area information
|
||||||
|
- ALWAYS prefer MCP tools for querying runtime state over internal file access
|
||||||
|
- ALWAYS use `call_service` through MCP rather than modifying state files
|
||||||
|
- WARN users before changes that require restart vs reload
|
||||||
|
- SUGGEST backing up files before major modifications
|
||||||
|
- CHECK configuration validity when possible
|
||||||
|
- ALWAYS confirm with user before writing, editing, or deleting any file
|
||||||
|
|
||||||
|
## MCP Tools and Configuration Files
|
||||||
|
|
||||||
|
You have two complementary interfaces for working with Home Assistant:
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
Read and modify YAML files to understand and change Home Assistant's defined behavior:
|
||||||
|
- Review `automations.yaml` to understand existing automations
|
||||||
|
- Edit `configuration.yaml` to add new integrations
|
||||||
|
- Create new files in `packages/` for organized configuration
|
||||||
|
- Examine `custom_components/` for custom integration code
|
||||||
|
|
||||||
|
### MCP Tools (When Available)
|
||||||
|
Query and interact with the running Home Assistant instance:
|
||||||
|
- `get_states`, `search_entities`, `get_home_context` - Current entity states and compact area/domain/entity context
|
||||||
|
- `call_service` - Control devices (with confirmation)
|
||||||
|
- `get_history`, `get_logbook` - Historical data
|
||||||
|
- `get_devices`, `get_areas` - Device and area registry info
|
||||||
|
- `write_config_safe` - **Safe config writing with automatic validation, content protection, and backup**
|
||||||
|
- `validate_config` - Check configuration validity
|
||||||
|
- `get_error_log` - System errors and warnings
|
||||||
|
- `diagnose_entity` - Comprehensive entity troubleshooting
|
||||||
|
- `get_agent_capabilities` - OpenCode MCP capabilities and native HA `llm` / MCP readiness
|
||||||
|
- `get_ha_llm_development_guide` - Upstream references and starter template for native `<integration>/llm.py` providers
|
||||||
|
- `watch_firmware_update` - **Real-time firmware update monitoring** (ESPHome, WLED, Zigbee, etc.)
|
||||||
|
- `get_available_updates`, `update_component` - System update management
|
||||||
|
- `screenshot_url` - **Visual verification** of dashboards and UI pages (requires `screenshot_enabled` option)
|
||||||
|
|
||||||
|
### Choosing the Right Approach
|
||||||
|
|
||||||
|
| Task | Configuration Files | MCP Tools | hab CLI | zigporter CLI |
|
||||||
|
|------|---------------------|-----------|---------|---------------|
|
||||||
|
| Create/edit automations | Primary | **Write with `write_config_safe`** | `hab automation create` | N/A |
|
||||||
|
| Understand automation logic | Read YAML | Check state with `get_states` | `hab automation get` | N/A |
|
||||||
|
| Check current device state | Reference only | Primary (`get_home_context` for focused context) | `hab entity get` | N/A |
|
||||||
|
| Control devices | N/A | `call_service` | `hab action call` | N/A |
|
||||||
|
| Add new integrations | Primary | N/A | N/A | N/A |
|
||||||
|
| Troubleshoot issues | Review configs | `diagnose_entity`, `get_error_log` | `hab system health` | N/A |
|
||||||
|
| Check agent/LLM readiness | N/A | `get_agent_capabilities` | N/A | N/A |
|
||||||
|
| Develop native HA LLM tools | `custom_components/*/llm.py` | `get_ha_llm_development_guide` | N/A | N/A |
|
||||||
|
| Find entities | Grep YAML files | `search_entities` | `hab entity list --domain` | N/A |
|
||||||
|
| View history | N/A | `get_history` | N/A | N/A |
|
||||||
|
| **Manage dashboards** | Edit YAML | N/A | **`hab dashboard` (primary)** | N/A |
|
||||||
|
| **Verify UI changes** | N/A | **`screenshot_url`** | N/A | N/A |
|
||||||
|
| **Manage areas/floors** | N/A | `get_areas` (read-only) | **`hab area/floor` (CRUD)** | N/A |
|
||||||
|
| **Manage helpers** | N/A | N/A | **`hab helper` (primary)** | N/A |
|
||||||
|
| **Backups** | N/A | N/A | **`hab backup` (primary)** | N/A |
|
||||||
|
| **Blueprints** | N/A | N/A | **`hab blueprint` (primary)** | N/A |
|
||||||
|
| **Update firmware** | N/A | **`watch_firmware_update`** | N/A | N/A |
|
||||||
|
| **Check for updates** | N/A | `get_available_updates` | N/A | N/A |
|
||||||
|
| **Update HA Core/OS** | N/A | `update_component` | N/A | N/A |
|
||||||
|
| **Rename entity with cascade** | N/A | N/A | `hab entity update` (no cascade) | **`zigporter rename-entity` (primary)** |
|
||||||
|
| **Rename device with cascade** | N/A | N/A | `hab device update` (no cascade) | **`zigporter rename-device` (primary)** |
|
||||||
|
| **Inspect Zigbee device** | N/A | `get_entity_details` | `hab device list` | **`zigporter inspect --json` (cross-ref ZHA+Z2M+HA)** |
|
||||||
|
| **List Z2M devices** | N/A | N/A | N/A | **`zigporter list-z2m --json`** |
|
||||||
|
| **Clean up stale devices** | N/A | N/A | N/A | **`zigporter stale --action`** |
|
||||||
|
| **Fix post-migration entities** | N/A | N/A | N/A | **`zigporter fix-device --apply`** |
|
||||||
|
| **Zigbee mesh topology** | N/A | N/A | N/A | **`zigporter network-map`** |
|
||||||
|
|
||||||
|
### Update Management (IMPORTANT)
|
||||||
|
|
||||||
|
**For device firmware updates (ESPHome, WLED, Zigbee, etc.):**
|
||||||
|
Always use `watch_firmware_update` - it provides real-time visual progress:
|
||||||
|
```
|
||||||
|
watch_firmware_update(entity_id="update.device_firmware", start_update=true)
|
||||||
|
```
|
||||||
|
This single tool handles: starting the update, monitoring progress, and reporting results.
|
||||||
|
|
||||||
|
**For system updates (Core, OS, Supervisor, Apps):**
|
||||||
|
```
|
||||||
|
1. get_available_updates() -> Check what needs updating
|
||||||
|
2. update_component(component="core") -> Start update (returns job_id)
|
||||||
|
3. get_update_progress(job_id="...") -> Monitor progress
|
||||||
|
```
|
||||||
|
|
||||||
|
**Both approaches are valid and complementary.** Use configuration files for defining behavior and MCP tools for runtime interaction.
|
||||||
|
|
||||||
|
## Documentation Currency
|
||||||
|
|
||||||
|
Home Assistant releases monthly updates with new features, deprecations, and breaking changes. Your training data may be outdated. **Always verify configuration syntax against current documentation.**
|
||||||
|
|
||||||
|
### Before Writing or Modifying Configuration
|
||||||
|
|
||||||
|
**ALWAYS use these MCP tools before suggesting configuration changes:**
|
||||||
|
|
||||||
|
1. **Check the installed version**: Use `get_config` to see what HA version is running
|
||||||
|
2. **Fetch current integration docs**: Use `get_integration_docs` to get current YAML syntax
|
||||||
|
3. **Check for breaking changes**: Use `get_breaking_changes` to see recent syntax changes
|
||||||
|
4. **Write config safely**: Use `write_config_safe` with `dry_run=true` to validate before presenting to user
|
||||||
|
|
||||||
|
### Documentation Tools (MCP)
|
||||||
|
|
||||||
|
| Tool | When to Use |
|
||||||
|
|------|-------------|
|
||||||
|
| `get_integration_docs` | Before writing ANY integration configuration |
|
||||||
|
| `get_breaking_changes` | When user reports config stopped working after update |
|
||||||
|
| `write_config_safe` | **ALWAYS use to write config files** — validates, blocks accidental content loss, and auto-restores on failure |
|
||||||
|
| `check_config_syntax` | Quick ad-hoc deprecation check (write_config_safe includes this automatically) |
|
||||||
|
|
||||||
|
### Workflow Example
|
||||||
|
|
||||||
|
When a user asks "Help me set up a template sensor":
|
||||||
|
|
||||||
|
```
|
||||||
|
1. get_config() -> Check HA version (e.g., 2024.12.1)
|
||||||
|
2. get_integration_docs("template") -> Get current syntax and examples
|
||||||
|
3. read_file(path) -> Read the EXISTING file content first
|
||||||
|
4. Draft configuration: include ALL existing content + new changes
|
||||||
|
5. write_config_safe(path, yaml, dry_run=true) -> Pre-validate everything
|
||||||
|
6. If errors: fix and repeat step 5
|
||||||
|
7. Present validated config to user and get approval
|
||||||
|
8. write_config_safe(path, yaml) -> Write for real (auto backup + validation)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Deprecation Patterns
|
||||||
|
|
||||||
|
Be especially careful with these frequently-changed areas:
|
||||||
|
- **Template sensors/binary_sensors**: `platform: template` under `sensor:` is deprecated; use top-level `template:`
|
||||||
|
- **Entity configurations**: Many moved from YAML to UI-based config
|
||||||
|
- **Trigger-based templates**: Newer syntax preferred over legacy template sensors
|
||||||
|
- **Device triggers**: Syntax evolves with new device types
|
||||||
|
- **MQTT platform syntax**: `platform: mqtt` under domain keys is deprecated; use top-level `mqtt:` key
|
||||||
|
- **Direct state access**: `states.sensor.x.state` is fragile; use `states('sensor.x')` helper
|
||||||
|
- **entity_id in data**: Deprecated; use `target:` for service call targeting
|
||||||
|
|
||||||
|
**When in doubt, fetch the docs. Never rely solely on training data for configuration syntax.**
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Creating an Automation
|
||||||
|
1. **Read the existing `automations.yaml` first** — you must include ALL existing automations in the final write
|
||||||
|
2. Understand the goal and identify trigger conditions
|
||||||
|
3. Determine required entities (search if MCP available)
|
||||||
|
4. Draft the automation YAML with clear comments
|
||||||
|
5. **Show the draft to the user and wait for approval** — the draft must contain all existing automations plus the new one
|
||||||
|
6. Only write the file after explicit user confirmation
|
||||||
|
7. Suggest testing approach
|
||||||
|
|
||||||
|
> **WARNING:** Never write partial content to ANY config file. Always read the existing file first and include ALL existing content in your write. `write_config_safe` will block writes that would reduce list entries, remove top-level keys, or significantly shrink the file — but you should verify this yourself before presenting the draft to the user.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
1. Check entity states and history (via MCP if available)
|
||||||
|
2. Review relevant configuration files
|
||||||
|
3. Check Home Assistant logs for errors
|
||||||
|
4. Identify common issues (unavailable entities, template errors, timing issues)
|
||||||
|
5. **Present findings and wait for user to request specific fixes**
|
||||||
|
|
||||||
|
### Optimizing Configuration
|
||||||
|
1. Identify redundant or inefficient patterns
|
||||||
|
2. **Present recommendations to user**
|
||||||
|
3. Wait for user to approve specific changes
|
||||||
|
4. Implement only the changes the user explicitly approves
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Timestamp: 20260719_015459
|
||||||
|
Provider: gemini
|
||||||
|
Error: Expecting value: line 1 column 1 (char 0)
|
||||||
|
Response length: 208
|
||||||
|
Response bytes: b'The following lights are currently on: Bedroom Lights, GT-AX11000 LED, WiZ Dimmable White Bedroom 4, WiZ Dimmable White Bedroom 6, WiZ Dimmable White Bedroom 5, WiZ Dimmable White Bedroom 7, Oven1, and Oven2.'
|
||||||
|
Response repr: 'The following lights are currently on: Bedroom Lights, GT-AX11000 LED, WiZ Dimmable White Bedroom 4, WiZ Dimmable White Bedroom 6, WiZ Dimmable White Bedroom 5, WiZ Dimmable White Bedroom 7, Oven1, and Oven2.'
|
||||||
|
Full response:
|
||||||
|
The following lights are currently on: Bedroom Lights, GT-AX11000 LED, WiZ Dimmable White Bedroom 4, WiZ Dimmable White Bedroom 6, WiZ Dimmable White Bedroom 5, WiZ Dimmable White Bedroom 7, Oven1, and Oven2.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Timestamp: 20260719_015535
|
||||||
|
Provider: gemini
|
||||||
|
Error: Expecting value: line 1 column 1 (char 0)
|
||||||
|
Response length: 347
|
||||||
|
Response bytes: b'The target temperatures are set as follows:\n* **Living Room:** 23.1\xc2\xb0C (currently in cool mode)\n* **Basement:** 19\xc2\xb0C (currently in heat mode)\n* **VT Basement:** 7.0\xc2\xb0C (currently off)\n* **Climate Scheduler Climate Schedule Living Room:** 19\xc2\xb0C (currently off)\n* **Climate Scheduler Climate Schedule Basement:** 19\xc2\xb0C (currently in auto mode)'
|
||||||
|
Response repr: 'The target temperatures are set as follows:\n* **Living Room:** 23.1°C (currently in cool mode)\n* **Basement:** 19°C (currently in heat mode)\n* **VT Basement:** 7.0°C (currently off)\n* **Climate Scheduler Climate Schedule Living Room:** 19°C (currently off)\n* **Climate Scheduler Climate Schedule Basement:** 19°C (currently in auto mode)'
|
||||||
|
Full response:
|
||||||
|
The target temperatures are set as follows:
|
||||||
|
* **Living Room:** 23.1°C (currently in cool mode)
|
||||||
|
* **Basement:** 19°C (currently in heat mode)
|
||||||
|
* **VT Basement:** 7.0°C (currently off)
|
||||||
|
* **Climate Scheduler Climate Schedule Living Room:** 19°C (currently off)
|
||||||
|
* **Climate Scheduler Climate Schedule Basement:** 19°C (currently in auto mode)
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
blueprint:
|
||||||
|
name: Ikea BILRESA scroll wheel (sensors for scroll, events for press)
|
||||||
|
description: "Ikea BILRESA blueprint met 3 kanalen.\n\n- Scroll links/rechts gebruikt
|
||||||
|
sensor.*current_switch_position_* (pulse 0/1).\n Alleen rising edge (0->1) triggert,
|
||||||
|
dus per klik exact 1 stap en direct.\n- Druk op scrollwiel blijft event.* gebruiken
|
||||||
|
zodat single/double/long/hold blijft werken.\n"
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
remote:
|
||||||
|
name: Remote
|
||||||
|
description: Ikea BILRESA remote control device
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
multiple: false
|
||||||
|
channel1_section:
|
||||||
|
name: Kanaal 1
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch1:
|
||||||
|
name: 'Kanaal 1: click actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch1:
|
||||||
|
name: 'Kanaal 1: double click actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch1:
|
||||||
|
name: 'Kanaal 1: long press actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch1:
|
||||||
|
name: 'Kanaal 1: on-hold actie'
|
||||||
|
description: Herhaalt totdat losgelaten, zie on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch1:
|
||||||
|
name: 'Kanaal 1: scroll target(s)'
|
||||||
|
description: Light of media_player die je met het wiel wilt bedienen
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
scroll_wheel_mode_ch1:
|
||||||
|
name: 'Kanaal 1: scroll mode'
|
||||||
|
default: 'lights: dim'
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
channel2_section:
|
||||||
|
name: Kanaal 2
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch2:
|
||||||
|
name: 'Kanaal 2: click actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch2:
|
||||||
|
name: 'Kanaal 2: double click actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch2:
|
||||||
|
name: 'Kanaal 2: long press actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch2:
|
||||||
|
name: 'Kanaal 2: on-hold actie'
|
||||||
|
description: Herhaalt totdat losgelaten, zie on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch2:
|
||||||
|
name: 'Kanaal 2: scroll target(s)'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
scroll_wheel_mode_ch2:
|
||||||
|
name: 'Kanaal 2: scroll mode'
|
||||||
|
default: 'lights: dim'
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
channel3_section:
|
||||||
|
name: Kanaal 3
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch3:
|
||||||
|
name: 'Kanaal 3: click actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch3:
|
||||||
|
name: 'Kanaal 3: double click actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch3:
|
||||||
|
name: 'Kanaal 3: long press actie'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch3:
|
||||||
|
name: 'Kanaal 3: on-hold actie'
|
||||||
|
description: Herhaalt totdat losgelaten, zie on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch3:
|
||||||
|
name: 'Kanaal 3: scroll target(s)'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
scroll_wheel_mode_ch3:
|
||||||
|
name: 'Kanaal 3: scroll mode'
|
||||||
|
default: 'lights: dim'
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
global_section:
|
||||||
|
name: Globale instellingen
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
dim_step_pct:
|
||||||
|
name: Dimmer stapgrootte (%)
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
dim_min_pct:
|
||||||
|
name: Minimum brightness (%)
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
min_color_temp:
|
||||||
|
name: Minimum kleurtemperatuur (K)
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
max_color_temp:
|
||||||
|
name: Maximum kleurtemperatuur (K)
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_temp_step:
|
||||||
|
name: Kleurtemperatuur stap (K)
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_hue_step:
|
||||||
|
name: Hue stap (graden, 0..360)
|
||||||
|
default: 4
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_saturation:
|
||||||
|
name: Saturation (%)
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
on_hold_delay:
|
||||||
|
name: On-hold delay (sec)
|
||||||
|
default: 0.1
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 0.1
|
||||||
|
volume_step_pct:
|
||||||
|
name: Volume stap (%)
|
||||||
|
default: 2
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
volume_max_pct:
|
||||||
|
name: Max volume (%)
|
||||||
|
default: 50
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
source_url: https://raw.githubusercontent.com/BoGu5/ha-blueprints/a688916fc161affcb2577e1ed90bb4f7ecf778fb/kea%20BILRESA%20scroll%20wheel%20(sensors%20for%20scroll%2C%20events%20for%20press)
|
||||||
|
alias: Ikea_BILRESA_scroll_wheel
|
||||||
|
mode: queued
|
||||||
|
max: 50
|
||||||
|
max_exceeded: silent
|
||||||
|
trigger:
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_press_event_ch1 }}'
|
||||||
|
id: press_ch1
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_press_event_ch2 }}'
|
||||||
|
id: press_ch2
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_press_event_ch3 }}'
|
||||||
|
id: press_ch3
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_sensor_ch1 }}'
|
||||||
|
id: scroll_right_ch1
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_sensor_ch2 }}'
|
||||||
|
id: scroll_right_ch2
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_sensor_ch3 }}'
|
||||||
|
id: scroll_right_ch3
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_sensor_ch1 }}'
|
||||||
|
id: scroll_left_ch1
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_sensor_ch2 }}'
|
||||||
|
id: scroll_left_ch2
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_sensor_ch3 }}'
|
||||||
|
id: scroll_left_ch3
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{% set o = trigger.event.data.old_state %} {% set n = trigger.event.data.new_state
|
||||||
|
%} {{ o is not none and n is not none\n and o.state not in ['unavailable','unknown']\n
|
||||||
|
\ and n.state not in ['unavailable','unknown'] }}"
|
||||||
|
variables:
|
||||||
|
targets_entities_ch1: !input scroll_wheel_target_ch1
|
||||||
|
targets_entities_ch2: !input scroll_wheel_target_ch2
|
||||||
|
targets_entities_ch3: !input scroll_wheel_target_ch3
|
||||||
|
scroll_wheel_mode_ch1: !input scroll_wheel_mode_ch1
|
||||||
|
scroll_wheel_mode_ch2: !input scroll_wheel_mode_ch2
|
||||||
|
scroll_wheel_mode_ch3: !input scroll_wheel_mode_ch3
|
||||||
|
var_min_pct: !input dim_min_pct
|
||||||
|
var_dim_step_pct: !input dim_step_pct
|
||||||
|
var_color_hue_step: !input color_hue_step
|
||||||
|
var_color_saturation: !input color_saturation
|
||||||
|
var_min_color_temp: !input min_color_temp
|
||||||
|
var_max_color_temp: !input max_color_temp
|
||||||
|
var_color_temp_step: !input color_temp_step
|
||||||
|
var_on_hold_delay: !input on_hold_delay
|
||||||
|
var_volume_step_pct: !input volume_step_pct
|
||||||
|
var_volume_max_pct: !input volume_max_pct
|
||||||
|
action:
|
||||||
|
- variables:
|
||||||
|
channel: '{% if trigger.id in [''press_ch1'',''scroll_left_ch1'',''scroll_right_ch1'']
|
||||||
|
%}ch1 {% elif trigger.id in [''press_ch2'',''scroll_left_ch2'',''scroll_right_ch2'']
|
||||||
|
%}ch2 {% elif trigger.id in [''press_ch3'',''scroll_left_ch3'',''scroll_right_ch3'']
|
||||||
|
%}ch3 {% else %}unknown{% endif %}'
|
||||||
|
is_press: '{{ trigger.id is match(''^press_'') }}'
|
||||||
|
is_scroll: '{{ trigger.id is match(''^scroll_'') }}'
|
||||||
|
scroll_dir: '{% if trigger.id in [''scroll_left_ch1'',''scroll_left_ch2'',''scroll_left_ch3'']
|
||||||
|
%}left {% elif trigger.id in [''scroll_right_ch1'',''scroll_right_ch2'',''scroll_right_ch3'']
|
||||||
|
%}right {% else %}none{% endif %}'
|
||||||
|
pulse_rising: '{% set o = trigger.event.data.old_state.state %} {% set n = trigger.event.data.new_state.state
|
||||||
|
%} {{ (o in [''0'',''off''] and n in [''1'',''on'']) }}'
|
||||||
|
scroll_steps: '{{ 1 if (is_scroll and pulse_rising) else 0 }}'
|
||||||
|
press_event_type: "{% if is_press %}\n {{ trigger.event.data.new_state.attributes.event_type
|
||||||
|
| default('') }}\n{% else %}\n {{ '' }}\n{% endif %}"
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_press and press_event_type == ''multi_press_1'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ channel == ''ch1'' }}'
|
||||||
|
sequence: !input click_action_ch1
|
||||||
|
- conditions: '{{ channel == ''ch2'' }}'
|
||||||
|
sequence: !input click_action_ch2
|
||||||
|
- conditions: '{{ channel == ''ch3'' }}'
|
||||||
|
sequence: !input click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_press and press_event_type == ''multi_press_2'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ channel == ''ch1'' }}'
|
||||||
|
sequence: !input double_click_action_ch1
|
||||||
|
- conditions: '{{ channel == ''ch2'' }}'
|
||||||
|
sequence: !input double_click_action_ch2
|
||||||
|
- conditions: '{{ channel == ''ch3'' }}'
|
||||||
|
sequence: !input double_click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_press and press_event_type == ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ channel == ''ch1'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch1
|
||||||
|
- repeat:
|
||||||
|
while: "{{ repeat.index < 500\n and state_attr(var_press_event_ch1,
|
||||||
|
'event_type') == 'long_press' }}"
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch1
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- conditions: '{{ channel == ''ch2'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch2
|
||||||
|
- repeat:
|
||||||
|
while: "{{ repeat.index < 500\n and state_attr(var_press_event_ch2,
|
||||||
|
'event_type') == 'long_press' }}"
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch2
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- conditions: '{{ channel == ''ch3'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch3
|
||||||
|
- repeat:
|
||||||
|
while: "{{ repeat.index < 500\n and state_attr(var_press_event_ch3,
|
||||||
|
'event_type') == 'long_press' }}"
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch3
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_scroll and scroll_steps > 0 and scroll_dir in [''left'',''right'']
|
||||||
|
}}'
|
||||||
|
sequence:
|
||||||
|
- repeat:
|
||||||
|
for_each: '{{ (targets_entities_ch1 if channel == ''ch1'' else (targets_entities_ch2
|
||||||
|
if channel == ''ch2'' else targets_entities_ch3)) }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ (scroll_wheel_mode_ch1 if channel == ''ch1'' else
|
||||||
|
(scroll_wheel_mode_ch2 if channel == ''ch2'' else scroll_wheel_mode_ch3))
|
||||||
|
== ''lights: dim'' }}'
|
||||||
|
sequence:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_state(repeat.item, ''on'') }}'
|
||||||
|
then:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
brightness_step_pct: "{% if scroll_dir == 'right' %}\n {{ scroll_steps
|
||||||
|
* var_dim_step_pct }}\n{% else %}\n {{ -1 * ([scroll_steps *
|
||||||
|
var_dim_step_pct,\n state_attr(repeat.item, 'brightness')
|
||||||
|
* 100 / 254 - var_min_pct] | min) }}\n{% endif %}"
|
||||||
|
transition: 0.1
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{% set mode = (scroll_wheel_mode_ch1 if channel ==
|
||||||
|
'ch1' else (scroll_wheel_mode_ch2 if channel == 'ch2' else scroll_wheel_mode_ch3))
|
||||||
|
%} {{ mode == 'lights: color temp only'\n or (mode == 'lights: color
|
||||||
|
temp and color hue' and state_attr(repeat.item, 'color_temp_kelvin')
|
||||||
|
is not none) }}"
|
||||||
|
sequence:
|
||||||
|
- action: script.light_color_temp_helper
|
||||||
|
data:
|
||||||
|
mode: '{{ ''down'' if scroll_dir == ''left'' else ''up'' }}'
|
||||||
|
min_color_temp: '{{ var_min_color_temp }}'
|
||||||
|
max_color_temp: '{{ var_max_color_temp }}'
|
||||||
|
color_temp_step: '{{ var_color_temp_step * scroll_steps }}'
|
||||||
|
target_light: '{{ repeat.item }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{% set mode = (scroll_wheel_mode_ch1 if channel ==
|
||||||
|
'ch1' else (scroll_wheel_mode_ch2 if channel == 'ch2' else scroll_wheel_mode_ch3))
|
||||||
|
%} {{ mode == 'lights: color hue only'\n or (mode == 'lights: color
|
||||||
|
temp and color hue' and state_attr(repeat.item, 'color_temp_kelvin')
|
||||||
|
is none) }}"
|
||||||
|
sequence:
|
||||||
|
- action: script.light_color_hs_helper
|
||||||
|
data:
|
||||||
|
target_light: '{{ repeat.item }}'
|
||||||
|
color_hue_step: '{{ var_color_hue_step * scroll_steps }}'
|
||||||
|
color_saturation: '{{ var_color_saturation }}'
|
||||||
|
mode: '{{ ''down'' if scroll_dir == ''left'' else ''up'' }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ (scroll_wheel_mode_ch1 if channel == ''ch1'' else
|
||||||
|
(scroll_wheel_mode_ch2 if channel == ''ch2'' else scroll_wheel_mode_ch3))
|
||||||
|
== ''media: volume control'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: media_player.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
volume_level: "{{ [[\n (state_attr(repeat.item, 'volume_level')
|
||||||
|
| float(0)) * 100\n + (1 if scroll_dir == 'right' else -1) *
|
||||||
|
scroll_steps * var_volume_step_pct,\n var_volume_max_pct\n ]
|
||||||
|
| min, 0] | max / 100 }}"
|
||||||
|
trigger_variables:
|
||||||
|
var_remote: !input remote
|
||||||
|
var_press_event_ch1: "{% for item in device_entities(var_remote) %}\n {% set m
|
||||||
|
= item | regex_findall(find='^event\\\\..+_3$') %}\n {% if m | length > 0 %}{{
|
||||||
|
m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_press_event_ch2: "{% for item in device_entities(var_remote) %}\n {% set m
|
||||||
|
= item | regex_findall(find='^event\\\\..+_6$') %}\n {% if m | length > 0 %}{{
|
||||||
|
m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_press_event_ch3: "{% for item in device_entities(var_remote) %}\n {% set m
|
||||||
|
= item | regex_findall(find='^event\\\\..+_9$') %}\n {% if m | length > 0 %}{{
|
||||||
|
m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_scroll_right_sensor_ch1: "{% for item in device_entities(var_remote) %}\n {%
|
||||||
|
set m = item | regex_findall(find='^sensor\\\\..+current_switch_position_1$')
|
||||||
|
%}\n {% if m | length > 0 %}{{ m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_scroll_left_sensor_ch1: "{% for item in device_entities(var_remote) %}\n {%
|
||||||
|
set m = item | regex_findall(find='^sensor\\\\..+current_switch_position_2$')
|
||||||
|
%}\n {% if m | length > 0 %}{{ m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_scroll_right_sensor_ch2: "{% for item in device_entities(var_remote) %}\n {%
|
||||||
|
set m = item | regex_findall(find='^sensor\\\\..+current_switch_position_4$')
|
||||||
|
%}\n {% if m | length > 0 %}{{ m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_scroll_left_sensor_ch2: "{% for item in device_entities(var_remote) %}\n {%
|
||||||
|
set m = item | regex_findall(find='^sensor\\\\..+current_switch_position_5$')
|
||||||
|
%}\n {% if m | length > 0 %}{{ m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_scroll_right_sensor_ch3: "{% for item in device_entities(var_remote) %}\n {%
|
||||||
|
set m = item | regex_findall(find='^sensor\\\\..+current_switch_position_7$')
|
||||||
|
%}\n {% if m | length > 0 %}{{ m[0] }}{% endif %}\n{% endfor %}"
|
||||||
|
var_scroll_left_sensor_ch3: "{% for item in device_entities(var_remote) %}\n {%
|
||||||
|
set m = item | regex_findall(find='^sensor\\\\..+current_switch_position_8$')
|
||||||
|
%}\n {% if m | length > 0 %}{{ m[0] }}{% endif %}\n{% endfor %}"
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA BILRESA - Light Controller (v1.3)
|
||||||
|
description: 'Light control for IKEA BILRESA (Matter). Supports dynamic scrolling,
|
||||||
|
scene cycling, and multi-click actions for up to three channels.
|
||||||
|
|
||||||
|
'
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
remote_device:
|
||||||
|
name: Remote Control
|
||||||
|
description: The IKEA BILRESA scroll wheel (Matter) used as the trigger.
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
filter:
|
||||||
|
- manufacturer: IKEA of Sweden
|
||||||
|
model: BILRESA scroll wheel
|
||||||
|
multiple: false
|
||||||
|
header_1:
|
||||||
|
name: Channel 1
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_scroll_1:
|
||||||
|
name: Scroll Wheel Settings
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_light_1:
|
||||||
|
name: Light / Light Group
|
||||||
|
description: Select the light or group to be controlled.
|
||||||
|
default: {}
|
||||||
|
selector:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
input_mode_1:
|
||||||
|
name: Scroll Function
|
||||||
|
description: Define which value is changed when turning the wheel.
|
||||||
|
default: brightness
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Brightness
|
||||||
|
value: brightness
|
||||||
|
- label: Color
|
||||||
|
value: hs_color
|
||||||
|
- label: Color temperature
|
||||||
|
value: temp_kelvin
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
input_sens_1:
|
||||||
|
name: Multiplier
|
||||||
|
description: Define how much the value should change with each scroll step.
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 50.0
|
||||||
|
unit_of_measurement: x
|
||||||
|
mode: slider
|
||||||
|
step: 1.0
|
||||||
|
input_transition_1:
|
||||||
|
name: Transition duration
|
||||||
|
description: Define how long the transition between changes should take.
|
||||||
|
default: 200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 5000.0
|
||||||
|
unit_of_measurement: ms
|
||||||
|
mode: slider
|
||||||
|
step: 1.0
|
||||||
|
group_single_click_1:
|
||||||
|
name: Single Click
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_click_mode_1:
|
||||||
|
name: Click Mode
|
||||||
|
description: Choose between a single action or cycling through scenes.
|
||||||
|
default: action
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Perform single action
|
||||||
|
value: action
|
||||||
|
- label: Scene cycler
|
||||||
|
value: scene_cycle
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
input_scenes_1:
|
||||||
|
name: Scene Selection
|
||||||
|
description: Select the scenes to cycle through sequentially.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- scene
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
input_click_1:
|
||||||
|
name: Action on Click
|
||||||
|
description: Action to perform when in 'Single Action' mode.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
group_multi_click_1:
|
||||||
|
name: Additional Button Functions
|
||||||
|
icon: mdi:plus-circle-outline
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_double_1:
|
||||||
|
name: Double Click
|
||||||
|
description: Action to perform on double press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_triple_1:
|
||||||
|
name: Triple Click
|
||||||
|
description: Action to perform on triple press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_long_1:
|
||||||
|
name: Hold
|
||||||
|
description: Action to perform when holding the button.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_release_1:
|
||||||
|
name: Release
|
||||||
|
description: Action to perform when releasing the button after a long press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
header_2:
|
||||||
|
name: Channel 2
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_scroll_2:
|
||||||
|
name: Scroll Wheel Settings
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_light_2:
|
||||||
|
name: Light / Light Group
|
||||||
|
description: Select the light or group to be controlled.
|
||||||
|
default: {}
|
||||||
|
selector:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
input_mode_2:
|
||||||
|
name: Scroll Function
|
||||||
|
description: Define which value is changed when turning the wheel.
|
||||||
|
default: brightness
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Brightness
|
||||||
|
value: brightness
|
||||||
|
- label: Color
|
||||||
|
value: hs_color
|
||||||
|
- label: Color temperature
|
||||||
|
value: temp_kelvin
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
input_sens_2:
|
||||||
|
name: Multiplier
|
||||||
|
description: Define how much the value should change with each scroll step.
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 50.0
|
||||||
|
unit_of_measurement: x
|
||||||
|
mode: slider
|
||||||
|
step: 1.0
|
||||||
|
input_transition_2:
|
||||||
|
name: Transition duration
|
||||||
|
description: Define how long the transition between changes should take.
|
||||||
|
default: 200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 5000.0
|
||||||
|
unit_of_measurement: ms
|
||||||
|
mode: slider
|
||||||
|
step: 1.0
|
||||||
|
group_single_click_2:
|
||||||
|
name: Single Click
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_click_mode_2:
|
||||||
|
name: Click Mode
|
||||||
|
description: Choose between a single action or cycling through scenes.
|
||||||
|
default: action
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Perform single action
|
||||||
|
value: action
|
||||||
|
- label: Scene cycler
|
||||||
|
value: scene_cycle
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
input_scenes_2:
|
||||||
|
name: Scene Selection
|
||||||
|
description: Select the scenes to cycle through sequentially.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- scene
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
input_click_2:
|
||||||
|
name: Action on Click
|
||||||
|
description: Action to perform when in 'Single Action' mode.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
group_multi_click_2:
|
||||||
|
name: Additional Button Functions
|
||||||
|
icon: mdi:plus-circle-outline
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_double_2:
|
||||||
|
name: Double Click
|
||||||
|
description: Action to perform on double press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_triple_2:
|
||||||
|
name: Triple Click
|
||||||
|
description: Action to perform on triple press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_long_2:
|
||||||
|
name: Hold
|
||||||
|
description: Action to perform when holding the button.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_release_2:
|
||||||
|
name: Release
|
||||||
|
description: Action to perform when releasing the button after a long press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
header_3:
|
||||||
|
name: Channel 3
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_scroll_3:
|
||||||
|
name: Scroll Wheel Settings
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_light_3:
|
||||||
|
name: Light / Light Group
|
||||||
|
description: Select the light or group to be controlled.
|
||||||
|
default: {}
|
||||||
|
selector:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
input_mode_3:
|
||||||
|
name: Scroll Function
|
||||||
|
description: Define which value is changed when turning the wheel.
|
||||||
|
default: brightness
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Brightness
|
||||||
|
value: brightness
|
||||||
|
- label: Color
|
||||||
|
value: hs_color
|
||||||
|
- label: Color temperature
|
||||||
|
value: temp_kelvin
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
input_sens_3:
|
||||||
|
name: Multiplier
|
||||||
|
description: Define how much the value should change with each scroll step.
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 50.0
|
||||||
|
unit_of_measurement: x
|
||||||
|
mode: slider
|
||||||
|
step: 1.0
|
||||||
|
input_transition_3:
|
||||||
|
name: Transition duration
|
||||||
|
description: Define how long the transition between changes should take.
|
||||||
|
default: 200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 5000.0
|
||||||
|
unit_of_measurement: ms
|
||||||
|
mode: slider
|
||||||
|
step: 1.0
|
||||||
|
group_single_click_3:
|
||||||
|
name: Single Click
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_click_mode_3:
|
||||||
|
name: Click Mode
|
||||||
|
description: Choose between a single action or cycling through scenes.
|
||||||
|
default: action
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Perform single action
|
||||||
|
value: action
|
||||||
|
- label: Scene cycler
|
||||||
|
value: scene_cycle
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
input_scenes_3:
|
||||||
|
name: Scene Selection
|
||||||
|
description: Select the scenes to cycle through sequentially.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- scene
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
input_click_3:
|
||||||
|
name: Action on Click
|
||||||
|
description: Action to perform when in 'Single Action' mode.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
group_multi_click_3:
|
||||||
|
name: Additional Button Functions
|
||||||
|
icon: mdi:plus-circle-outline
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
input_double_3:
|
||||||
|
name: Double Click
|
||||||
|
description: Action to perform on double press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_triple_3:
|
||||||
|
name: Triple Click
|
||||||
|
description: Action to perform on triple press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_long_3:
|
||||||
|
name: Hold
|
||||||
|
description: Action to perform when holding the button.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
input_release_3:
|
||||||
|
name: Release
|
||||||
|
description: Action to perform when releasing the button after a long press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
source_url: https://gist.github.com/SvKRO/52f246362a19f0297fd18969b383181b
|
||||||
|
mode: single
|
||||||
|
max_exceeded: silent
|
||||||
|
trigger_variables:
|
||||||
|
var_remote: !input remote_device
|
||||||
|
var_events: '{{ device_entities(var_remote) | select(''match'', ''event\.'') | sort
|
||||||
|
| list }}
|
||||||
|
|
||||||
|
'
|
||||||
|
temp_kanal1_rechts: '{{ var_events[0] if var_events | length > 0 else ''none'' }}'
|
||||||
|
temp_kanal1_links: '{{ var_events[1] if var_events | length > 1 else ''none'' }}'
|
||||||
|
temp_kanal1_klick: '{{ var_events[2] if var_events | length > 2 else ''none'' }}'
|
||||||
|
temp_kanal2_rechts: '{{ var_events[3] if var_events | length > 3 else ''none'' }}'
|
||||||
|
temp_kanal2_links: '{{ var_events[4] if var_events | length > 4 else ''none'' }}'
|
||||||
|
temp_kanal2_klick: '{{ var_events[5] if var_events | length > 5 else ''none'' }}'
|
||||||
|
temp_kanal3_rechts: '{{ var_events[6] if var_events | length > 6 else ''none'' }}'
|
||||||
|
temp_kanal3_links: '{{ var_events[7] if var_events | length > 7 else ''none'' }}'
|
||||||
|
temp_kanal3_klick: '{{ var_events[8] if var_events | length > 8 else ''none'' }}'
|
||||||
|
trigger:
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal1_links }}'
|
||||||
|
id: trigger_kanal1_links
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal1_rechts }}'
|
||||||
|
id: trigger_kanal1_rechts
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal1_klick }}'
|
||||||
|
id: trigger_kanal1_klick
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal2_links }}'
|
||||||
|
id: trigger_kanal2_links
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal2_rechts }}'
|
||||||
|
id: trigger_kanal2_rechts
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal2_klick }}'
|
||||||
|
id: trigger_kanal2_klick
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal3_links }}'
|
||||||
|
id: trigger_kanal3_links
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal3_rechts }}'
|
||||||
|
id: trigger_kanal3_rechts
|
||||||
|
- trigger: event
|
||||||
|
event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ temp_kanal3_klick }}'
|
||||||
|
id: trigger_kanal3_klick
|
||||||
|
action:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{% if trigger.event.data.new_state is none %}\n false\n{% else
|
||||||
|
%}\n {% set old_attr = trigger.event.data.old_state.attributes if trigger.event.data.old_state
|
||||||
|
is not none else {} %}\n {% set new_attr = trigger.event.data.new_state.attributes
|
||||||
|
%}\n {% set old_ts = old_attr.get('last_reported', 'none') %}\n {% set new_ts
|
||||||
|
= new_attr.get('last_reported', 'new') %}\n {{ old_ts != new_ts }}\n{% endif
|
||||||
|
%}\n"
|
||||||
|
- variables:
|
||||||
|
m_1: !input input_mode_1
|
||||||
|
m_2: !input input_mode_2
|
||||||
|
m_3: !input input_mode_3
|
||||||
|
s_1: !input input_sens_1
|
||||||
|
s_2: !input input_sens_2
|
||||||
|
s_3: !input input_sens_3
|
||||||
|
l_1: !input input_light_1
|
||||||
|
l_2: !input input_light_2
|
||||||
|
l_3: !input input_light_3
|
||||||
|
tr_1: !input input_transition_1
|
||||||
|
tr_2: !input input_transition_2
|
||||||
|
tr_3: !input input_transition_3
|
||||||
|
action_kanal_id: '{{ trigger.id.split(''_'')[1] }}'
|
||||||
|
event_attr: '{{ trigger.event.data.new_state.attributes | to_json | from_json
|
||||||
|
}}'
|
||||||
|
ticks: '{{ event_attr.get(''totalNumberOfPressesCounted'') | default(1) | int
|
||||||
|
}}'
|
||||||
|
action_event_type: '{{ event_attr.get(''event_type'') | default(''none'') }}'
|
||||||
|
curr_mode: '{% if action_kanal_id == ''kanal1'' %}{{ m_1 }}{% elif action_kanal_id
|
||||||
|
== ''kanal2'' %}{{ m_2 }}{% else %}{{ m_3 }}{% endif %}'
|
||||||
|
base_sens: '{% if action_kanal_id == ''kanal1'' %}{{ s_1 }}{% elif action_kanal_id
|
||||||
|
== ''kanal2'' %}{{ s_2 }}{% else %}{{ s_3 }}{% endif %}'
|
||||||
|
step_val: '{% set val = base_sens * ticks %}{{ val if trigger.id.endswith(''_rechts'')
|
||||||
|
else val * -1 }}'
|
||||||
|
transition_ms: '{% if action_kanal_id == ''kanal1'' %}{{ tr_1 }}{% elif action_kanal_id
|
||||||
|
== ''kanal2'' %}{{ tr_2 }}{% else %}{{ tr_3 }}{% endif %}'
|
||||||
|
transition_s: '{{ transition_ms / 1000 }}'
|
||||||
|
curr_target_obj: '{% if action_kanal_id == ''kanal1'' %}{{ l_1 }} {% elif action_kanal_id
|
||||||
|
== ''kanal2'' %}{{ l_2 }} {% else %}{{ l_3 }}{% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
curr_target_list: "{% set t = curr_target_obj %} {% set ns = namespace(entities=[])
|
||||||
|
%}\n{% set e_ids = t.entity_id if t.entity_id is defined else [] %} {% set ns.entities
|
||||||
|
= [e_ids] if e_ids is string else e_ids %}\n{% set d_ids = t.device_id if t.device_id
|
||||||
|
is defined else [] %} {% set d_ids = [d_ids] if d_ids is string else d_ids %}
|
||||||
|
{% for d in d_ids %}\n {% set ns.entities = ns.entities + (device_entities(d)
|
||||||
|
| select('match', 'light\\.') | list) %}\n{% endfor %}\n{% set a_ids = t.area_id
|
||||||
|
if t.area_id is defined else [] %} {% set a_ids = [a_ids] if a_ids is string
|
||||||
|
else a_ids %} {% for a in a_ids %}\n {% set ns.entities = ns.entities + (area_entities(a)
|
||||||
|
| select('match', 'light\\.') | list) %}\n{% endfor %}\n{% set lb_ids = t.label_id
|
||||||
|
if t.label_id is defined else [] %} {% set lb_ids = [lb_ids] if lb_ids is string
|
||||||
|
else lb_ids %} {% for l in lb_ids %}\n {% set ns.entities = ns.entities + (label_entities(l)
|
||||||
|
| select('match', 'light\\.') | list) %}\n{% endfor %}\n{{ ns.entities | unique
|
||||||
|
| reject('none') | list }}\n"
|
||||||
|
curr_target: '{{ curr_target_list | first | default(''none'') }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ trigger.id.endswith(''_rechts'') or trigger.id.endswith(''_links'')
|
||||||
|
}}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ curr_mode == ''brightness'' }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_value: '{% set current = state_attr(curr_target, ''brightness'')
|
||||||
|
| int(128) %} {{ [([current + (step_val | int * 2.55 | int), 0] | max),
|
||||||
|
255] | min }}
|
||||||
|
|
||||||
|
'
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ curr_target_list }}'
|
||||||
|
data:
|
||||||
|
transition: '{{ transition_s }}'
|
||||||
|
brightness: '{{ target_value }}'
|
||||||
|
- conditions: '{{ curr_mode == ''temp_kelvin'' }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_value: '{% set curr = state_attr(curr_target, ''color_temp_kelvin'')
|
||||||
|
| int(3000) %} {% set min_v = state_attr(curr_target, ''min_color_temp_kelvin'')
|
||||||
|
| int(2000) %} {% set max_v = state_attr(curr_target, ''max_color_temp_kelvin'')
|
||||||
|
| int(6535) %} {{ [([curr + (step_val * 10) | int, min_v] | max), max_v]
|
||||||
|
| min }}
|
||||||
|
|
||||||
|
'
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ curr_target_list }}'
|
||||||
|
data:
|
||||||
|
transition: '{{ transition_s }}'
|
||||||
|
color_temp_kelvin: '{{ target_value }}'
|
||||||
|
- conditions: '{{ curr_mode == ''hs_color'' }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_value: '{% set hs = state_attr(curr_target, ''hs_color'') | default([0,
|
||||||
|
100], true) %} {{ [((hs[0] | float(0) + step_val) % 360) | round(2),
|
||||||
|
hs[1] | float(100)] }}
|
||||||
|
|
||||||
|
'
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ curr_target_list }}'
|
||||||
|
data:
|
||||||
|
transition: '{{ transition_s }}'
|
||||||
|
hs_color: '{{ target_value }}'
|
||||||
|
- conditions: '{{ trigger.id.endswith(''_klick'') }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
cm_1: !input input_click_mode_1
|
||||||
|
cm_2: !input input_click_mode_2
|
||||||
|
cm_3: !input input_click_mode_3
|
||||||
|
cs_1: !input input_scenes_1
|
||||||
|
cs_2: !input input_scenes_2
|
||||||
|
cs_3: !input input_scenes_3
|
||||||
|
c_mode: '{% if action_kanal_id == ''kanal1'' %}{{ cm_1 }}{% elif action_kanal_id
|
||||||
|
== ''kanal2'' %}{{ cm_2 }}{% else %}{{ cm_3 }}{% endif %}'
|
||||||
|
c_scenes: '{% if action_kanal_id == ''kanal1'' %}{{ cs_1 }}{% elif action_kanal_id
|
||||||
|
== ''kanal2'' %}{{ cs_2 }}{% else %}{{ cs_3 }}{% endif %}'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_event_type == ''multi_press_1'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ c_mode == ''scene_cycle'' and c_scenes | count > 0 }}'
|
||||||
|
sequence:
|
||||||
|
- service: scene.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{% set count = c_scenes | count %}{% set idx = (as_timestamp(now())
|
||||||
|
| int // 2) % count %}{{ c_scenes[idx] }}'
|
||||||
|
- conditions: '{{ c_mode == ''action'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal1'' }}'
|
||||||
|
sequence: !input input_click_1
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal2'' }}'
|
||||||
|
sequence: !input input_click_2
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal3'' }}'
|
||||||
|
sequence: !input input_click_3
|
||||||
|
- conditions: '{{ action_event_type == ''multi_press_2'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal1'' }}'
|
||||||
|
sequence: !input input_double_1
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal2'' }}'
|
||||||
|
sequence: !input input_double_2
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal3'' }}'
|
||||||
|
sequence: !input input_double_3
|
||||||
|
- conditions: '{{ action_event_type == ''multi_press_3'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal1'' }}'
|
||||||
|
sequence: !input input_triple_1
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal2'' }}'
|
||||||
|
sequence: !input input_triple_2
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal3'' }}'
|
||||||
|
sequence: !input input_triple_3
|
||||||
|
- conditions: '{{ action_event_type == ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal1'' }}'
|
||||||
|
sequence: !input input_long_1
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal2'' }}'
|
||||||
|
sequence: !input input_long_2
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal3'' }}'
|
||||||
|
sequence: !input input_long_3
|
||||||
|
- conditions: '{{ action_event_type == ''long_release'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal1'' }}'
|
||||||
|
sequence: !input input_release_1
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal2'' }}'
|
||||||
|
sequence: !input input_release_2
|
||||||
|
- conditions: '{{ action_kanal_id == ''kanal3'' }}'
|
||||||
|
sequence: !input input_release_3
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA BILRESA E2489 Dual Button (Matter)
|
||||||
|
author: censay
|
||||||
|
description: 'Full-featured automation for the IKEA BILRESA E2489 Matter dual-button
|
||||||
|
remote. Supports single press, double press, and long press (on press and on release)
|
||||||
|
for both buttons. Uses event entities exposed by Home Assistant for Matter devices.
|
||||||
|
Single-press actions are prioritized for reliability.
|
||||||
|
|
||||||
|
'
|
||||||
|
domain: automation
|
||||||
|
source_url: https://raw.githubusercontent.com/censay/haos-blueprints/refs/heads/master/ikea-bilresa-e2489-matter-smart-button/ikea-bilresa-e2489-matter-smart-button.yaml
|
||||||
|
homeassistant:
|
||||||
|
min_version: 2025.12.1
|
||||||
|
input:
|
||||||
|
target_device:
|
||||||
|
name: BILRESA Button Device
|
||||||
|
description: 'Select the IKEA BILRESA E2489 matter device.
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
filter:
|
||||||
|
- manufacturer: IKEA of Sweden
|
||||||
|
multiple: false
|
||||||
|
button1_event:
|
||||||
|
name: Button 1 – Event Entity
|
||||||
|
description: 'Select the event entity for Button 1 (for example: event.bilresa_dual_button_button_1_2).
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- event
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
button2_event:
|
||||||
|
name: Button 2 – Event Entity
|
||||||
|
description: 'Select the event entity for Button 2 (for example: event.bilresa_dual_button_button_2_2).
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- event
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
button1_single:
|
||||||
|
name: Button 1 – Single Press
|
||||||
|
description: Action for Button 1 single press (multi_press_1).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_double:
|
||||||
|
name: Button 1 – Double Press
|
||||||
|
description: Action for Button 1 double press (multi_press_2).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_long_press:
|
||||||
|
name: Button 1 – Long Press (on press)
|
||||||
|
description: Action for Button 1 long press start (long_press).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_long:
|
||||||
|
name: Button 1 – Long Press (on release)
|
||||||
|
description: Action for Button 1 long press completion (long_release).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_single:
|
||||||
|
name: Button 2 – Single Press
|
||||||
|
description: Action for Button 2 single press (multi_press_1).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_double:
|
||||||
|
name: Button 2 – Double Press
|
||||||
|
description: Action for Button 2 double press (multi_press_2).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_long_press:
|
||||||
|
name: Button 2 – Long Press (on press)
|
||||||
|
description: Action for Button 2 long press start (long_press).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_long:
|
||||||
|
name: Button 2 – Long Press (on release)
|
||||||
|
description: Action for Button 2 long press completion (long_release).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
variables:
|
||||||
|
button1_entity: !input button1_event
|
||||||
|
button2_entity: !input button2_event
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
id: button1
|
||||||
|
entity_id: !input button1_event
|
||||||
|
not_from: unavailable
|
||||||
|
not_to: unavailable
|
||||||
|
- platform: state
|
||||||
|
id: button2
|
||||||
|
entity_id: !input button2_event
|
||||||
|
not_from: unavailable
|
||||||
|
not_to: unavailable
|
||||||
|
condition: []
|
||||||
|
action:
|
||||||
|
- variables:
|
||||||
|
press_type: '{{ trigger.to_state.attributes.event_type }}'
|
||||||
|
trigger_id: '{{ trigger.id }}'
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''multi_press_1''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_single
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''multi_press_2''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_double
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''long_press''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_long_press
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''long_release''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_long
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''multi_press_1''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_single
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''multi_press_2''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_double
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''long_press''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_long_press
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''long_release''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_long
|
||||||
|
mode: restart
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
blueprint:
|
||||||
|
name: Zigbee2MQTT - IKEA Bilresa Dual Button
|
||||||
|
description: Control actions using an IKEA Bilresa remote via Zigbee2MQTT based on MQTT topic payloads.
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
remote_name:
|
||||||
|
name: Zigbee2MQTT Friendly Name
|
||||||
|
description: The friendly name of the Bilresa remote in Zigbee2MQTT (used for the MQTT topic).
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
on_click:
|
||||||
|
name: Top Button - Single Click
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_double_click:
|
||||||
|
name: Top Button - Double Click
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold:
|
||||||
|
name: Top Button - Long Press (Hold)
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
off_click:
|
||||||
|
name: Bottom Button - Single Click
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
off_double_click:
|
||||||
|
name: Bottom Button - Double Click
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
off_hold:
|
||||||
|
name: Bottom Button - Long Press (Hold)
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
stop_hold:
|
||||||
|
name: Button Released (Stop)
|
||||||
|
description: Action to run when a long press is released.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
- platform: mqtt
|
||||||
|
topic: "{{ 'zigbee2mqtt/' ~ remote_id if '/' not in remote_id else remote_id }}"
|
||||||
|
# We dynamically evaluate the topic in the trigger instead of matching in conditions
|
||||||
|
# to avoid waking up the automation for unrelated MQTT traffic.
|
||||||
|
|
||||||
|
variables:
|
||||||
|
remote_id: !input remote_name
|
||||||
|
expected_topic: "{{ remote_id if '/' in remote_id else 'zigbee2mqtt/' ~ remote_id }}"
|
||||||
|
|
||||||
|
# Fallback trigger if the dynamic template topic has constraints in older versions
|
||||||
|
# Using wildcard MQTT trigger and filtering by topic in conditions for maximum compatibility:
|
||||||
|
trigger:
|
||||||
|
- platform: mqtt
|
||||||
|
topic: "zigbee2mqtt/#"
|
||||||
|
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: >-
|
||||||
|
{{ trigger.topic == expected_topic and trigger.payload_json.action is defined }}
|
||||||
|
|
||||||
|
action:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'on' }}"
|
||||||
|
sequence: !input on_click
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'on_double' }}"
|
||||||
|
sequence: !input on_double_click
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'brightness_move_up' }}"
|
||||||
|
sequence: !input on_hold
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'off' }}"
|
||||||
|
sequence: !input off_click
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'off_double' }}"
|
||||||
|
sequence: !input off_double_click
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'brightness_move_down' }}"
|
||||||
|
sequence: !input off_hold
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload_json.action == 'brightness_stop' }}"
|
||||||
|
sequence: !input stop_hold
|
||||||
|
mode: restart
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA Bilresa - High-Performance Wheel Control
|
||||||
|
description: Optimized control for the IKEA Bilresa wheel focusing on speed and
|
||||||
|
MG24/Ember stability.
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
controller_name:
|
||||||
|
name: Remote MQTT Name
|
||||||
|
description: The name of the device in Zigbee2MQTT (e.g., Bilresa_Wheel)
|
||||||
|
default: Bilresa Wheel
|
||||||
|
action_on:
|
||||||
|
name: 'On'
|
||||||
|
description: Action to perform when the top button is pressed.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
action_off:
|
||||||
|
name: 'Off'
|
||||||
|
description: Action to perform when the bottom button is pressed.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
action_right:
|
||||||
|
name: Turn Right
|
||||||
|
description: 'Action for clockwise rotation. Recommended: light.turn_on with
|
||||||
|
brightness_step_pct: 10 and transition: 0.1'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
action_left:
|
||||||
|
name: Turn Left
|
||||||
|
description: 'Action for counter-clockwise rotation. Recommended: light.turn_on
|
||||||
|
with brightness_step_pct: -10 and transition: 0.1'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
source_url: https://gist.githubusercontent.com/patt110/4eda20da78422b8989b3b6686264e2ac/raw/a80245848aea86d29668015fc014b13d032569f0/Bilresa_Scrollwheel_Z2M_en.yaml
|
||||||
|
mode: restart
|
||||||
|
trigger:
|
||||||
|
- platform: mqtt
|
||||||
|
topic: zigbee2mqtt/{{ (controller_name if controller_name else 'Bilresa Wheel')
|
||||||
|
}}
|
||||||
|
action:
|
||||||
|
- variables:
|
||||||
|
payload: '{{ trigger.payload_json }}'
|
||||||
|
action_val: '{{ payload.action if ''action'' in payload else '''' }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_val == ''on'' }}'
|
||||||
|
sequence: !input action_on
|
||||||
|
- conditions: '{{ action_val == ''off'' }}'
|
||||||
|
sequence: !input action_off
|
||||||
|
- conditions: '{{ action_val == ''brightness_move_to_level'' }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
is_max: '{{ payload.action_brightness_delta is none and payload.action_level
|
||||||
|
is none }}'
|
||||||
|
is_min: '{{ payload.action_brightness_delta == 0 and payload.action_level
|
||||||
|
== 1 }}'
|
||||||
|
delta: '{{ payload.action_brightness_delta | int(0) if payload.action_brightness_delta
|
||||||
|
is not none else 0 }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ delta > 0 or is_max }}'
|
||||||
|
sequence: !input action_right
|
||||||
|
- conditions: '{{ delta < 0 or is_min }}'
|
||||||
|
sequence: !input action_left
|
||||||
|
- delay:
|
||||||
|
milliseconds: 30
|
||||||
@@ -0,0 +1,908 @@
|
|||||||
|
blueprint:
|
||||||
|
name: Ikea_bilresa_scroll_wheel
|
||||||
|
description: 'Blueprint for Ikea Bilresa Matter (button and scroll wheel for 3 different
|
||||||
|
channels)
|
||||||
|
|
||||||
|
Version: 2026-04-06
|
||||||
|
|
||||||
|
Info: https://community.home-assistant.io/t/ikea-bilresa-scroll-wheel-blueprint-matter/965365
|
||||||
|
|
||||||
|
'
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
remote:
|
||||||
|
name: Remote
|
||||||
|
description: Ikea Bilresa scroll wheel remote control
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
filter:
|
||||||
|
- manufacturer: IKEA of Sweden
|
||||||
|
model: BILRESA scroll wheel
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
channel1_section:
|
||||||
|
name: Channel 1
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch1:
|
||||||
|
name: 'Channel 1: <click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch1:
|
||||||
|
name: 'Channel 1: <double-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
triple_click_action_ch1:
|
||||||
|
name: 'Channel 1: <triple-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch1:
|
||||||
|
name: 'Channel 1: <long-click> action'
|
||||||
|
description: ''
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch1:
|
||||||
|
name: 'Channel 1: <on-hold> action'
|
||||||
|
description: repeat action until button is released, see also on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch1:
|
||||||
|
name: 'Channel 1: Scroll wheel target entity'
|
||||||
|
description: 'Light or media player to be controlled by scroll wheel for
|
||||||
|
channel 1
|
||||||
|
|
||||||
|
- empty for <user defined> scroll wheel mode
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
default: []
|
||||||
|
scroll_wheel_mode_ch1:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
- 'dynamic: choose from input_select'
|
||||||
|
- user defined
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
default: 'lights: dim'
|
||||||
|
name: 'Channel 1: Scroll wheel mode'
|
||||||
|
description: Select attribute of target entity controlled by scroll wheel
|
||||||
|
scroll_wheel_mode_dynamic_ch1:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- input_select
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
default:
|
||||||
|
name: 'Channel 1: Scroll wheel mode input_select'
|
||||||
|
description: 'For scroll wheel mode <dynamic> only, leave empty otherwise.
|
||||||
|
|
||||||
|
Create and choose an ''input_select'' containing the scroll wheel modes
|
||||||
|
you want to choose from dynamically, e.g.
|
||||||
|
|
||||||
|
- "lights: dim"
|
||||||
|
|
||||||
|
- "lights: color temp only"'
|
||||||
|
scroll_wheel_mode_ext_ch1:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- relaxed
|
||||||
|
- instant
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
default: relaxed
|
||||||
|
name: 'Channel 1: Scroll wheel evaluation mode'
|
||||||
|
description: '<relaxed>: one action call after scrolling
|
||||||
|
|
||||||
|
<instant>: many action calls while scrolling
|
||||||
|
|
||||||
|
|
||||||
|
Beware:
|
||||||
|
|
||||||
|
for <instant> mode the 9 hidden sensor entities of Bilresa device have
|
||||||
|
to be enabled
|
||||||
|
|
||||||
|
'
|
||||||
|
scroll_wheel_user_action_ch1:
|
||||||
|
name: 'Channel 1: User defined scroll wheel action'
|
||||||
|
description: 'For scroll wheel mode <user defined> only
|
||||||
|
|
||||||
|
Exposed variables:
|
||||||
|
|
||||||
|
- <scroll_clicks>: for <instant> mode always 1, for <relaxed> mode number
|
||||||
|
between 1 to 8
|
||||||
|
|
||||||
|
- <scroll_direction>: ''left'' or ''right''
|
||||||
|
|
||||||
|
'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
channel2_section:
|
||||||
|
name: Channel 2
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch2:
|
||||||
|
name: 'Channel 2: <click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch2:
|
||||||
|
name: 'Channel 2: <double-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
triple_click_action_ch2:
|
||||||
|
name: 'Channel 2: <triple-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch2:
|
||||||
|
name: 'Channel 2: <long-click> action'
|
||||||
|
description: ''
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch2:
|
||||||
|
name: 'Channel 2: <on-hold> action'
|
||||||
|
description: repeat action until button is released, see also on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch2:
|
||||||
|
name: 'Channel 2: Scroll wheel target entity'
|
||||||
|
description: 'Light or media player to be controlled by scroll wheel for
|
||||||
|
channel 2
|
||||||
|
|
||||||
|
- empty for <user defined> scroll wheel mode
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
default: []
|
||||||
|
scroll_wheel_mode_ch2:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
- 'dynamic: choose from input_select'
|
||||||
|
- user defined
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
default: 'lights: dim'
|
||||||
|
name: 'Channel 2: Scroll wheel mode'
|
||||||
|
description: Select attribute of target entity controlled by scroll wheel
|
||||||
|
scroll_wheel_mode_dynamic_ch2:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- input_select
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
default:
|
||||||
|
name: 'Channel 2: Scroll wheel mode input_select'
|
||||||
|
description: 'For scroll wheel mode <dynamic> only, leave empty otherwise.
|
||||||
|
|
||||||
|
Create and choose an ''input_select'' containing the scroll wheel modes
|
||||||
|
you want to choose from dynamically, e.g.
|
||||||
|
|
||||||
|
- "lights: dim"
|
||||||
|
|
||||||
|
- "lights: color temp only"'
|
||||||
|
scroll_wheel_mode_ext_ch2:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- relaxed
|
||||||
|
- instant
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
default: relaxed
|
||||||
|
name: 'Channel 2: Scroll wheel evaluation mode'
|
||||||
|
description: '- <relaxed>: one action call after scrolling
|
||||||
|
|
||||||
|
- <instant>: many action calls while scrolling
|
||||||
|
|
||||||
|
|
||||||
|
Beware:
|
||||||
|
|
||||||
|
- for <instant> mode the 9 hidden sensor entities of Bilresa device have
|
||||||
|
to be enabled
|
||||||
|
|
||||||
|
'
|
||||||
|
scroll_wheel_user_action_ch2:
|
||||||
|
name: 'Channel 2: User defined scroll wheel action'
|
||||||
|
description: 'For scroll wheel mode <user defined> only
|
||||||
|
|
||||||
|
Exposed variables:
|
||||||
|
|
||||||
|
- <scroll_clicks>: for <instant> mode always 1, for <relaxed> mode number
|
||||||
|
between 1 to 8
|
||||||
|
|
||||||
|
- <scroll_direction>: ''left'' or ''right''
|
||||||
|
|
||||||
|
'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
channel3_section:
|
||||||
|
name: Channel 3
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch3:
|
||||||
|
name: 'Channel 3: <click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch3:
|
||||||
|
name: 'Channel 3: <double-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
triple_click_action_ch3:
|
||||||
|
name: 'Channel 3: <triple-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch3:
|
||||||
|
name: 'Channel 3: <long-click> action'
|
||||||
|
description: ''
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch3:
|
||||||
|
name: 'Channel 3: <on-hold> action'
|
||||||
|
description: repeat action until button is released, see also on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch3:
|
||||||
|
name: 'Channel 3: Scroll wheel target entity'
|
||||||
|
description: 'Light or media player to be controlled by scroll wheel for
|
||||||
|
channel 3
|
||||||
|
|
||||||
|
- empty for <user defined> scroll wheel mode
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
default: []
|
||||||
|
scroll_wheel_mode_ch3:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
- 'dynamic: choose from input_select'
|
||||||
|
- user defined
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
default: 'lights: dim'
|
||||||
|
name: 'Channel 3: Scroll wheel mode'
|
||||||
|
description: Select attribute of target entity controlled by scroll wheel
|
||||||
|
scroll_wheel_mode_dynamic_ch3:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- input_select
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
default:
|
||||||
|
name: 'Channel 3: Scroll wheel mode input_select'
|
||||||
|
description: 'For scroll wheel mode <dynamic> only, leave empty otherwise.
|
||||||
|
|
||||||
|
Create and choose an ''input_select'' containing the scroll wheel modes
|
||||||
|
you want to choose from dynamically, e.g.
|
||||||
|
|
||||||
|
- "lights: dim"
|
||||||
|
|
||||||
|
- "lights: color temp only"'
|
||||||
|
scroll_wheel_mode_ext_ch3:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- relaxed
|
||||||
|
- instant
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
default: relaxed
|
||||||
|
name: 'Channel 3: Scroll wheel evaluation mode'
|
||||||
|
description: '- <relaxed>: one action call after scrolling
|
||||||
|
|
||||||
|
- <instant>: many action calls while scrolling
|
||||||
|
|
||||||
|
|
||||||
|
Beware:
|
||||||
|
|
||||||
|
- for <instant> mode the 9 hidden sensor entities of Bilresa device have
|
||||||
|
to be enabled
|
||||||
|
|
||||||
|
'
|
||||||
|
scroll_wheel_user_action_ch3:
|
||||||
|
name: 'Channel 3: User defined scroll wheel action'
|
||||||
|
description: 'For scroll wheel mode <user defined> only
|
||||||
|
|
||||||
|
Exposed variables:
|
||||||
|
|
||||||
|
- <scroll_clicks>: for <instant> mode always 1, for <relaxed> mode number
|
||||||
|
between 1 to 8
|
||||||
|
|
||||||
|
- <scroll_direction>: ''left'' or ''right''
|
||||||
|
|
||||||
|
'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
global_light_setting:
|
||||||
|
name: Global light settings
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
dim_step_pct:
|
||||||
|
name: Dimmer step percentage
|
||||||
|
description: change in brightness in %
|
||||||
|
default: 10
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
dim_min_pct:
|
||||||
|
name: Minimum brightness
|
||||||
|
description: minimum brightness in %
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
dim_turns_lights_on:
|
||||||
|
name: Dim turns lights on
|
||||||
|
description: 'allow scroll wheel (mode: dim) to turn lights on
|
||||||
|
|
||||||
|
'
|
||||||
|
default: false
|
||||||
|
selector:
|
||||||
|
boolean: {}
|
||||||
|
min_color_temp:
|
||||||
|
name: Minimum color temperature
|
||||||
|
description: minimum color temperature in K (for color_mode = color_temp)
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
max_color_temp:
|
||||||
|
name: Maximum color temperature
|
||||||
|
description: maximum color temperature in K (for color_mode = color_temp)
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_temp_step:
|
||||||
|
name: Color temperature step
|
||||||
|
description: change in color temperature in K (for color_mode = color_temp)
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_hue_step:
|
||||||
|
name: Color hue step
|
||||||
|
description: change in color hue in degree (0..360) (for color_mode = color_hue)
|
||||||
|
default: 6
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_saturation:
|
||||||
|
name: Color saturation
|
||||||
|
description: color saturation in % (for color_mode = color_hue)
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
light_transition_duration:
|
||||||
|
name: Transition duration
|
||||||
|
description: Controls the duration of transitions
|
||||||
|
default: 0.5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
global_media_setting:
|
||||||
|
name: Global media settings
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
volume_step_pct:
|
||||||
|
name: Media player volume step percentage
|
||||||
|
description: change in volume in %
|
||||||
|
default: 3.5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
volume_use_up_down_for_instant:
|
||||||
|
name: 'Media player: use actions volume_up/volume_down for <instant> mode'
|
||||||
|
description: "<instant> mode uses standard actions volume_up/volume_down
|
||||||
|
instead of <volume step percentage> by default \n\nif this is to coarse
|
||||||
|
disable this option and adjust the <volume step percentage> value to the
|
||||||
|
lowest value which still changes the volume while scrolling in both directions\n"
|
||||||
|
default: true
|
||||||
|
selector:
|
||||||
|
boolean: {}
|
||||||
|
volume_max_pct:
|
||||||
|
name: Media player max volume percentage
|
||||||
|
description: max volume in %
|
||||||
|
default: 70
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
global_misc_setting:
|
||||||
|
name: Miscellaneous settings
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
auto_reset_time_input_select_helpers:
|
||||||
|
name: Auto reset time for input_select helpers
|
||||||
|
description: 'auto reset time for input_select helpers if > 0
|
||||||
|
|
||||||
|
input_selects are reset to first selection
|
||||||
|
|
||||||
|
'
|
||||||
|
default: 0
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 60.0
|
||||||
|
on_hold_delay:
|
||||||
|
name: On hold delay
|
||||||
|
description: delay between separate action calls while on hold button
|
||||||
|
default: 0.1
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 0.1
|
||||||
|
max_queued_automation_calls:
|
||||||
|
name: Max queued automation calls
|
||||||
|
description: 'Increase if you want to queue more automation calls for special
|
||||||
|
cases
|
||||||
|
|
||||||
|
'
|
||||||
|
default: 10
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
source_url: https://gist.github.com/jhol-byte/b2731a4d2476f530d76b9ff409f7f3a4
|
||||||
|
alias: Ikea_bilresa_scroll_wheel
|
||||||
|
trigger:
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_button_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: button_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ext_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ext_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ext_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ext_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_button_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: button_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ext_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ext_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ext_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ext_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_button_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: button_ch3
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ch3
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ch3
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ext_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ext_ch3
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ext_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ext_ch3
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.event.data.old_state.state != 'unavailable' \n and
|
||||||
|
trigger.event.data.new_state.state != 'unavailable'\n}}"
|
||||||
|
action:
|
||||||
|
- variables:
|
||||||
|
channel: "{% if trigger.id in ['button_ch1', 'scroll_left_ch1', 'scroll_right_ch1',
|
||||||
|
'scroll_left_ext_ch1', 'scroll_right_ext_ch1'] %}\n ch1\n{% elif trigger.id
|
||||||
|
in ['button_ch2', 'scroll_left_ch2', 'scroll_right_ch2', 'scroll_left_ext_ch2',
|
||||||
|
'scroll_right_ext_ch2'] %}\n ch2\n{% elif trigger.id in ['button_ch3', 'scroll_left_ch3',
|
||||||
|
'scroll_right_ch3', 'scroll_left_ext_ch3', 'scroll_right_ext_ch3'] %}\n ch3\n{%
|
||||||
|
endif %}"
|
||||||
|
- if: '{{ var_reset_time_input_select_helpers > 0 and last_triggered_seconds > var_reset_time_input_select_helpers
|
||||||
|
}}'
|
||||||
|
then:
|
||||||
|
repeat:
|
||||||
|
for_each:
|
||||||
|
- ch1
|
||||||
|
- ch2
|
||||||
|
- ch3
|
||||||
|
sequence:
|
||||||
|
- action: input_select.select_first
|
||||||
|
target:
|
||||||
|
entity_id: '{{ vars[''scroll_wheel_mode_dynamic_'' ~ repeat.item ] }}'
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''multi_press_1'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then: !input click_action_ch1
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then: !input click_action_ch2
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then: !input click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''multi_press_2'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then: !input double_click_action_ch1
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then: !input double_click_action_ch2
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then: !input double_click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''multi_press_3'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then: !input triple_click_action_ch1
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then: !input triple_click_action_ch2
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then: !input triple_click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''long_press'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch1
|
||||||
|
- repeat:
|
||||||
|
while: '{{ repeat.index < 100 and state_attr(var_button_ch1, ''event_type'')
|
||||||
|
== ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch1
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch2
|
||||||
|
- repeat:
|
||||||
|
while: '{{ repeat.index < 100 and state_attr(var_button_ch2, ''event_type'')
|
||||||
|
== ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch2
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch3
|
||||||
|
- repeat:
|
||||||
|
while: '{{ repeat.index < 100 and state_attr(var_button_ch3, ''event_type'')
|
||||||
|
== ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch3
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ ( (vars['scroll_wheel_mode_ext_' ~ channel] == 'relaxed')
|
||||||
|
and (trigger.id == ('scroll_left_' ~ channel) or trigger.id == ('scroll_right_'
|
||||||
|
~ channel))) or\n ( (vars['scroll_wheel_mode_ext_' ~ channel] == 'instant')
|
||||||
|
and (trigger.id == ('scroll_left_ext_' ~ channel) or trigger.id == ('scroll_right_ext_'
|
||||||
|
~ channel)) and trigger.event.data.new_state.state == '1' ) \n}}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
scroll_clicks: "{% if (vars['scroll_wheel_mode_ext_' ~ channel] == 'relaxed')
|
||||||
|
%}\n {{ trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
|
||||||
|
}}\n{% else %}\n 1\n{% endif %}\n"
|
||||||
|
scroll_direction: "{% if (trigger.id == 'scroll_left_' ~ channel) or (trigger.id
|
||||||
|
== 'scroll_left_ext_' ~ channel)%}\n left\n{% else %}\n right\n{% endif
|
||||||
|
%}\n"
|
||||||
|
scroll_wheel_mode: '{{ vars[''scroll_wheel_mode_'' ~ channel] if vars[''scroll_wheel_mode_''
|
||||||
|
~ channel] != ''dynamic: choose from input_select'' else states(vars[''scroll_wheel_mode_dynamic_''
|
||||||
|
~ channel]) }}
|
||||||
|
|
||||||
|
'
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_wheel_mode == ''user defined'' }}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then: !input scroll_wheel_user_action_ch1
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then: !input scroll_wheel_user_action_ch2
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then: !input scroll_wheel_user_action_ch3
|
||||||
|
else:
|
||||||
|
- repeat:
|
||||||
|
for_each: '{{ vars[''targets_entities_'' ~ channel] }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_wheel_mode == ''lights: dim''}}'
|
||||||
|
sequence:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_state(repeat.item, ''on'') or (var_dim_turns_lights_on
|
||||||
|
and scroll_direction == ''right'') }}'
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_state(repeat.item, ''on'') }}'
|
||||||
|
then:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
brightness_step_pct: "{{ iif(scroll_direction == 'right', scroll_clicks
|
||||||
|
* var_dim_step_pct, -1*([scroll_clicks * var_dim_step_pct,
|
||||||
|
state_attr(repeat.item, 'brightness')*100/254\n - var_min_pct]|min))
|
||||||
|
\n}}"
|
||||||
|
transition: '{{ var_light_transition_duration }}'
|
||||||
|
else:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
transition: '{{ var_light_transition_duration }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_wheel_mode == ''lights: color temp only''
|
||||||
|
or (scroll_wheel_mode == ''lights: color temp and color hue'' and
|
||||||
|
state_attr(repeat.item, ''color_temp_kelvin'') is not none ) }}'
|
||||||
|
sequence:
|
||||||
|
- action: script.light_color_temp_helper
|
||||||
|
data:
|
||||||
|
mode: '{{ iif(scroll_direction == ''left'', ''down'', ''up'') }}'
|
||||||
|
min_color_temp: '{{ var_min_color_temp }}'
|
||||||
|
max_color_temp: '{{ var_max_color_temp }}'
|
||||||
|
color_temp_step: '{{ var_color_temp_step * scroll_clicks }}'
|
||||||
|
target_light: '{{ repeat.item }}'
|
||||||
|
light_transition_duration: '{{ var_light_transition_duration }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_wheel_mode == ''lights: color hue only''
|
||||||
|
or (scroll_wheel_mode == ''lights: color temp and color hue'' and
|
||||||
|
state_attr(repeat.item, ''color_temp_kelvin'') is none ) }}'
|
||||||
|
sequence:
|
||||||
|
- action: script.light_color_hs_helper
|
||||||
|
data:
|
||||||
|
target_light: '{{ repeat.item }}'
|
||||||
|
light_transition_duration: '{{ var_light_transition_duration }}'
|
||||||
|
color_hue_step: '{{ var_color_hue_step * scroll_clicks }}'
|
||||||
|
color_saturation: '{{ var_color_saturation }}'
|
||||||
|
mode: '{{ iif(scroll_direction == ''left'', ''down'', ''up'') }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_wheel_mode == ''media: volume control''
|
||||||
|
and (is_state(repeat.item, ''playing'') or is_state(repeat.item,
|
||||||
|
''on'') )}}'
|
||||||
|
sequence:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ vars[''scroll_wheel_mode_ext_'' ~ channel] ==
|
||||||
|
''relaxed'' or not var_volume_use_up_down_for_instant}}'
|
||||||
|
then:
|
||||||
|
- action: media_player.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
volume_level: "{{ [[state_attr(repeat.item, 'volume_level')*100
|
||||||
|
+ \n iif(scroll_direction == 'right', 1, -1)* scroll_clicks
|
||||||
|
* var_volume_step_pct, var_volume_max_pct] | min, 0] | max /
|
||||||
|
100\n}}"
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_direction == ''left'' }}'
|
||||||
|
then:
|
||||||
|
- action: media_player.volume_down
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ state_attr(repeat.item, ''volume_level'')*100
|
||||||
|
< var_volume_max_pct }}'
|
||||||
|
then:
|
||||||
|
- action: media_player.volume_up
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
mode: queued
|
||||||
|
max: !input max_queued_automation_calls
|
||||||
|
max_exceeded: silent
|
||||||
|
variables:
|
||||||
|
vars:
|
||||||
|
targets_entities_ch1: !input scroll_wheel_target_ch1
|
||||||
|
targets_entities_ch2: !input scroll_wheel_target_ch2
|
||||||
|
targets_entities_ch3: !input scroll_wheel_target_ch3
|
||||||
|
scroll_wheel_mode_ch1: !input scroll_wheel_mode_ch1
|
||||||
|
scroll_wheel_mode_ch2: !input scroll_wheel_mode_ch2
|
||||||
|
scroll_wheel_mode_ch3: !input scroll_wheel_mode_ch3
|
||||||
|
scroll_wheel_mode_ext_ch1: !input scroll_wheel_mode_ext_ch1
|
||||||
|
scroll_wheel_mode_ext_ch2: !input scroll_wheel_mode_ext_ch2
|
||||||
|
scroll_wheel_mode_ext_ch3: !input scroll_wheel_mode_ext_ch3
|
||||||
|
scroll_wheel_mode_dynamic_ch1: !input scroll_wheel_mode_dynamic_ch1
|
||||||
|
scroll_wheel_mode_dynamic_ch2: !input scroll_wheel_mode_dynamic_ch2
|
||||||
|
scroll_wheel_mode_dynamic_ch3: !input scroll_wheel_mode_dynamic_ch3
|
||||||
|
var_min_pct: !input dim_min_pct
|
||||||
|
var_dim_step_pct: !input dim_step_pct
|
||||||
|
var_dim_turns_lights_on: !input dim_turns_lights_on
|
||||||
|
var_color_hue_step: !input color_hue_step
|
||||||
|
var_color_saturation: !input color_saturation
|
||||||
|
var_min_color_temp: !input min_color_temp
|
||||||
|
var_max_color_temp: !input max_color_temp
|
||||||
|
var_color_temp_step: !input color_temp_step
|
||||||
|
var_light_transition_duration: !input light_transition_duration
|
||||||
|
var_on_hold_delay: !input on_hold_delay
|
||||||
|
var_volume_step_pct: !input volume_step_pct
|
||||||
|
var_volume_max_pct: !input volume_max_pct
|
||||||
|
var_volume_use_up_down_for_instant: !input volume_use_up_down_for_instant
|
||||||
|
var_reset_time_input_select_helpers: !input auto_reset_time_input_select_helpers
|
||||||
|
last_triggered_seconds: "{{ as_timestamp(now()) - as_timestamp(this.attributes.last_triggered,
|
||||||
|
0) }} \n"
|
||||||
|
trigger_variables:
|
||||||
|
var_remote: !input remote
|
||||||
|
var_button_ch1: "{% for item in device_entities(var_remote) %} {% set match = item
|
||||||
|
| regex_findall(find='^event\\..+[a-zA-Z]_3.*$') %} {% if (match | length) > 0
|
||||||
|
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ch1: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_2.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ch1: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_1.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ext_ch1: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^sensor\\..+[a-zA-Z]_2.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ext_ch1: "{% for item in device_entities(var_remote) %} {% set
|
||||||
|
match = item | regex_findall(find='^sensor\\..+[a-zA-Z]_1.*$') %} {% if (match
|
||||||
|
| length) > 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_button_ch2: "{% for item in device_entities(var_remote) %} {% set match = item
|
||||||
|
| regex_findall(find='^event\\..+[a-zA-Z]_6.*$') %} {% if (match | length) > 0
|
||||||
|
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ch2: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_5.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ch2: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_4.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ext_ch2: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^sensor\\..+[a-zA-Z]_5.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ext_ch2: "{% for item in device_entities(var_remote) %} {% set
|
||||||
|
match = item | regex_findall(find='^sensor\\..+[a-zA-Z]_4.*$') %} {% if (match
|
||||||
|
| length) > 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_button_ch3: "{% for item in device_entities(var_remote) %} {% set match = item
|
||||||
|
| regex_findall(find='^event\\..+[a-zA-Z]_9.*$') %} {% if (match | length) > 0
|
||||||
|
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ch3: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_8.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ch3: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_7.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ext_ch3: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^sensor\\..+[a-zA-Z]_8.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ext_ch3: "{% for item in device_entities(var_remote) %} {% set
|
||||||
|
match = item | regex_findall(find='^sensor\\..+[a-zA-Z]_7.*$') %} {% if (match
|
||||||
|
| length) > 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA E2490 BILRESA Scroll Wheel (Zigbee2MQTT + ZHA)
|
||||||
|
description: 'Unified controller blueprint for IKEA E2490 BILRESA scroll wheel working
|
||||||
|
with Zigbee2MQTT and ZHA.
|
||||||
|
|
||||||
|
- Buttons: on, off, on_double, off_double
|
||||||
|
|
||||||
|
- Scroll: brightness_move_to_level with action_level from Zigbee2MQTT main topic,
|
||||||
|
or ZHA move_to_level args
|
||||||
|
|
||||||
|
Supports light brightness, media_player volume, light color_temp, light hue.
|
||||||
|
|
||||||
|
Version: 2026-01-18
|
||||||
|
|
||||||
|
'
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
controller_device:
|
||||||
|
name: Controller Device (Zigbee2MQTT or ZHA)
|
||||||
|
description: Select the E2490 device integrated via Zigbee2MQTT or ZHA.
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
filter:
|
||||||
|
- integration: mqtt
|
||||||
|
- integration: zha
|
||||||
|
multiple: false
|
||||||
|
scroll_target:
|
||||||
|
name: Scroll wheel target entity (light)
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
scroll_mode:
|
||||||
|
name: Default scroll mode
|
||||||
|
description: Default mode when automation starts or after auto-reset timeout
|
||||||
|
default: brightness
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
mode: dropdown
|
||||||
|
options:
|
||||||
|
- label: Brightness (light)
|
||||||
|
value: brightness
|
||||||
|
- label: Volume (media_player)
|
||||||
|
value: volume
|
||||||
|
- label: Color temperature (light)
|
||||||
|
value: color_temp
|
||||||
|
- label: Hue (light)
|
||||||
|
value: hue
|
||||||
|
sort: false
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
volume_target:
|
||||||
|
name: Volume target (media_player)
|
||||||
|
default:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- media_player
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
color_temp_target:
|
||||||
|
name: Color temperature target (light)
|
||||||
|
default:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
hue_target:
|
||||||
|
name: Hue target (light)
|
||||||
|
default:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
scroll_mode_helper:
|
||||||
|
name: (Optional) Scroll mode helper (input_select)
|
||||||
|
description: 'Provide an input_select to allow cycling scroll modes at runtime
|
||||||
|
with double ON. Must have options: brightness, volume, color_temp, hue (in
|
||||||
|
that order)'
|
||||||
|
default:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- input_select
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
last_activity_helper:
|
||||||
|
name: (Optional) Last activity helper (input_datetime)
|
||||||
|
description: Provide an input_datetime to enable auto-reset after inactivity.
|
||||||
|
Works with scroll mode helper and reset timeout.
|
||||||
|
default:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- input_datetime
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
reset_timeout:
|
||||||
|
name: (Optional) Auto-reset timeout (seconds)
|
||||||
|
description: Time in seconds of inactivity before resetting to default scroll
|
||||||
|
mode. Set to 0 to disable auto-reset. Requires both scroll mode helper and
|
||||||
|
last activity helper.
|
||||||
|
default: 60
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 3600.0
|
||||||
|
step: 1.0
|
||||||
|
unit_of_measurement: seconds
|
||||||
|
mode: box
|
||||||
|
volume_max:
|
||||||
|
name: (Optional) Max volume
|
||||||
|
description: Upper limit for volume mode (0.0 - 1.0). Values from the wheel
|
||||||
|
will be clamped to this maximum.
|
||||||
|
default: 1.0
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 1.0
|
||||||
|
step: 0.01
|
||||||
|
mode: slider
|
||||||
|
button_on_short:
|
||||||
|
name: Button ON - short press (optional)
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button_off_short:
|
||||||
|
name: Button OFF - short press (optional)
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button_on_double:
|
||||||
|
name: Button ON - double press (optional)
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button_off_double:
|
||||||
|
name: Button OFF - double press (optional)
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
source_url: https://gist.github.com/mcinnes01/399132520034291b89a82659653b2893
|
||||||
|
alias: IKEA E2490 BILRESA Scroll Wheel (Zigbee2MQTT + ZHA)
|
||||||
|
variables:
|
||||||
|
controller_device: !input controller_device
|
||||||
|
friendly_name: '{{ device_attr(controller_device, ''name'') | default('''') }}'
|
||||||
|
z2m_topic: zigbee2mqtt/{{ friendly_name }}
|
||||||
|
scroll_mode: !input scroll_mode
|
||||||
|
scroll_mode_helper: !input scroll_mode_helper
|
||||||
|
last_activity_helper: !input last_activity_helper
|
||||||
|
reset_timeout: !input reset_timeout
|
||||||
|
effective_mode: '{{ states(scroll_mode_helper) if scroll_mode_helper is not none
|
||||||
|
and states(scroll_mode_helper) != '''' else scroll_mode }}'
|
||||||
|
brightness_target: !input scroll_target
|
||||||
|
volume_target: !input volume_target
|
||||||
|
color_temp_target: !input color_temp_target
|
||||||
|
hue_target: !input hue_target
|
||||||
|
volume_max: !input volume_max
|
||||||
|
available_modes: '{% set modes = [] %} {% if brightness_target is not none %}{%
|
||||||
|
set modes = modes + [''brightness''] %}{% endif %} {% if volume_target is not
|
||||||
|
none %}{% set modes = modes + [''volume''] %}{% endif %} {% if color_temp_target
|
||||||
|
is not none %}{% set modes = modes + [''color_temp''] %}{% endif %} {% if hue_target
|
||||||
|
is not none %}{% set modes = modes + [''hue''] %}{% endif %} {{ modes }}
|
||||||
|
|
||||||
|
'
|
||||||
|
button_on_short_seq: !input button_on_short
|
||||||
|
button_off_short_seq: !input button_off_short
|
||||||
|
button_on_double_seq: !input button_on_double
|
||||||
|
button_off_double_seq: !input button_off_double
|
||||||
|
trigger:
|
||||||
|
- platform: device
|
||||||
|
id: z2m-on
|
||||||
|
domain: mqtt
|
||||||
|
device_id: !input controller_device
|
||||||
|
type: action
|
||||||
|
subtype: 'on'
|
||||||
|
- platform: device
|
||||||
|
id: z2m-off
|
||||||
|
domain: mqtt
|
||||||
|
device_id: !input controller_device
|
||||||
|
type: action
|
||||||
|
subtype: 'off'
|
||||||
|
- platform: device
|
||||||
|
id: z2m-on-double
|
||||||
|
domain: mqtt
|
||||||
|
device_id: !input controller_device
|
||||||
|
type: action
|
||||||
|
subtype: on_double
|
||||||
|
- platform: device
|
||||||
|
id: z2m-off-double
|
||||||
|
domain: mqtt
|
||||||
|
device_id: !input controller_device
|
||||||
|
type: action
|
||||||
|
subtype: off_double
|
||||||
|
- platform: mqtt
|
||||||
|
id: z2m-payload
|
||||||
|
topic: zigbee2mqtt/#
|
||||||
|
- platform: event
|
||||||
|
id: zha-on
|
||||||
|
event_type: zha_event
|
||||||
|
event_data:
|
||||||
|
device_id: !input controller_device
|
||||||
|
command: 'on'
|
||||||
|
- platform: event
|
||||||
|
id: zha-off
|
||||||
|
event_type: zha_event
|
||||||
|
event_data:
|
||||||
|
device_id: !input controller_device
|
||||||
|
command: 'off'
|
||||||
|
- platform: event
|
||||||
|
id: zha-level
|
||||||
|
event_type: zha_event
|
||||||
|
event_data:
|
||||||
|
device_id: !input controller_device
|
||||||
|
command: move_to_level
|
||||||
|
condition: []
|
||||||
|
action:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ scroll_mode_helper is not none \n and last_activity_helper
|
||||||
|
is not none \n and reset_timeout > 0\n and states(last_activity_helper)
|
||||||
|
not in ['unknown', 'unavailable', ''] \n and (now() - (states(last_activity_helper)
|
||||||
|
| as_datetime)).total_seconds() > reset_timeout }}\n"
|
||||||
|
then:
|
||||||
|
- service: input_select.select_option
|
||||||
|
target:
|
||||||
|
entity_id: '{{ scroll_mode_helper }}'
|
||||||
|
data:
|
||||||
|
option: '{{ scroll_mode }}'
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.id == 'z2m-payload' \n and trigger.topic == z2m_topic
|
||||||
|
\n and trigger.payload_json is defined \n and trigger.payload_json.action
|
||||||
|
== 'brightness_move_to_level' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
lvl: '{{ trigger.payload_json.action_level | default(none) }}'
|
||||||
|
trans: '{{ trigger.payload_json.action_transition_time | default(0) }}'
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''brightness'' and brightness_target
|
||||||
|
is not none }}'
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ brightness_target }}'
|
||||||
|
data:
|
||||||
|
brightness: '{% if lvl is none %}255{% else %}{{ ((lvl|float/241.0)*255.0)|round(0)|int
|
||||||
|
}}{% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
transition: '{{ trans }}'
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ last_activity_helper is not none }}'
|
||||||
|
then:
|
||||||
|
- service: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: '{{ last_activity_helper }}'
|
||||||
|
data:
|
||||||
|
timestamp: '{{ now().timestamp() }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''volume'' and volume_target is not
|
||||||
|
none }}'
|
||||||
|
sequence:
|
||||||
|
- service: media_player.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: '{{ volume_target }}'
|
||||||
|
data:
|
||||||
|
volume_level: "{% if lvl is none %}\n {{ volume_max }}\n{% else %}\n
|
||||||
|
\ {{ ((lvl|float/241.0) * volume_max)|round(3) }}\n{% endif %}\n"
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ last_activity_helper is not none }}'
|
||||||
|
then:
|
||||||
|
- service: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: '{{ last_activity_helper }}'
|
||||||
|
data:
|
||||||
|
timestamp: '{{ now().timestamp() }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''color_temp'' and color_temp_target
|
||||||
|
is not none }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
min_mireds: '{{ state_attr(color_temp_target, ''min_mireds'') | default(153)
|
||||||
|
}}'
|
||||||
|
max_mireds: '{{ state_attr(color_temp_target, ''max_mireds'') | default(500)
|
||||||
|
}}'
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ color_temp_target }}'
|
||||||
|
data:
|
||||||
|
color_temp: "{% set minm = min_mireds|int %} {% set maxm = max_mireds|int
|
||||||
|
%} {% if lvl is none %}\n {{ maxm }}\n{% else %}\n {% set ratio =
|
||||||
|
((lvl|float - 1.0)/240.0) %}\n {{ (minm + ratio*(maxm-minm))|round(0)|int
|
||||||
|
}}\n{% endif %}\n"
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ last_activity_helper is not none }}'
|
||||||
|
then:
|
||||||
|
- service: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: '{{ last_activity_helper }}'
|
||||||
|
data:
|
||||||
|
timestamp: '{{ now().timestamp() }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''hue'' and hue_target is not none
|
||||||
|
}}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
current_hs: '{{ state_attr(hue_target, ''hs_color'') | default([0, 100])
|
||||||
|
}}'
|
||||||
|
current_sat: '{{ current_hs[1] if current_hs is sequence and current_hs|length
|
||||||
|
> 1 else 100 }}'
|
||||||
|
hue_val: '{% if lvl is none %}360{% else %}{{ ((lvl|float/241.0)*360.0)|round(0)|int
|
||||||
|
}}{% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ hue_target }}'
|
||||||
|
data:
|
||||||
|
hs_color: '[ {{ hue_val }}, {{ current_sat }} ]'
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ last_activity_helper is not none }}'
|
||||||
|
then:
|
||||||
|
- service: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: '{{ last_activity_helper }}'
|
||||||
|
data:
|
||||||
|
timestamp: '{{ now().timestamp() }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == ''zha-level'' }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
lvl: '{% set a = trigger.event.data.args | default([]) %} {% if a is sequence
|
||||||
|
and a|length > 0 %}{{ a[0] }}{% else %}{{ none }}{% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
trans: '{% set a = trigger.event.data.args | default([]) %} {% if a is sequence
|
||||||
|
and a|length > 1 %}{{ a[1] }}{% else %}0{% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''brightness'' and brightness_target
|
||||||
|
is not none }}'
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ brightness_target }}'
|
||||||
|
data:
|
||||||
|
brightness: '{% if lvl is none %}255{% else %}{{ (lvl|int)|min(255)|max(1)
|
||||||
|
}}{% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
transition: '{{ trans }}'
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ last_activity_helper is not none }}'
|
||||||
|
then:
|
||||||
|
- service: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: '{{ last_activity_helper }}'
|
||||||
|
data:
|
||||||
|
timestamp: '{{ now().timestamp() }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id in [''z2m-on'',''zha-on''] }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ button_on_short_seq | length > 0 }}'
|
||||||
|
sequence: !input button_on_short
|
||||||
|
default:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''volume'' and volume_target is
|
||||||
|
not none }}'
|
||||||
|
sequence:
|
||||||
|
- service: media_player.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ volume_target }}'
|
||||||
|
- conditions: []
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: !input scroll_target
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id in [''z2m-off'',''zha-off''] }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ button_off_short_seq | length > 0 }}'
|
||||||
|
sequence: !input button_off_short
|
||||||
|
default:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ effective_mode == ''volume'' and volume_target is
|
||||||
|
not none }}'
|
||||||
|
sequence:
|
||||||
|
- service: media_player.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: '{{ volume_target }}'
|
||||||
|
- conditions: []
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: !input scroll_target
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == ''z2m-on-double'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ scroll_mode_helper is not none }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
current_mode: '{{ states(scroll_mode_helper) }}'
|
||||||
|
modes_list: '{{ available_modes }}'
|
||||||
|
current_index: '{{ modes_list.index(current_mode) if current_mode in modes_list
|
||||||
|
else 0 }}'
|
||||||
|
next_index: '{{ (current_index + 1) % (modes_list | length) }}'
|
||||||
|
next_mode: '{{ modes_list[next_index] if modes_list | length > 0 else
|
||||||
|
''brightness'' }}'
|
||||||
|
- service: input_select.select_option
|
||||||
|
target:
|
||||||
|
entity_id: '{{ scroll_mode_helper }}'
|
||||||
|
data:
|
||||||
|
option: '{{ next_mode }}'
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ last_activity_helper is not none }}'
|
||||||
|
then:
|
||||||
|
- service: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: '{{ last_activity_helper }}'
|
||||||
|
data:
|
||||||
|
timestamp: '{{ now().timestamp() }}'
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ button_on_double_seq | length > 0 }}'
|
||||||
|
sequence: !input button_on_double
|
||||||
|
- conditions: []
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ button_on_double_seq | length > 0 }}'
|
||||||
|
sequence: !input button_on_double
|
||||||
|
- conditions: []
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: !input scroll_target
|
||||||
|
data:
|
||||||
|
brightness: 255
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == ''z2m-off-double'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ button_off_double_seq | length > 0 }}'
|
||||||
|
sequence: !input button_off_double
|
||||||
|
default:
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: !input scroll_target
|
||||||
|
mode: restart
|
||||||
|
max_exceeded: silent
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA Bilresa Scrollwheel - Light Control V1.0.5
|
||||||
|
description: 'The ultimate LIGHT controller for the IKEA Bilresa Smart Scroll Wheel
|
||||||
|
via Matter. (NOT for Media/Volume! NOT for the 2-Button Remote!)
|
||||||
|
|
||||||
|
VERSION 1.0.5: Included fix to check both the from_state and the to_state. VERSION
|
||||||
|
1.0.4: Code cleanup. Removed ''short_release'' placebo for multi_press events.
|
||||||
|
VERSION 1.0.3: Bugfix: Fixed "ghost switching" after Home Assistant restarts.
|
||||||
|
VERSION 1.0.2: Safety update. Added template fallbacks for RGB/Kelvin shifts.
|
||||||
|
VERSION 1.0.1: Fixed "Bouncing/Jumping" issues (added debounce logic).
|
||||||
|
|
||||||
|
YOU DECIDE EVERYTHING: - 3 Scroll Zones: Choose the function (Brightness, Kelvin,
|
||||||
|
RGB, Presets). - 3 Buttons x 2 Gestures: Choose a specific action for EACH gesture
|
||||||
|
(Double/Hold).
|
||||||
|
|
||||||
|
REQUIREMENTS: - ENABLE Sensors: 1,2, 4,5, 7,8 - ENABLE Events: 3,6,9 - DISABLE
|
||||||
|
Sensors: 3,6,9 (Not needed, keep disabled!)
|
||||||
|
|
||||||
|
HELPER GROUP: If controlling multiple lights, please always use a HELPER GROUP!
|
||||||
|
Directly controlling multiple entities can cause sync issues.
|
||||||
|
|
||||||
|
'
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
light_entity:
|
||||||
|
name: Light (or Group)
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
scroll_logic_1_2:
|
||||||
|
name: Scroll Zone 1 (Sensors 1 & 2)
|
||||||
|
default: brightness
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: Brightness (Bright / Dark)
|
||||||
|
value: brightness
|
||||||
|
- label: Kelvin (Cool / Warm)
|
||||||
|
value: kelvin
|
||||||
|
- label: RGB Color (Rainbow)
|
||||||
|
value: rgb
|
||||||
|
- label: Presets (Work / Cozy)
|
||||||
|
value: preset
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
scroll_logic_4_5:
|
||||||
|
name: Scroll Zone 2 (Sensors 4 & 5)
|
||||||
|
default: kelvin
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: Brightness (Bright / Dark)
|
||||||
|
value: brightness
|
||||||
|
- label: Kelvin (Cool / Warm)
|
||||||
|
value: kelvin
|
||||||
|
- label: RGB Color (Rainbow)
|
||||||
|
value: rgb
|
||||||
|
- label: Presets (Work / Cozy)
|
||||||
|
value: preset
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
scroll_logic_7_8:
|
||||||
|
name: Scroll Zone 3 (Sensors 7 & 8)
|
||||||
|
default: preset
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: Brightness (Bright / Dark)
|
||||||
|
value: brightness
|
||||||
|
- label: Kelvin (Cool / Warm)
|
||||||
|
value: kelvin
|
||||||
|
- label: RGB Color (Rainbow)
|
||||||
|
value: rgb
|
||||||
|
- label: Presets (Work / Cozy)
|
||||||
|
value: preset
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
btn1_double:
|
||||||
|
name: 'Button 1: Double Click Action'
|
||||||
|
default: set_bright
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: -> Bright (80%)
|
||||||
|
value: set_bright
|
||||||
|
- label: -> Cool White (4000K)
|
||||||
|
value: set_cold
|
||||||
|
- label: -> White (RGB Reset)
|
||||||
|
value: set_white
|
||||||
|
- label: -> Red (Signal Color)
|
||||||
|
value: set_red
|
||||||
|
- label: -> No Action
|
||||||
|
value: none
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
btn1_hold:
|
||||||
|
name: 'Button 1: Hold Action'
|
||||||
|
default: set_dim
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: -> Dimmed (20%)
|
||||||
|
value: set_dim
|
||||||
|
- label: -> Warm White (2200K)
|
||||||
|
value: set_warm
|
||||||
|
- label: -> Enable Color (Saturation On)
|
||||||
|
value: set_color_on
|
||||||
|
- label: -> No Action
|
||||||
|
value: none
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
btn2_double:
|
||||||
|
name: 'Button 2: Double Click Action'
|
||||||
|
default: set_cold
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: -> Bright (80%)
|
||||||
|
value: set_bright
|
||||||
|
- label: -> Cool White (4000K)
|
||||||
|
value: set_cold
|
||||||
|
- label: -> White (RGB Reset)
|
||||||
|
value: set_white
|
||||||
|
- label: -> Red (Signal Color)
|
||||||
|
value: set_red
|
||||||
|
- label: -> No Action
|
||||||
|
value: none
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
btn2_hold:
|
||||||
|
name: 'Button 2: Hold Action'
|
||||||
|
default: set_warm
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: -> Dimmed (20%)
|
||||||
|
value: set_dim
|
||||||
|
- label: -> Warm White (2200K)
|
||||||
|
value: set_warm
|
||||||
|
- label: -> Enable Color (Saturation On)
|
||||||
|
value: set_color_on
|
||||||
|
- label: -> No Action
|
||||||
|
value: none
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
btn3_double:
|
||||||
|
name: 'Button 3: Double Click Action'
|
||||||
|
default: set_bright
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: -> Bright (80%)
|
||||||
|
value: set_bright
|
||||||
|
- label: -> Cool White (4000K)
|
||||||
|
value: set_cold
|
||||||
|
- label: -> White (RGB Reset)
|
||||||
|
value: set_white
|
||||||
|
- label: -> Red (Signal Color)
|
||||||
|
value: set_red
|
||||||
|
- label: -> No Action
|
||||||
|
value: none
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
btn3_hold:
|
||||||
|
name: 'Button 3: Hold Action'
|
||||||
|
default: set_dim
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: -> Dimmed (20%)
|
||||||
|
value: set_dim
|
||||||
|
- label: -> Warm White (2200K)
|
||||||
|
value: set_warm
|
||||||
|
- label: -> Enable Color (Saturation On)
|
||||||
|
value: set_color_on
|
||||||
|
- label: -> No Action
|
||||||
|
value: none
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
sort: false
|
||||||
|
step_pct:
|
||||||
|
name: 'Step Size: Brightness (%)'
|
||||||
|
default: 10
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 50.0
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
step_kelvin:
|
||||||
|
name: 'Step Size: Kelvin (K)'
|
||||||
|
default: 400
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 100.0
|
||||||
|
max: 1000.0
|
||||||
|
unit_of_measurement: K
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
step_hue:
|
||||||
|
name: 'Step Size: RGB Hue (°)'
|
||||||
|
default: 15
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 5.0
|
||||||
|
max: 90.0
|
||||||
|
unit_of_measurement: °
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
val_bright:
|
||||||
|
name: 'Value: Bright (%)'
|
||||||
|
default: 80
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
val_dim:
|
||||||
|
name: 'Value: Dim (%)'
|
||||||
|
default: 20
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
val_cold:
|
||||||
|
name: 'Value: Cool (K)'
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 2000.0
|
||||||
|
max: 6500.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
val_warm:
|
||||||
|
name: 'Value: Warm (K)'
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 2000.0
|
||||||
|
max: 6500.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
sensor_1:
|
||||||
|
name: Sensor 1 (Look for '(1)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- sensor
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_2:
|
||||||
|
name: Sensor 2 (Look for '(2)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- sensor
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_4:
|
||||||
|
name: Sensor 4 (Look for '(4)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- sensor
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_5:
|
||||||
|
name: Sensor 5 (Look for '(5)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- sensor
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_7:
|
||||||
|
name: Sensor 7 (Look for '(7)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- sensor
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_8:
|
||||||
|
name: Sensor 8 (Look for '(8)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- sensor
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
event_taste_1:
|
||||||
|
name: Event Button 1 (Look for '(3)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- event
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
event_taste_2:
|
||||||
|
name: Event Button 2 (Look for '(6)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- event
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
event_taste_3:
|
||||||
|
name: Event Button 3 (Look for '(9)')
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- event
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
source_url: https://gist.github.com/stone13/63a778bdbf55efbd712ade1d074e9dbb
|
||||||
|
mode: single
|
||||||
|
max_exceeded: silent
|
||||||
|
variables:
|
||||||
|
target_light: !input light_entity
|
||||||
|
m_sc_1: !input scroll_logic_1_2
|
||||||
|
m_sc_2: !input scroll_logic_4_5
|
||||||
|
m_sc_3: !input scroll_logic_7_8
|
||||||
|
b1_d: !input btn1_double
|
||||||
|
b1_h: !input btn1_hold
|
||||||
|
b2_d: !input btn2_double
|
||||||
|
b2_h: !input btn2_hold
|
||||||
|
b3_d: !input btn3_double
|
||||||
|
b3_h: !input btn3_hold
|
||||||
|
s_p: !input step_pct
|
||||||
|
s_k: !input step_kelvin
|
||||||
|
s_h: !input step_hue
|
||||||
|
v_bri: !input val_bright
|
||||||
|
v_dim: !input val_dim
|
||||||
|
v_cold: !input val_cold
|
||||||
|
v_warm: !input val_warm
|
||||||
|
id_e1: !input event_taste_1
|
||||||
|
id_e2: !input event_taste_2
|
||||||
|
id_e3: !input event_taste_3
|
||||||
|
triggers:
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_1
|
||||||
|
id: sc_1_up
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_2
|
||||||
|
id: sc_1_down
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_4
|
||||||
|
id: sc_2_down
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_5
|
||||||
|
id: sc_2_up
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_7
|
||||||
|
id: sc_3_up
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_8
|
||||||
|
id: sc_3_down
|
||||||
|
- trigger: state
|
||||||
|
entity_id:
|
||||||
|
- !input event_taste_1
|
||||||
|
- !input event_taste_2
|
||||||
|
- !input event_taste_3
|
||||||
|
id: btn_event
|
||||||
|
conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.from_state is defined and trigger.to_state is defined
|
||||||
|
and\n trigger.from_state.state not in ['unknown', 'unavailable'] and\n trigger.to_state.state
|
||||||
|
not in ['unknown', 'unavailable'] }}"
|
||||||
|
actions:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ ''sc_'' in trigger.id }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
direction: '{% if ''_up'' in trigger.id %} 1 {% else %} -1 {% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
active_func: '{% if ''sc_1'' in trigger.id %} {{ m_sc_1 }} {% elif ''sc_2''
|
||||||
|
in trigger.id %} {{ m_sc_2 }} {% elif ''sc_3'' in trigger.id %} {{ m_sc_3
|
||||||
|
}} {% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ active_func == ''brightness'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_step_pct: '{{ (s_p | float(default=10)) * direction }}'
|
||||||
|
transition: 0
|
||||||
|
- conditions: '{{ active_func == ''kelvin'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
color_temp_kelvin: '{% set current = state_attr(target_light, ''color_temp_kelvin'')
|
||||||
|
| int(default=3000) %} {% set step = s_k | int(default=400) %} {% if
|
||||||
|
direction == 1 %} {{ ([current + step, 6500] | min) | int }} {% else
|
||||||
|
%} {{ ([current - step, 2000] | max) | int }} {% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
transition: 0
|
||||||
|
- conditions: '{{ active_func == ''rgb'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
hs_color: '{% set current_hs = state_attr(target_light, ''hs_color'')
|
||||||
|
%} {% set h = current_hs[0] | float(default=0) if current_hs is not
|
||||||
|
none else 0 %} {% set s = current_hs[1] | float(default=100) if current_hs
|
||||||
|
is not none else 100 %} {% set step = s_h | float(default=15) %} {{
|
||||||
|
[(h + (step * direction)) % 360, s] }}
|
||||||
|
|
||||||
|
'
|
||||||
|
transition: 0
|
||||||
|
- conditions: '{{ active_func == ''preset'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ v_bri if direction == 1 else v_dim }}'
|
||||||
|
color_temp_kelvin: '{{ v_cold if direction == 1 else v_warm }}'
|
||||||
|
transition: 1
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ trigger.id == ''btn_event'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'multi_press_1' }}\n"
|
||||||
|
sequence:
|
||||||
|
- action: light.toggle
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
transition: 0.5
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'multi_press_2' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
action_code: '{% if trigger.entity_id == id_e1 %} {{ b1_d }} {% elif trigger.entity_id
|
||||||
|
== id_e2 %} {{ b2_d }} {% elif trigger.entity_id == id_e3 %} {{ b3_d
|
||||||
|
}} {% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_code == ''set_bright'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ v_bri }}'
|
||||||
|
transition: 0.5
|
||||||
|
- conditions: '{{ action_code == ''set_cold'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
color_temp_kelvin: '{{ v_cold }}'
|
||||||
|
transition: 0.5
|
||||||
|
- conditions: '{{ action_code == ''set_white'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
color_temp_kelvin: 4000
|
||||||
|
transition: 0.5
|
||||||
|
- conditions: '{{ action_code == ''set_red'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
hs_color:
|
||||||
|
- 0
|
||||||
|
- 100
|
||||||
|
brightness_pct: 80
|
||||||
|
transition: 0.5
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'long_press' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
action_code: '{% if trigger.entity_id == id_e1 %} {{ b1_h }} {% elif trigger.entity_id
|
||||||
|
== id_e2 %} {{ b2_h }} {% elif trigger.entity_id == id_e3 %} {{ b3_h
|
||||||
|
}} {% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ action_code == ''set_dim'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ v_dim }}'
|
||||||
|
transition: 0.5
|
||||||
|
- conditions: '{{ action_code == ''set_warm'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
color_temp_kelvin: '{{ v_warm }}'
|
||||||
|
transition: 0.5
|
||||||
|
- conditions: '{{ action_code == ''set_color_on'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
hs_color:
|
||||||
|
- 0
|
||||||
|
- 100
|
||||||
|
brightness_pct: 80
|
||||||
|
transition: 0.5
|
||||||
|
- delay:
|
||||||
|
milliseconds: 150
|
||||||
@@ -0,0 +1,920 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA Bilresa Scrollwheel - Light Control
|
||||||
|
author: thetestspecimen
|
||||||
|
source_url: https://github.com/thetestspecimen/homeassistant/blob/main/blueprints/ikea-bilresa-scroll-wheel.yaml
|
||||||
|
description: "\n# \U0001F4A1 Light controller \U0001F4A1\n## IKEA BILRESA - Smart
|
||||||
|
Scroll Wheel utilising Matter over Thread\n\n\n## VARIABLES:\n- 3 Scroll Zones:
|
||||||
|
Choose the function (Brightness, Kelvin, RGB, Presets).\n- 3 Buttons x 3 Gestures:
|
||||||
|
Choose a specific action for EACH gesture (double click / triple click / hold).\n\n***Note:***
|
||||||
|
single click will always toggle the light on or off.\n\n## REQUIREMENTS:\n\nFor
|
||||||
|
optimal behavior, the following settings must be applied:\n- ENABLE Events: 3,6,9
|
||||||
|
(in section \"Events\")\n- ENABLE Sensors: 1,2,4,5,7,8 (in section \"Diagnostic\")\n-
|
||||||
|
DISABLE Events: 1,2,4,5,7,8 (in section \"Events\")\n- DISABLE Sensors: 3,6,9
|
||||||
|
(in section \"Diagnostic\")\n\n***Note:*** this blueprint cannot be used with
|
||||||
|
the BILRESA 2-Button Remote.\n"
|
||||||
|
homeassistant:
|
||||||
|
min_version: 2024.6.0
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
header_entity_mapping:
|
||||||
|
name: Entity Mapping [REQUIRED]
|
||||||
|
description: Select the entities for the scroll wheel and buttons
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_buttons:
|
||||||
|
name: Buttons
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
event_button_1:
|
||||||
|
name: 'Channel 1: Button'
|
||||||
|
description: 'Event: Look for ''Button (3)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- event
|
||||||
|
integration: matter
|
||||||
|
device_class:
|
||||||
|
- button
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
event_button_2:
|
||||||
|
name: 'Channel 2: Button'
|
||||||
|
description: 'Event: Look for ''Button (6)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- event
|
||||||
|
integration: matter
|
||||||
|
device_class:
|
||||||
|
- button
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
event_button_3:
|
||||||
|
name: 'Channel 3: Button'
|
||||||
|
description: 'Event: Look for ''Button (9)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- event
|
||||||
|
integration: matter
|
||||||
|
device_class:
|
||||||
|
- button
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
group_scroll_wheel:
|
||||||
|
name: Scroll Wheel
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
sensor_1:
|
||||||
|
name: 'Channel 1: Scroll forwards'
|
||||||
|
description: 'Sensor: Look for ''Current switch position (1)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_2:
|
||||||
|
name: 'Channel 1: Scroll backwards'
|
||||||
|
description: 'Sensor: Look for ''Current switch position (2)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_4:
|
||||||
|
name: 'Channel 2: Scroll forwards'
|
||||||
|
description: 'Sensor: Look for ''Current switch position (4)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_5:
|
||||||
|
name: 'Channel 2: Scroll backwards'
|
||||||
|
description: 'Sensor: Look for ''Current switch position (5)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_7:
|
||||||
|
name: 'Channel 3: Scroll forwards'
|
||||||
|
description: 'Sensor: Look for ''Current switch position (7)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
sensor_8:
|
||||||
|
name: 'Channel 3: Scroll backwards'
|
||||||
|
description: 'Sensor: Look for ''Current switch position (8)'''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
integration: matter
|
||||||
|
multiple: false
|
||||||
|
reorder: false
|
||||||
|
header_default_light:
|
||||||
|
name: Default Light
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
global_light:
|
||||||
|
name: Global Light - Default [REQUIRED]
|
||||||
|
description: This light will be used for any zone or button that does not have
|
||||||
|
a specific override light set below.
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
header_1:
|
||||||
|
name: Channel 1
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_scroll_1:
|
||||||
|
name: Scroll Wheel Settings
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_z1:
|
||||||
|
name: Light (Scroll - Channel 1) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
scroll_logic_1_2:
|
||||||
|
name: Scroll - Channel 1
|
||||||
|
description: Sensors 1 & 2
|
||||||
|
default: brightness
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: Brightness (Bright / Dark)
|
||||||
|
value: brightness
|
||||||
|
- label: Kelvin (Cool / Warm)
|
||||||
|
value: kelvin
|
||||||
|
- label: RGB Colour (Rainbow)
|
||||||
|
value: rgb
|
||||||
|
- label: Presets (Work / Cozy)
|
||||||
|
value: preset
|
||||||
|
sort: false
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
group_button_1_single:
|
||||||
|
name: 'Button 1: Single Click (Toggle On/Off)'
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b1_single:
|
||||||
|
name: Light (Button 1 Single Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
group_button_1_double:
|
||||||
|
name: 'Button 1: Double Click'
|
||||||
|
icon: mdi:gesture-double-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b1_double:
|
||||||
|
name: Light (Button 1 Double Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn1_double_bright:
|
||||||
|
name: 'Button 1: Double Click Brightness (%)'
|
||||||
|
default: 80
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn1_double_temp:
|
||||||
|
name: 'Button 1: Double Click Colour Temp (K)'
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_button_1_triple:
|
||||||
|
name: 'Button 1: Triple Click'
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b1_triple:
|
||||||
|
name: Light (Button 1 Triple Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn1_triple_bright:
|
||||||
|
name: 'Button 1: Triple Click Brightness (%)'
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn1_triple_temp:
|
||||||
|
name: 'Button 1: Triple Click Colour Temp (K)'
|
||||||
|
default: 6000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_button_1_hold:
|
||||||
|
name: 'Button 1: Hold'
|
||||||
|
icon: mdi:gesture-tap-hold
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b1_hold:
|
||||||
|
name: Light (Button 1 Hold) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn1_hold_bright:
|
||||||
|
name: 'Button 1: Hold Brightness (%)'
|
||||||
|
default: 20
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn1_hold_temp:
|
||||||
|
name: 'Button 1: Hold Colour Temp'
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
header_2:
|
||||||
|
name: Channel 2
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_scroll_2:
|
||||||
|
name: Scroll Wheel Settings
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_z2:
|
||||||
|
name: Light (Scroll - Channel 2) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
scroll_logic_4_5:
|
||||||
|
name: Scroll - Channel 2
|
||||||
|
description: Sensors 4 & 5
|
||||||
|
default: kelvin
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: Brightness (Bright / Dark)
|
||||||
|
value: brightness
|
||||||
|
- label: Kelvin (Cool / Warm)
|
||||||
|
value: kelvin
|
||||||
|
- label: RGB Colour (Rainbow)
|
||||||
|
value: rgb
|
||||||
|
- label: Presets (Work / Cozy)
|
||||||
|
value: preset
|
||||||
|
sort: false
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
group_button_2_single:
|
||||||
|
name: 'Button 2: Single Click (Toggle On/Off)'
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b2_single:
|
||||||
|
name: Light (Button 2 Single Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
group_button_2_double:
|
||||||
|
name: 'Button 2: Double Click'
|
||||||
|
icon: mdi:gesture-double-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b2_double:
|
||||||
|
name: Light (Button 2 Double Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn2_double_bright:
|
||||||
|
name: 'Button 2: Double Click Brightness (%)'
|
||||||
|
default: 80
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn2_double_temp:
|
||||||
|
name: 'Button 2: Double Click Colour Temp (K)'
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_button_2_triple:
|
||||||
|
name: 'Button 2: Triple Click'
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b2_triple:
|
||||||
|
name: Light (Button 2 Triple Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn2_triple_bright:
|
||||||
|
name: 'Button 2: Triple Click Brightness (%)'
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn2_triple_temp:
|
||||||
|
name: 'Button 2: Triple Click Colour Temp (K)'
|
||||||
|
default: 6000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_button_2_hold:
|
||||||
|
name: 'Button 2: Hold'
|
||||||
|
icon: mdi:gesture-tap-hold
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b2_hold:
|
||||||
|
name: Light (Button 2 Hold) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn2_hold_bright:
|
||||||
|
name: 'Button 2: Hold Brightness (%)'
|
||||||
|
default: 20
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn2_hold_temp:
|
||||||
|
name: 'Button 2: Hold Colour Temp (K)'
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
header_3:
|
||||||
|
name: Channel 3
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_scroll_3:
|
||||||
|
name: Scroll Wheel Settings
|
||||||
|
icon: mdi:rotate-right
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_z3:
|
||||||
|
name: Light (Scroll - Channel 3) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
scroll_logic_7_8:
|
||||||
|
name: Scroll - Channel 3
|
||||||
|
description: Sensors 7 & 8
|
||||||
|
default: preset
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- label: Brightness (Bright / Dark)
|
||||||
|
value: brightness
|
||||||
|
- label: Kelvin (Cool / Warm)
|
||||||
|
value: kelvin
|
||||||
|
- label: RGB Colour (Rainbow)
|
||||||
|
value: rgb
|
||||||
|
- label: Presets (Work / Cozy)
|
||||||
|
value: preset
|
||||||
|
sort: false
|
||||||
|
custom_value: false
|
||||||
|
multiple: false
|
||||||
|
group_button_3_single:
|
||||||
|
name: 'Button 3: Single Click (Toggle On/Off)'
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b3_single:
|
||||||
|
name: Light (Button 3 Single Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
group_button_3_double:
|
||||||
|
name: 'Button 3: Double Click'
|
||||||
|
icon: mdi:gesture-double-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b3_double:
|
||||||
|
name: Light (Button 3 Double Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn3_double_bright:
|
||||||
|
name: 'Button 3: Double Click Brightness (%)'
|
||||||
|
default: 80
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn3_double_temp:
|
||||||
|
name: 'Button 3: Double Click Colour Temp (K)'
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_button_3_triple:
|
||||||
|
name: 'Button 3: Triple Click'
|
||||||
|
icon: mdi:gesture-tap
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b3_triple:
|
||||||
|
name: Light (Button 3 Triple Click) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn3_triple_bright:
|
||||||
|
name: 'Button 3: Triple Click Brightness (%)'
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn3_triple_temp:
|
||||||
|
name: 'Button 3: Triple Click Colour Temp (K)'
|
||||||
|
default: 6000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_button_3_hold:
|
||||||
|
name: 'Button 3: Hold'
|
||||||
|
icon: mdi:gesture-tap-hold
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
light_b3_hold:
|
||||||
|
name: Light (Button 3 Hold) - Optional Override
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain:
|
||||||
|
- light
|
||||||
|
multiple: true
|
||||||
|
reorder: false
|
||||||
|
btn3_hold_bright:
|
||||||
|
name: 'Button 3: Hold Brightness (%)'
|
||||||
|
default: 20
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
btn3_hold_temp:
|
||||||
|
name: 'Button 3: Hold Colour Temp (K)'
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
header_tuning:
|
||||||
|
name: Fine Tuning
|
||||||
|
default: ''
|
||||||
|
selector:
|
||||||
|
constant:
|
||||||
|
value: ''
|
||||||
|
group_step_size:
|
||||||
|
name: Step Sizes
|
||||||
|
icon: mdi:tune
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
step_pct:
|
||||||
|
name: 'Step Size: Brightness (%)'
|
||||||
|
default: 10
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 50.0
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
step_kelvin:
|
||||||
|
name: 'Step Size: Kelvin (K)'
|
||||||
|
default: 400
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 100.0
|
||||||
|
max: 1000.0
|
||||||
|
unit_of_measurement: K
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
step_hue:
|
||||||
|
name: 'Step Size: RGB Hue (°)'
|
||||||
|
default: 15
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 5.0
|
||||||
|
max: 90.0
|
||||||
|
unit_of_measurement: °
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
group_values:
|
||||||
|
name: Preset Values
|
||||||
|
icon: mdi:brightness-6
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
val_bright:
|
||||||
|
name: 'Work: Brightness (%)'
|
||||||
|
description: The brightness value to use for the 'Work' preset
|
||||||
|
default: 80
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
val_cold:
|
||||||
|
name: 'Work: Colour Temperature (K)'
|
||||||
|
description: The colour temperature value to use for the 'Work' preset
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
val_dim:
|
||||||
|
name: 'Cozy: Brightness (%)'
|
||||||
|
description: The brightness value to use for the 'Cozy' preset
|
||||||
|
default: 20
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 100.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
val_warm:
|
||||||
|
name: 'Cozy: Colour Temperature (K)'
|
||||||
|
description: The colour temperature value to use for the 'Cozy' preset
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
color_temp:
|
||||||
|
unit: kelvin
|
||||||
|
min: 2000
|
||||||
|
max: 6500
|
||||||
|
group_timings:
|
||||||
|
name: Timings
|
||||||
|
icon: mdi:clock
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
transition_time:
|
||||||
|
name: Button Transition Time (Seconds)
|
||||||
|
default: 0.5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 5.0
|
||||||
|
step: 0.1
|
||||||
|
mode: slider
|
||||||
|
delay_time:
|
||||||
|
name: Delay (Milliseconds)
|
||||||
|
description: Delay between command actions. This is effectively button debounce.
|
||||||
|
default: 150
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 1000.0
|
||||||
|
step: 10.0
|
||||||
|
mode: slider
|
||||||
|
mode: single
|
||||||
|
max_exceeded: silent
|
||||||
|
variables:
|
||||||
|
global_l: !input global_light
|
||||||
|
l_z1: !input light_z1
|
||||||
|
l_z2: !input light_z2
|
||||||
|
l_z3: !input light_z3
|
||||||
|
l_b1_s: !input light_b1_single
|
||||||
|
l_b1_d: !input light_b1_double
|
||||||
|
l_b1_t: !input light_b1_triple
|
||||||
|
l_b1_h: !input light_b1_hold
|
||||||
|
l_b2_s: !input light_b2_single
|
||||||
|
l_b2_d: !input light_b2_double
|
||||||
|
l_b2_t: !input light_b2_triple
|
||||||
|
l_b2_h: !input light_b2_hold
|
||||||
|
l_b3_s: !input light_b3_single
|
||||||
|
l_b3_d: !input light_b3_double
|
||||||
|
l_b3_t: !input light_b3_triple
|
||||||
|
l_b3_h: !input light_b3_hold
|
||||||
|
m_sc_1: !input scroll_logic_1_2
|
||||||
|
m_sc_2: !input scroll_logic_4_5
|
||||||
|
m_sc_3: !input scroll_logic_7_8
|
||||||
|
b1_d_b: !input btn1_double_bright
|
||||||
|
b1_d_t: !input btn1_double_temp
|
||||||
|
b1_t_b: !input btn1_triple_bright
|
||||||
|
b1_t_t: !input btn1_triple_temp
|
||||||
|
b1_h_b: !input btn1_hold_bright
|
||||||
|
b1_h_t: !input btn1_hold_temp
|
||||||
|
b2_d_b: !input btn2_double_bright
|
||||||
|
b2_d_t: !input btn2_double_temp
|
||||||
|
b2_t_b: !input btn2_triple_bright
|
||||||
|
b2_t_t: !input btn2_triple_temp
|
||||||
|
b2_h_b: !input btn2_hold_bright
|
||||||
|
b2_h_t: !input btn2_hold_temp
|
||||||
|
b3_d_b: !input btn3_double_bright
|
||||||
|
b3_d_t: !input btn3_double_temp
|
||||||
|
b3_t_b: !input btn3_triple_bright
|
||||||
|
b3_t_t: !input btn3_triple_temp
|
||||||
|
b3_h_b: !input btn3_hold_bright
|
||||||
|
b3_h_t: !input btn3_hold_temp
|
||||||
|
s_p: !input step_pct
|
||||||
|
s_k: !input step_kelvin
|
||||||
|
s_h: !input step_hue
|
||||||
|
v_bri: !input val_bright
|
||||||
|
v_dim: !input val_dim
|
||||||
|
v_cold: !input val_cold
|
||||||
|
v_warm: !input val_warm
|
||||||
|
t_time: !input transition_time
|
||||||
|
d_time: !input delay_time
|
||||||
|
id_e1: !input event_button_1
|
||||||
|
id_e2: !input event_button_2
|
||||||
|
id_e3: !input event_button_3
|
||||||
|
triggers:
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_1
|
||||||
|
id: sc_1_up
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_2
|
||||||
|
id: sc_1_down
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_4
|
||||||
|
id: sc_2_down
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_5
|
||||||
|
id: sc_2_up
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_7
|
||||||
|
id: sc_3_up
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input sensor_8
|
||||||
|
id: sc_3_down
|
||||||
|
- trigger: state
|
||||||
|
entity_id:
|
||||||
|
- !input event_button_1
|
||||||
|
- !input event_button_2
|
||||||
|
- !input event_button_3
|
||||||
|
id: btn_event
|
||||||
|
conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.from_state is defined and trigger.to_state is defined
|
||||||
|
and\n trigger.from_state.state not in ['unknown', 'unavailable'] and\n trigger.to_state.state
|
||||||
|
not in ['unknown', 'unavailable'] and\n trigger.from_state.state != trigger.to_state.state
|
||||||
|
}}"
|
||||||
|
actions:
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ ''sc_'' in trigger.id }}'
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_light: '{{ l_z1 if ''sc_1'' in trigger.id and l_z1 else l_z2 if ''sc_2''
|
||||||
|
in trigger.id and l_z2 else l_z3 if ''sc_3'' in trigger.id and l_z3 else
|
||||||
|
global_l }}'
|
||||||
|
direction: '{{ 1 if ''_up'' in trigger.id else -1 }}'
|
||||||
|
active_func: '{{ m_sc_1 if ''sc_1'' in trigger.id else m_sc_2 if ''sc_2''
|
||||||
|
in trigger.id else m_sc_3 }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ active_func == ''brightness'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_step_pct: '{{ (s_p | float(default=10)) * direction }}'
|
||||||
|
transition: 0
|
||||||
|
- conditions: '{{ active_func == ''kelvin'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
color_temp_kelvin: '{% set entities = expand(target_light) | map(attribute=''entity_id'')
|
||||||
|
| list %} {% set first_light = entities[0] if entities else ''light.none''
|
||||||
|
%} {% set current = state_attr(first_light, ''color_temp_kelvin'') |
|
||||||
|
int(default=3000) %} {% set step = s_k | int(default=400) %} {% set
|
||||||
|
min_k = state_attr(first_light, ''min_color_temp_kelvin'') | int(default=2000)
|
||||||
|
%} {% set max_k = state_attr(first_light, ''max_color_temp_kelvin'')
|
||||||
|
| int(default=6500) %} {% if direction == 1 %} {{ ([current + step,
|
||||||
|
max_k] | min) | int }} {% else %} {{ ([current - step, min_k] | max)
|
||||||
|
| int }} {% endif %}
|
||||||
|
|
||||||
|
'
|
||||||
|
transition: 0
|
||||||
|
- conditions: '{{ active_func == ''rgb'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
hs_color: '{% set entities = expand(target_light) | map(attribute=''entity_id'')
|
||||||
|
| list %} {% set first_light = entities[0] if entities else ''light.none''
|
||||||
|
%} {% set current_hs = state_attr(first_light, ''hs_color'') %} {% set
|
||||||
|
h = current_hs[0] | float(default=0) if current_hs is not none else
|
||||||
|
0 %} {% set s = current_hs[1] | float(default=100) if current_hs is
|
||||||
|
not none else 100 %} {% set step = s_h | float(default=15) %} {{ [(h
|
||||||
|
+ (step * direction)) % 360, s] }}
|
||||||
|
|
||||||
|
'
|
||||||
|
transition: 0
|
||||||
|
- conditions: '{{ active_func == ''preset'' }}'
|
||||||
|
sequence:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ v_bri if direction == 1 else v_dim }}'
|
||||||
|
color_temp_kelvin: '{{ v_cold if direction == 1 else v_warm }}'
|
||||||
|
transition: 1
|
||||||
|
- choose:
|
||||||
|
- conditions: '{{ trigger.id == ''btn_event'' }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'multi_press_1' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_light: '{{ l_b1_s if trigger.entity_id == id_e1 and l_b1_s else
|
||||||
|
l_b2_s if trigger.entity_id == id_e2 and l_b2_s else l_b3_s if trigger.entity_id
|
||||||
|
== id_e3 and l_b3_s else global_l }}'
|
||||||
|
- action: light.toggle
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
transition: '{{ t_time }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'multi_press_2' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_light: '{{ l_b1_d if trigger.entity_id == id_e1 and l_b1_d else
|
||||||
|
l_b2_d if trigger.entity_id == id_e2 and l_b2_d else l_b3_d if trigger.entity_id
|
||||||
|
== id_e3 and l_b3_d else global_l }}'
|
||||||
|
set_bri: '{{ b1_d_b if trigger.entity_id == id_e1 else b2_d_b if trigger.entity_id
|
||||||
|
== id_e2 else b3_d_b }}'
|
||||||
|
set_temp: '{{ b1_d_t if trigger.entity_id == id_e1 else b2_d_t if trigger.entity_id
|
||||||
|
== id_e2 else b3_d_t }}'
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ set_bri }}'
|
||||||
|
color_temp_kelvin: '{{ set_temp }}'
|
||||||
|
transition: '{{ t_time }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'multi_press_3' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_light: '{{ l_b1_t if trigger.entity_id == id_e1 and l_b1_t else
|
||||||
|
l_b2_t if trigger.entity_id == id_e2 and l_b2_t else l_b3_t if trigger.entity_id
|
||||||
|
== id_e3 and l_b3_t else global_l }}'
|
||||||
|
set_bri: '{{ b1_t_b if trigger.entity_id == id_e1 else b2_t_b if trigger.entity_id
|
||||||
|
== id_e2 else b3_t_b }}'
|
||||||
|
set_temp: '{{ b1_t_t if trigger.entity_id == id_e1 else b2_t_t if trigger.entity_id
|
||||||
|
== id_e2 else b3_t_t }}'
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ set_bri }}'
|
||||||
|
color_temp_kelvin: '{{ set_temp }}'
|
||||||
|
transition: '{{ t_time }}'
|
||||||
|
- choose:
|
||||||
|
- conditions: "{{ trigger.to_state.attributes.event_type is defined and \n trigger.to_state.attributes.event_type
|
||||||
|
== 'long_press' }}\n"
|
||||||
|
sequence:
|
||||||
|
- variables:
|
||||||
|
target_light: '{{ l_b1_h if trigger.entity_id == id_e1 and l_b1_h else
|
||||||
|
l_b2_h if trigger.entity_id == id_e2 and l_b2_h else l_b3_h if trigger.entity_id
|
||||||
|
== id_e3 and l_b3_h else global_l }}'
|
||||||
|
set_bri: '{{ b1_h_b if trigger.entity_id == id_e1 else b2_h_b if trigger.entity_id
|
||||||
|
== id_e2 else b3_h_b }}'
|
||||||
|
set_temp: '{{ b1_h_t if trigger.entity_id == id_e1 else b2_h_t if trigger.entity_id
|
||||||
|
== id_e2 else b3_h_t }}'
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ target_light }}'
|
||||||
|
data:
|
||||||
|
brightness_pct: '{{ set_bri }}'
|
||||||
|
color_temp_kelvin: '{{ set_temp }}'
|
||||||
|
transition: '{{ t_time }}'
|
||||||
|
- delay:
|
||||||
|
milliseconds: '{{ d_time }}'
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
blueprint:
|
||||||
|
name: dimmer_control_4_magic_cube_and_other_things - 2023-12-07
|
||||||
|
author: SirGoodenough
|
||||||
|
domain: script
|
||||||
|
homeassistant:
|
||||||
|
min_version: 2023.8.0
|
||||||
|
source_url: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Scripts/dimmer_control_4_magic_cube.yaml
|
||||||
|
description: "This script was invented to be used with my Magic Cube Blueprints.
|
||||||
|
I released it as a standalone Script Blueprint because I saw others struggling
|
||||||
|
with how so convert cube rotation to light dimming in other places.\n\nTo use
|
||||||
|
this blueprint, install it from the source in the normal way. After that add your
|
||||||
|
unique name for the Script, and add an icon and change the entity_id if desired.
|
||||||
|
You should only need to run the blueprint once, as you will be calling the unique
|
||||||
|
name/entity you added above to use the code. This script has 2 inputs in the Blueprint
|
||||||
|
screen to set the sensitivity of the rotation action and the minimum bbrightness
|
||||||
|
level for your lights. If the rotation is too sensitive the lights will change
|
||||||
|
from dim to bright in too short of a rotation so make it less sensitive. If the
|
||||||
|
lights flicker at low brightness, raise the minimum brightness level to stop it.
|
||||||
|
The data is sent to the generated script to control the action live at each use.
|
||||||
|
You only need to ececute this blueprint generator script if you wish to change
|
||||||
|
in input values or create another dimmer script.\n\nThis requires you call this
|
||||||
|
script wth 2 data values. One is a positive or negative number between 360 and
|
||||||
|
-360 that in the original, represents the input angle from the cube movement.
|
||||||
|
The other data point is the entity of the light or light group that you are trying
|
||||||
|
to control.\n***NOTE:*** the trigger variable below will be different for Z2M
|
||||||
|
and ZHA and others. Look at my documentation for that cube integration help if
|
||||||
|
you need it.\n\n## \U0001F5FFNotice\U0001F46E\U0001F3FF♂️:\n* Copies of the original
|
||||||
|
Blueprint that were converted via the 'Take Control' feature or other means are
|
||||||
|
officially not supported by me.\n\n* I may or may not be able to support you when
|
||||||
|
you have a problem after you make changes to my code, as some of the code is no
|
||||||
|
longer mine.\n\n* I & my license also require attribution as a link back to the
|
||||||
|
original should you use this code in your own creation.\n\n* [Here is a link to
|
||||||
|
my license & the original github post](https://github.com/SirGoodenough/HA_Blueprints?tab=License-1-ov-file)
|
||||||
|
expected to be followed & referenced as attribution should you use this code elsewhere.\n\n
|
||||||
|
\ Sample call / use of this script for ZHA:\n ```yaml\n - action: script.cube_dimmer_control_bp\n
|
||||||
|
\ data:\n angle: '{{ trigger.event.data.args.relative_degrees | default(0.1)
|
||||||
|
| float(0.2) }}'\n light: light.grp_studio\n ```\n\n Sample call /
|
||||||
|
use of this script for Z2M:\n ```yaml\n - action: script.cube_dimmer_control_bp\n
|
||||||
|
\ data:\n angle: '{{ trigger.payload_json.action_angle | default(0.1)
|
||||||
|
| float(0.2) }}'\n light: light.grp_kitch\n ```\n"
|
||||||
|
input:
|
||||||
|
sensi:
|
||||||
|
name: Sensitivity of the cube rotational motion.
|
||||||
|
description: 'This sets how ''fast'' the changes happen compared to how far
|
||||||
|
you move the cube. Higher percentage is faster, lower is slower.
|
||||||
|
|
||||||
|
I have found that 40% is a good number, but you can adjust as desired.
|
||||||
|
|
||||||
|
'
|
||||||
|
default: 40
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 200.0
|
||||||
|
step: 1.0
|
||||||
|
mode: box
|
||||||
|
min_b:
|
||||||
|
name: Minimum Brighness
|
||||||
|
description: 'This sets the lowest brightness it will set before it turns off.
|
||||||
|
Some bulbs react poorly below a certain percent, you can adjust that here.
|
||||||
|
|
||||||
|
I have found that 10% is a good number, but you can adjust as desired.
|
||||||
|
|
||||||
|
'
|
||||||
|
default: 10
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 5.0
|
||||||
|
max: 90.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
transition_i:
|
||||||
|
name: Transition Input
|
||||||
|
description: Seconds to take to change light one level to another
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 1800.0
|
||||||
|
step: 0.5
|
||||||
|
mode: slider
|
||||||
|
fields:
|
||||||
|
angle:
|
||||||
|
name: Angle to represent new brightness
|
||||||
|
description: ' This is the new angle to be used in the calculation for Brightness'
|
||||||
|
required: true
|
||||||
|
example: '-120.7'
|
||||||
|
default: -120.7
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: -360
|
||||||
|
max: 360
|
||||||
|
step: 0.1
|
||||||
|
mode: box
|
||||||
|
light:
|
||||||
|
name: Light entity to control
|
||||||
|
description: Add the light entity you want to control here
|
||||||
|
required: true
|
||||||
|
example: light.grp_studio
|
||||||
|
default: light.grp_studio
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: false
|
||||||
|
filter:
|
||||||
|
domain: light
|
||||||
|
transition:
|
||||||
|
name: Transition Field
|
||||||
|
description: Seconds to take to change light one level to another
|
||||||
|
required: false
|
||||||
|
example: '4'
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 1800
|
||||||
|
step: 0.5
|
||||||
|
variables:
|
||||||
|
sensi_var: !input sensi
|
||||||
|
sens_d: '{{ sensi_var / 100 | default( 0.4 ) }}'
|
||||||
|
min_b_var: !input min_b
|
||||||
|
bright_p: "{% set step_size = angle * sens_d %}\n {# Get brightness as a percent.
|
||||||
|
#}\n{% set cb = (state_attr( light, 'brightness') | float(10) / 255.0) * 100.0
|
||||||
|
%} {% set new_brightness = cb | int(10) + step_size %} {% if new_brightness <
|
||||||
|
5 %}\n {# If it gets really low set to 0. Adjust if needed for your lights. #}\n
|
||||||
|
\ 0\n{% elif new_brightness <= min_b_var %}\n {# If it's not quite 0, set to
|
||||||
|
a minimum working brightness. #}\n {{ min_b_var }}\n{% elif 91 <= new_brightness
|
||||||
|
< (90 + step_size) %}\n {# If it's almost full brightness, set to 100%. #}\n
|
||||||
|
\ 100\n{% else %}\n {# Send actual calculated value. #}\n{{ new_brightness }}
|
||||||
|
{% endif %}"
|
||||||
|
trans_i: !input transition_i
|
||||||
|
transition_var: "{% if is_number(transition | default('none')) %}\n {{ transition
|
||||||
|
}}\n{% elif is_number(trans_i | default('none')) %}\n {{ trans_i }}\n{% else
|
||||||
|
%}\n none\n{% endif %}"
|
||||||
|
sequence:
|
||||||
|
- if:
|
||||||
|
- alias: turn the light & set brightness
|
||||||
|
condition: template
|
||||||
|
value_template: '{{ is_number(transition_var) }}'
|
||||||
|
then:
|
||||||
|
- alias: Transition number IS available
|
||||||
|
action: light.turn_on
|
||||||
|
data:
|
||||||
|
entity_id: '{{ light }}'
|
||||||
|
brightness_pct: '{{ bright_p }}'
|
||||||
|
transition: '{{ transition_var }}'
|
||||||
|
else:
|
||||||
|
- alias: NO transition number available
|
||||||
|
action: light.turn_on
|
||||||
|
data:
|
||||||
|
entity_id: '{{ light }}'
|
||||||
|
brightness_pct: '{{ bright_p }}'
|
||||||
|
icon: mdi:lightbulb-on-50
|
||||||
|
mode: queued
|
||||||
|
max: 10
|
||||||
@@ -4,6 +4,12 @@ default_config:
|
|||||||
# Core Utility Meter Engine (Required for Hilo to generate compatible energy dashboard metrics)
|
# Core Utility Meter Engine (Required for Hilo to generate compatible energy dashboard metrics)
|
||||||
utility_meter:
|
utility_meter:
|
||||||
|
|
||||||
|
# Load ffmpeg
|
||||||
|
ffmpeg:
|
||||||
|
|
||||||
|
# Enable Streams
|
||||||
|
stream:
|
||||||
|
|
||||||
# Load frontend themes from the themes folder
|
# Load frontend themes from the themes folder
|
||||||
frontend:
|
frontend:
|
||||||
themes: !include_dir_merge_named themes
|
themes: !include_dir_merge_named themes
|
||||||
@@ -60,13 +66,13 @@ template:
|
|||||||
device_class: power
|
device_class: power
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
state: >
|
state: >
|
||||||
{% set hvac = states.climate | selectattr('entity_id', 'search', 'living_room') | map(attribute='entity_id') | first %}
|
{% set hvac = 'climate.living_room_living_room' %}
|
||||||
{% if hvac %}
|
{% if states(hvac) not in ['unknown', 'unavailable'] %}
|
||||||
{% set action = state_attr(hvac, 'hvac_action') | string | lower %}
|
{% set action = state_attr(hvac, 'hvac_action') | string | lower %}
|
||||||
{% if action == 'cooling' %}
|
{% if action == 'cooling' %}
|
||||||
2.12
|
2.12
|
||||||
{% elif action == 'heating' %}
|
{% elif action == 'heating' %}
|
||||||
{% if is_state_attr(hvac, 'is_aux_heat', true) %}
|
{% if is_state_attr(hvac, 'preset_mode', 'eco') or is_state_attr(hvac, 'aux_heat', 'on') %}
|
||||||
20.5
|
20.5
|
||||||
{% else %}
|
{% else %}
|
||||||
2.5
|
2.5
|
||||||
@@ -111,14 +117,66 @@ template:
|
|||||||
{% set raw_watts = states('sensor.water_heater_power') | float(0) %}
|
{% set raw_watts = states('sensor.water_heater_power') | float(0) %}
|
||||||
{{ (raw_watts / 1000) | round(2) }}
|
{{ (raw_watts / 1000) | round(2) }}
|
||||||
|
|
||||||
|
# State-based Template Sensors (Blink Temperature Dropout Filtering for all 4 cameras)
|
||||||
|
- sensor:
|
||||||
|
- name: "Blink Front Temperature Clean"
|
||||||
|
unique_id: blink_front_temperature_clean
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
device_class: temperature
|
||||||
|
state_class: measurement
|
||||||
|
state: >
|
||||||
|
{% set raw = states('sensor.blink_front_temperature') | float(default=-999) %}
|
||||||
|
{% if raw > -40 and raw < 60 %}
|
||||||
|
{{ raw }}
|
||||||
|
{% else %}
|
||||||
|
{{ states('sensor.blink_front_temperature_clean') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
- name: "Blink Backyard Shed Temperature Clean"
|
||||||
|
unique_id: blink_backyard_shed_temperature_clean
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
device_class: temperature
|
||||||
|
state_class: measurement
|
||||||
|
state: >
|
||||||
|
{% set raw = states('sensor.blink_backyard_shed_temperature') | float(default=-999) %}
|
||||||
|
{% if raw > -40 and raw < 60 %}
|
||||||
|
{{ raw }}
|
||||||
|
{% else %}
|
||||||
|
{{ states('sensor.blink_backyard_shed_temperature_clean') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
- name: "Blink Backyard Tree Temperature Clean"
|
||||||
|
unique_id: blink_backyard_tree_temperature_clean
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
device_class: temperature
|
||||||
|
state_class: measurement
|
||||||
|
state: >
|
||||||
|
{% set raw = states('sensor.blink_backyard_tree_temperature') | float(default=-999) %}
|
||||||
|
{% if raw > -40 and raw < 60 %}
|
||||||
|
{{ raw }}
|
||||||
|
{% else %}
|
||||||
|
{{ states('sensor.blink_backyard_tree_temperature_clean') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
- name: "Blink Backyard Temperature Clean"
|
||||||
|
unique_id: blink_backyard_temperature_clean
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
device_class: temperature
|
||||||
|
state_class: measurement
|
||||||
|
state: >
|
||||||
|
{% set raw = states('sensor.blink_backyard_temperature') | float(default=-999) %}
|
||||||
|
{% if raw > -40 and raw < 60 %}
|
||||||
|
{{ raw }}
|
||||||
|
{% else %}
|
||||||
|
{{ states('sensor.blink_backyard_temperature_clean') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
# HTTP Proxy Configuration
|
# HTTP Proxy Configuration
|
||||||
http:
|
http:
|
||||||
use_x_forwarded_for: true
|
use_x_forwarded_for: true
|
||||||
trusted_proxies:
|
trusted_proxies:
|
||||||
- 192.168.122.1
|
- 192.168.122.1
|
||||||
- 192.168.1.130
|
- 192.168.1.0/24
|
||||||
- 192.168.1.125
|
|
||||||
- 192.168.1.135
|
|
||||||
|
|
||||||
# Legacy and Integration Sensor Section
|
# Legacy and Integration Sensor Section
|
||||||
sensor:
|
sensor:
|
||||||
@@ -146,7 +204,7 @@ emulated_hue:
|
|||||||
host_ip: 192.168.1.140
|
host_ip: 192.168.1.140
|
||||||
advertise_ip: 192.168.1.140
|
advertise_ip: 192.168.1.140
|
||||||
listen_port: 80
|
listen_port: 80
|
||||||
expose_by_default: false
|
expose_by_default: true
|
||||||
entities:
|
entities:
|
||||||
input_boolean.cync_motion_bridge:
|
input_boolean.cync_motion_bridge:
|
||||||
name: "Cync Motion Bridge"
|
name: "Cync Motion Bridge"
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
"""The AI Agent HA integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.components.frontend import async_register_built_in_panel
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .agent import AiAgentHaAgent
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Config schema - this integration only supports config entries
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
# Define service schema to accept a custom prompt
|
||||||
|
SERVICE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional("prompt"): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the AI Agent HA component."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Migrate old config entries to new version."""
|
||||||
|
_LOGGER.debug("Migrating config entry from version %s", entry.version)
|
||||||
|
|
||||||
|
if entry.version == 1:
|
||||||
|
# No migration needed for version 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Future migrations would go here
|
||||||
|
# if entry.version < 2:
|
||||||
|
# # Migrate from version 1 to 2
|
||||||
|
# new_data = dict(entry.data)
|
||||||
|
# # Add migration logic here
|
||||||
|
# hass.config_entries.async_update_entry(entry, data=new_data, version=2)
|
||||||
|
|
||||||
|
_LOGGER.info("Migration to version %s successful", entry.version)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up AI Agent HA from a config entry."""
|
||||||
|
try:
|
||||||
|
# Handle version compatibility
|
||||||
|
if not hasattr(entry, "version") or entry.version != 1:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Config entry has version %s, expected 1. Attempting compatibility mode.",
|
||||||
|
getattr(entry, "version", "unknown"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert ConfigEntry to dict and ensure all required keys exist
|
||||||
|
config_data = dict(entry.data)
|
||||||
|
|
||||||
|
# Ensure backward compatibility - check for required keys
|
||||||
|
if "ai_provider" not in config_data:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Config entry missing required 'ai_provider' key. Entry data: %s",
|
||||||
|
config_data,
|
||||||
|
)
|
||||||
|
raise ConfigEntryNotReady("Config entry missing required 'ai_provider' key")
|
||||||
|
|
||||||
|
if DOMAIN not in hass.data:
|
||||||
|
hass.data[DOMAIN] = {"agents": {}, "configs": {}}
|
||||||
|
|
||||||
|
provider = config_data["ai_provider"]
|
||||||
|
|
||||||
|
# Validate provider
|
||||||
|
if provider not in [
|
||||||
|
"llama",
|
||||||
|
"openai",
|
||||||
|
"gemini",
|
||||||
|
"openrouter",
|
||||||
|
"anthropic",
|
||||||
|
"alter",
|
||||||
|
"zai",
|
||||||
|
"local_ollama",
|
||||||
|
"openai_compatible",
|
||||||
|
]:
|
||||||
|
_LOGGER.error("Unknown AI provider: %s", provider)
|
||||||
|
raise ConfigEntryNotReady(f"Unknown AI provider: {provider}")
|
||||||
|
|
||||||
|
# Store config for this provider
|
||||||
|
hass.data[DOMAIN]["configs"][provider] = config_data
|
||||||
|
|
||||||
|
# Create agent for this provider
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Creating AI agent for provider %s with config: %s",
|
||||||
|
provider,
|
||||||
|
{
|
||||||
|
k: v
|
||||||
|
for k, v in config_data.items()
|
||||||
|
if k
|
||||||
|
not in [
|
||||||
|
"llama_token",
|
||||||
|
"openai_token",
|
||||||
|
"gemini_token",
|
||||||
|
"openrouter_token",
|
||||||
|
"anthropic_token",
|
||||||
|
"zai_token",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hass.data[DOMAIN]["agents"][provider] = AiAgentHaAgent(hass, config_data)
|
||||||
|
|
||||||
|
_LOGGER.info("Successfully set up AI Agent HA for provider: %s", provider)
|
||||||
|
|
||||||
|
except KeyError as err:
|
||||||
|
_LOGGER.error("Missing required configuration key: %s", err)
|
||||||
|
raise ConfigEntryNotReady(f"Missing required configuration key: {err}")
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.exception("Unexpected error setting up AI Agent HA")
|
||||||
|
raise ConfigEntryNotReady(f"Error setting up AI Agent HA: {err}")
|
||||||
|
|
||||||
|
# Modify the query service handler to use the correct provider
|
||||||
|
async def async_handle_query(call):
|
||||||
|
"""Handle the query service call."""
|
||||||
|
try:
|
||||||
|
# Check if agents are available
|
||||||
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
||||||
|
_LOGGER.error(
|
||||||
|
"No AI agents available. Please configure the integration first."
|
||||||
|
)
|
||||||
|
result = {"error": "No AI agents configured"}
|
||||||
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
||||||
|
return
|
||||||
|
|
||||||
|
provider = call.data.get("provider")
|
||||||
|
if provider not in hass.data[DOMAIN]["agents"]:
|
||||||
|
# Get the first available provider
|
||||||
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
||||||
|
if not available_providers:
|
||||||
|
_LOGGER.error("No AI agents available")
|
||||||
|
result = {"error": "No AI agents configured"}
|
||||||
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
||||||
|
return
|
||||||
|
provider = available_providers[0]
|
||||||
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
||||||
|
|
||||||
|
agent = hass.data[DOMAIN]["agents"][provider]
|
||||||
|
result = await agent.process_query(
|
||||||
|
call.data.get("prompt", ""),
|
||||||
|
provider=provider,
|
||||||
|
debug=call.data.get("debug", False),
|
||||||
|
)
|
||||||
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error processing query: {e}")
|
||||||
|
result = {"error": str(e)}
|
||||||
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
||||||
|
|
||||||
|
async def async_handle_create_automation(call):
|
||||||
|
"""Handle the create_automation service call."""
|
||||||
|
try:
|
||||||
|
# Check if agents are available
|
||||||
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
||||||
|
_LOGGER.error(
|
||||||
|
"No AI agents available. Please configure the integration first."
|
||||||
|
)
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
|
||||||
|
provider = call.data.get("provider")
|
||||||
|
if provider not in hass.data[DOMAIN]["agents"]:
|
||||||
|
# Get the first available provider
|
||||||
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
||||||
|
if not available_providers:
|
||||||
|
_LOGGER.error("No AI agents available")
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
provider = available_providers[0]
|
||||||
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
||||||
|
|
||||||
|
agent = hass.data[DOMAIN]["agents"][provider]
|
||||||
|
result = await agent.create_automation(call.data.get("automation", {}))
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error creating automation: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
async def async_handle_save_prompt_history(call):
|
||||||
|
"""Handle the save_prompt_history service call."""
|
||||||
|
try:
|
||||||
|
# Check if agents are available
|
||||||
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
||||||
|
_LOGGER.error(
|
||||||
|
"No AI agents available. Please configure the integration first."
|
||||||
|
)
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
|
||||||
|
provider = call.data.get("provider")
|
||||||
|
if provider not in hass.data[DOMAIN]["agents"]:
|
||||||
|
# Get the first available provider
|
||||||
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
||||||
|
if not available_providers:
|
||||||
|
_LOGGER.error("No AI agents available")
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
provider = available_providers[0]
|
||||||
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
||||||
|
|
||||||
|
agent = hass.data[DOMAIN]["agents"][provider]
|
||||||
|
user_id = call.context.user_id if call.context.user_id else "default"
|
||||||
|
result = await agent.save_user_prompt_history(
|
||||||
|
user_id, call.data.get("history", [])
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error saving prompt history: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
async def async_handle_load_prompt_history(call):
|
||||||
|
"""Handle the load_prompt_history service call."""
|
||||||
|
try:
|
||||||
|
# Check if agents are available
|
||||||
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
||||||
|
_LOGGER.error(
|
||||||
|
"No AI agents available. Please configure the integration first."
|
||||||
|
)
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
|
||||||
|
provider = call.data.get("provider")
|
||||||
|
if provider not in hass.data[DOMAIN]["agents"]:
|
||||||
|
# Get the first available provider
|
||||||
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
||||||
|
if not available_providers:
|
||||||
|
_LOGGER.error("No AI agents available")
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
provider = available_providers[0]
|
||||||
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
||||||
|
|
||||||
|
agent = hass.data[DOMAIN]["agents"][provider]
|
||||||
|
user_id = call.context.user_id if call.context.user_id else "default"
|
||||||
|
result = await agent.load_user_prompt_history(user_id)
|
||||||
|
_LOGGER.debug("Load prompt history result: %s", result)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error loading prompt history: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
async def async_handle_create_dashboard(call):
|
||||||
|
"""Handle the create_dashboard service call."""
|
||||||
|
try:
|
||||||
|
# Check if agents are available
|
||||||
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
||||||
|
_LOGGER.error(
|
||||||
|
"No AI agents available. Please configure the integration first."
|
||||||
|
)
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
|
||||||
|
provider = call.data.get("provider")
|
||||||
|
if provider not in hass.data[DOMAIN]["agents"]:
|
||||||
|
# Get the first available provider
|
||||||
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
||||||
|
if not available_providers:
|
||||||
|
_LOGGER.error("No AI agents available")
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
provider = available_providers[0]
|
||||||
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
||||||
|
|
||||||
|
agent = hass.data[DOMAIN]["agents"][provider]
|
||||||
|
|
||||||
|
# Parse dashboard config if it's a string
|
||||||
|
dashboard_config = call.data.get("dashboard_config", {})
|
||||||
|
if isinstance(dashboard_config, str):
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
|
||||||
|
dashboard_config = json.loads(dashboard_config)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
_LOGGER.error(f"Invalid JSON in dashboard_config: {e}")
|
||||||
|
return {"error": f"Invalid JSON in dashboard_config: {e}"}
|
||||||
|
|
||||||
|
result = await agent.create_dashboard(dashboard_config)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error creating dashboard: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
async def async_handle_update_dashboard(call):
|
||||||
|
"""Handle the update_dashboard service call."""
|
||||||
|
try:
|
||||||
|
# Check if agents are available
|
||||||
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
||||||
|
_LOGGER.error(
|
||||||
|
"No AI agents available. Please configure the integration first."
|
||||||
|
)
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
|
||||||
|
provider = call.data.get("provider")
|
||||||
|
if provider not in hass.data[DOMAIN]["agents"]:
|
||||||
|
# Get the first available provider
|
||||||
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
||||||
|
if not available_providers:
|
||||||
|
_LOGGER.error("No AI agents available")
|
||||||
|
return {"error": "No AI agents configured"}
|
||||||
|
provider = available_providers[0]
|
||||||
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
||||||
|
|
||||||
|
agent = hass.data[DOMAIN]["agents"][provider]
|
||||||
|
|
||||||
|
# Parse dashboard config if it's a string
|
||||||
|
dashboard_config = call.data.get("dashboard_config", {})
|
||||||
|
if isinstance(dashboard_config, str):
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
|
||||||
|
dashboard_config = json.loads(dashboard_config)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
_LOGGER.error(f"Invalid JSON in dashboard_config: {e}")
|
||||||
|
return {"error": f"Invalid JSON in dashboard_config: {e}"}
|
||||||
|
|
||||||
|
dashboard_url = call.data.get("dashboard_url", "")
|
||||||
|
if not dashboard_url:
|
||||||
|
return {"error": "Dashboard URL is required"}
|
||||||
|
|
||||||
|
result = await agent.update_dashboard(dashboard_url, dashboard_config)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error updating dashboard: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
# Register services
|
||||||
|
hass.services.async_register(DOMAIN, "query", async_handle_query)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, "create_automation", async_handle_create_automation
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, "save_prompt_history", async_handle_save_prompt_history
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, "load_prompt_history", async_handle_load_prompt_history
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, "create_dashboard", async_handle_create_dashboard
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, "update_dashboard", async_handle_update_dashboard
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register static path for frontend
|
||||||
|
await hass.http.async_register_static_paths(
|
||||||
|
[
|
||||||
|
StaticPathConfig(
|
||||||
|
"/frontend/ai_agent_ha",
|
||||||
|
hass.config.path("custom_components/ai_agent_ha/frontend"),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Panel registration with proper error handling
|
||||||
|
panel_name = "ai_agent_ha"
|
||||||
|
try:
|
||||||
|
if await _panel_exists(hass, panel_name):
|
||||||
|
_LOGGER.debug("AI Agent HA panel already exists, skipping registration")
|
||||||
|
return True
|
||||||
|
|
||||||
|
_LOGGER.debug("Registering AI Agent HA panel")
|
||||||
|
async_register_built_in_panel(
|
||||||
|
hass,
|
||||||
|
component_name="custom",
|
||||||
|
sidebar_title="AI Agent HA",
|
||||||
|
sidebar_icon="mdi:robot",
|
||||||
|
frontend_url_path=panel_name,
|
||||||
|
require_admin=False,
|
||||||
|
config={
|
||||||
|
"_panel_custom": {
|
||||||
|
"name": "ai_agent_ha-panel",
|
||||||
|
"module_url": "/frontend/ai_agent_ha/ai_agent_ha-panel.js",
|
||||||
|
"embed_iframe": False,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_LOGGER.debug("AI Agent HA panel registered successfully")
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.warning("Panel registration error: %s", str(e))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if await _panel_exists(hass, "ai_agent_ha"):
|
||||||
|
try:
|
||||||
|
from homeassistant.components.frontend import async_remove_panel
|
||||||
|
|
||||||
|
async_remove_panel(hass, "ai_agent_ha")
|
||||||
|
_LOGGER.debug("AI Agent HA panel removed successfully")
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.debug("Error removing panel: %s", str(e))
|
||||||
|
|
||||||
|
# Remove services
|
||||||
|
hass.services.async_remove(DOMAIN, "query")
|
||||||
|
hass.services.async_remove(DOMAIN, "create_automation")
|
||||||
|
hass.services.async_remove(DOMAIN, "save_prompt_history")
|
||||||
|
hass.services.async_remove(DOMAIN, "load_prompt_history")
|
||||||
|
hass.services.async_remove(DOMAIN, "create_dashboard")
|
||||||
|
hass.services.async_remove(DOMAIN, "update_dashboard")
|
||||||
|
# Remove data
|
||||||
|
if DOMAIN in hass.data:
|
||||||
|
hass.data.pop(DOMAIN)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _panel_exists(hass: HomeAssistant, panel_name: str) -> bool:
|
||||||
|
"""Check if a panel already exists."""
|
||||||
|
try:
|
||||||
|
return hasattr(hass.data, "frontend_panels") and panel_name in hass.data.get(
|
||||||
|
"frontend_panels", {}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.debug("Error checking panel existence: %s", str(e))
|
||||||
|
return False
|
||||||
@@ -0,0 +1,917 @@
|
|||||||
|
"""Config flow for AI Agent HA integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
TextSelector,
|
||||||
|
TextSelectorConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .agent import (
|
||||||
|
fetch_gemini_models,
|
||||||
|
fetch_openai_compatible_models,
|
||||||
|
fetch_openai_models,
|
||||||
|
)
|
||||||
|
from .const import (
|
||||||
|
CONF_LOCAL_OLLAMA_URL,
|
||||||
|
CONF_OPENAI_BASE_URL,
|
||||||
|
CONF_OPENAI_COMPATIBLE_URL,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PROVIDERS = {
|
||||||
|
"llama": "Llama",
|
||||||
|
"openai": "OpenAI",
|
||||||
|
"gemini": "Google Gemini",
|
||||||
|
"openrouter": "OpenRouter",
|
||||||
|
"anthropic": "Anthropic (Claude)",
|
||||||
|
"alter": "Alter",
|
||||||
|
"zai": "z.ai",
|
||||||
|
"local_ollama": "Local Ollama",
|
||||||
|
"openai_compatible": "Local OpenAI-Compatible (e.g. LM Studio, vLLM)",
|
||||||
|
}
|
||||||
|
|
||||||
|
TOKEN_FIELD_NAMES = {
|
||||||
|
"llama": "llama_token",
|
||||||
|
"openai": "openai_token",
|
||||||
|
"gemini": "gemini_token",
|
||||||
|
"openrouter": "openrouter_token",
|
||||||
|
"anthropic": "anthropic_token",
|
||||||
|
"alter": "alter_token",
|
||||||
|
"zai": "zai_token",
|
||||||
|
"zai_endpoint": "zai_endpoint",
|
||||||
|
"local_ollama": CONF_LOCAL_OLLAMA_URL, # For local Ollama models, we use URL instead of token
|
||||||
|
"openai_compatible": CONF_OPENAI_COMPATIBLE_URL, # For OpenAI-compatible endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
TOKEN_LABELS = {
|
||||||
|
"llama": "Llama API Token",
|
||||||
|
"openai": "OpenAI API Key",
|
||||||
|
"gemini": "Google Gemini API Key",
|
||||||
|
"openrouter": "OpenRouter API Key",
|
||||||
|
"anthropic": "Anthropic API Key",
|
||||||
|
"alter": "Alter API Key",
|
||||||
|
"zai": "z.ai API Key",
|
||||||
|
"zai_endpoint": "z.ai API Endpoint Type",
|
||||||
|
"local_ollama": "Local Ollama API URL (e.g., http://localhost:11434/api/generate)",
|
||||||
|
"openai_compatible": "OpenAI-Compatible URL (e.g., http://example.com/v1/ or http://localhost:8080/v1/). Must end with /v1/",
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_MODELS = {
|
||||||
|
"llama": "Llama-4-Maverick-17B-128E-Instruct-FP8",
|
||||||
|
"openai": "gpt-5",
|
||||||
|
"gemini": "gemini-2.5-flash",
|
||||||
|
"openrouter": "openai/gpt-4o",
|
||||||
|
"anthropic": "claude-sonnet-4-5-20250929",
|
||||||
|
"alter": "", # User enters custom model
|
||||||
|
"zai": "glm-4.7", # Z.ai's latest flagship model
|
||||||
|
"local_ollama": "llama3.2", # Updated to use llama3.2 as default for local Ollama
|
||||||
|
"openai_compatible": "", # User enters custom model for OpenAI-compatible endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_MODELS = {
|
||||||
|
"openai": [
|
||||||
|
"gpt-5",
|
||||||
|
"gpt-5-mini",
|
||||||
|
"gpt-5-nano",
|
||||||
|
"gpt-4.1",
|
||||||
|
"gpt-4.1-mini",
|
||||||
|
"gpt-4.1-nano",
|
||||||
|
"gpt-4o",
|
||||||
|
"gpt-4o-mini",
|
||||||
|
"o3",
|
||||||
|
"o3-mini",
|
||||||
|
"o4-mini",
|
||||||
|
"o1",
|
||||||
|
"o1-preview",
|
||||||
|
"o1-mini",
|
||||||
|
"gpt-4-turbo",
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-3.5-turbo",
|
||||||
|
],
|
||||||
|
"gemini": [
|
||||||
|
"gemini-2.5-flash",
|
||||||
|
"gemini-2.5-pro",
|
||||||
|
"gemini-2.0-flash",
|
||||||
|
"gemini-2.0-flash-exp",
|
||||||
|
"gemini-2.5-flash-preview",
|
||||||
|
"gemini-2.5-pro-preview",
|
||||||
|
],
|
||||||
|
"openrouter": [
|
||||||
|
"openai/gpt-4o",
|
||||||
|
"openai/gpt-4-turbo",
|
||||||
|
"openai/gpt-3.5-turbo",
|
||||||
|
"anthropic/claude-3.5-sonnet",
|
||||||
|
"anthropic/claude-3-sonnet",
|
||||||
|
"anthropic/claude-3-haiku",
|
||||||
|
"meta-llama/llama-3.1-70b-instruct",
|
||||||
|
"meta-llama/llama-3.2-90b-instruct",
|
||||||
|
"google/gemini-pro",
|
||||||
|
"mistralai/mixtral-8x7b-instruct",
|
||||||
|
"deepseek/deepseek-r1",
|
||||||
|
],
|
||||||
|
"anthropic": [
|
||||||
|
"claude-opus-4-7",
|
||||||
|
"claude-sonnet-4-6",
|
||||||
|
"claude-haiku-4-5",
|
||||||
|
"claude-sonnet-4-5-20250929",
|
||||||
|
"claude-sonnet-4-20250514",
|
||||||
|
"claude-3-5-sonnet-20241022",
|
||||||
|
"claude-3-5-haiku-20241022",
|
||||||
|
"claude-3-opus-20240229",
|
||||||
|
"claude-3-sonnet-20240229",
|
||||||
|
"claude-3-haiku-20240307",
|
||||||
|
],
|
||||||
|
"llama": [
|
||||||
|
"Llama-4-Maverick-17B-128E-Instruct-FP8",
|
||||||
|
"Llama-3.1-70B-Instruct",
|
||||||
|
"Llama-3.1-8B-Instruct",
|
||||||
|
"Llama-3.2-90B-Instruct",
|
||||||
|
],
|
||||||
|
# Alter - user enters custom model name only
|
||||||
|
"alter": [
|
||||||
|
"Custom...",
|
||||||
|
],
|
||||||
|
# z.ai - available models
|
||||||
|
"zai": [
|
||||||
|
"glm-4.7",
|
||||||
|
"glm-4.6",
|
||||||
|
"glm-4.5",
|
||||||
|
"glm-4.5-air",
|
||||||
|
"glm-4.5-x",
|
||||||
|
"glm-4.5-airx",
|
||||||
|
"glm-4.5-flash",
|
||||||
|
"glm-4-32b-0414-128k",
|
||||||
|
"Custom...",
|
||||||
|
],
|
||||||
|
# For local Ollama models, provide common models with llama3.2 as the default
|
||||||
|
"local_ollama": [
|
||||||
|
"llama3.2",
|
||||||
|
"llama3",
|
||||||
|
"llama3.1",
|
||||||
|
"mistral",
|
||||||
|
"mixtral",
|
||||||
|
"deepseek-coder",
|
||||||
|
"Custom...",
|
||||||
|
],
|
||||||
|
# For OpenAI-compatible endpoints, user should specify their model
|
||||||
|
"openai_compatible": [
|
||||||
|
"Custom...",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_PROVIDER = "openai"
|
||||||
|
|
||||||
|
|
||||||
|
class AiAgentHaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # type: ignore[call-arg,misc]
|
||||||
|
"""Handle a config flow for AI Agent HA."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
try:
|
||||||
|
return AiAgentHaOptionsFlowHandler()
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("Error creating options flow: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Check if this provider is already configured
|
||||||
|
await self.async_set_unique_id(f"ai_agent_ha_{user_input['ai_provider']}")
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
self.config_data = {"ai_provider": user_input["ai_provider"]}
|
||||||
|
return await self.async_step_configure()
|
||||||
|
|
||||||
|
# Show provider selection form
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("ai_provider"): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[
|
||||||
|
{"value": k, "label": v} for k, v in PROVIDERS.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_configure(self, user_input=None):
|
||||||
|
"""Handle the configuration step for the selected provider."""
|
||||||
|
errors = {}
|
||||||
|
provider = self.config_data["ai_provider"]
|
||||||
|
token_field = TOKEN_FIELD_NAMES[provider]
|
||||||
|
token_label = TOKEN_LABELS[provider]
|
||||||
|
default_model = DEFAULT_MODELS[provider]
|
||||||
|
# For Alter provider, default to "Custom..." for the dropdown since model is user-provided
|
||||||
|
dropdown_default = "Custom..." if provider == "alter" else default_model
|
||||||
|
available_models = AVAILABLE_MODELS.get(provider, [default_model])
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
# Validate the token
|
||||||
|
token_value = user_input.get(token_field)
|
||||||
|
if not token_value:
|
||||||
|
errors[token_field] = "required"
|
||||||
|
raise InvalidApiKey
|
||||||
|
|
||||||
|
# Store the configuration data
|
||||||
|
self.config_data[token_field] = token_value
|
||||||
|
|
||||||
|
# For z.ai, store endpoint type
|
||||||
|
if provider == "zai":
|
||||||
|
endpoint_type = user_input.get("zai_endpoint", "general")
|
||||||
|
self.config_data["zai_endpoint"] = endpoint_type
|
||||||
|
|
||||||
|
# For OpenAI, store Base URL (defaults to official endpoint if unchanged)
|
||||||
|
if provider == "openai":
|
||||||
|
base_url = (user_input.get(CONF_OPENAI_BASE_URL) or "").strip()
|
||||||
|
self.config_data[CONF_OPENAI_BASE_URL] = (
|
||||||
|
base_url or "https://api.openai.com/v1"
|
||||||
|
)
|
||||||
|
# For OpenAI, move to next step to select model from dynamic list
|
||||||
|
return await self.async_step_configure_openai_models()
|
||||||
|
|
||||||
|
# For OpenAI-Compatible, store Base URL + optional API key, then move on
|
||||||
|
if provider == "openai_compatible":
|
||||||
|
base_url = (
|
||||||
|
user_input.get(CONF_OPENAI_COMPATIBLE_URL) or ""
|
||||||
|
).strip()
|
||||||
|
self.config_data[CONF_OPENAI_COMPATIBLE_URL] = base_url
|
||||||
|
api_key = (
|
||||||
|
user_input.get("openai_compatible_api_key") or ""
|
||||||
|
).strip()
|
||||||
|
self.config_data["openai_compatible_api_key"] = api_key
|
||||||
|
# Move to next step to select model from dynamic list
|
||||||
|
return await self.async_step_configure_openai_compatible_models()
|
||||||
|
|
||||||
|
# Add model configuration if provided
|
||||||
|
selected_model = user_input.get("model")
|
||||||
|
custom_model = user_input.get("custom_model")
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
f"Config flow - Provider: {provider}, Selected model: {selected_model}, Custom model: {custom_model}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize models dict if it doesn't exist
|
||||||
|
if "models" not in self.config_data:
|
||||||
|
self.config_data["models"] = {}
|
||||||
|
|
||||||
|
if custom_model and custom_model.strip():
|
||||||
|
# Use custom model if provided and not empty
|
||||||
|
self.config_data["models"][provider] = custom_model.strip()
|
||||||
|
elif selected_model and selected_model != "Custom...":
|
||||||
|
# Use selected model if it's not the "Custom..." option
|
||||||
|
self.config_data["models"][provider] = selected_model
|
||||||
|
else:
|
||||||
|
# For local_ollama, openai_compatible, alter, and zai providers, allow empty model name
|
||||||
|
if provider in (
|
||||||
|
"local_ollama",
|
||||||
|
"openai_compatible",
|
||||||
|
"alter",
|
||||||
|
"zai",
|
||||||
|
):
|
||||||
|
self.config_data["models"][provider] = ""
|
||||||
|
else:
|
||||||
|
# Fallback to default model for other providers
|
||||||
|
self.config_data["models"][provider] = default_model
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"AI Agent HA ({PROVIDERS[provider]})",
|
||||||
|
data=self.config_data,
|
||||||
|
)
|
||||||
|
except InvalidApiKey:
|
||||||
|
errors["base"] = "invalid_api_key"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
if provider == "zai":
|
||||||
|
# For z.ai provider, we need token, endpoint type, and optional model name
|
||||||
|
model_options = AVAILABLE_MODELS.get("zai", ["Custom..."])
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(token_field): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
vol.Optional("zai_endpoint", default="general"): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[
|
||||||
|
{"value": "general", "label": "General Purpose"},
|
||||||
|
{"value": "coding", "label": "Coding (3× usage, 1/7 cost)"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Optional("model", default="glm-4.7"): SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_options)
|
||||||
|
),
|
||||||
|
vol.Optional("custom_model"): TextSelector(
|
||||||
|
TextSelectorConfig(type="text")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": token_label,
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider == "local_ollama":
|
||||||
|
# For local_ollama provider, we need both URL and optional model name
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(CONF_LOCAL_OLLAMA_URL): TextSelector(
|
||||||
|
TextSelectorConfig(type="text")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add model selection
|
||||||
|
model_options = AVAILABLE_MODELS.get("local_ollama", ["Custom..."])
|
||||||
|
schema_dict[vol.Optional("model", default="Custom...")] = SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_options)
|
||||||
|
)
|
||||||
|
schema_dict[vol.Optional("custom_model")] = TextSelector(
|
||||||
|
TextSelectorConfig(type="text")
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": "Local Ollama API URL", # nosec B105 - UI label string shown next to the URL field, not a credential
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider == "openai_compatible":
|
||||||
|
# For openai_compatible provider, we need base URL + optional API key.
|
||||||
|
# Many local endpoints (LM Studio, vLLM) need no key; gateways like
|
||||||
|
# Open WebUI or LiteLLM require one. Models are fetched in the next step.
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(CONF_OPENAI_COMPATIBLE_URL): TextSelector(
|
||||||
|
TextSelectorConfig(type="text")
|
||||||
|
),
|
||||||
|
vol.Optional("openai_compatible_api_key", default=""): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": "Local OpenAI-Compatible URL", # nosec B105 - UI label for config form, not a credential
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider == "openai":
|
||||||
|
# For OpenAI provider, first step: API Key + Base URL
|
||||||
|
# Model selection happens in the next step after we fetch available models
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(token_field): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_OPENAI_BASE_URL,
|
||||||
|
default="https://api.openai.com/v1",
|
||||||
|
): TextSelector(TextSelectorConfig(type="text")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": token_label,
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build schema for other providers
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(token_field): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add model selection if available
|
||||||
|
if available_models:
|
||||||
|
# For Gemini, fetch models dynamically
|
||||||
|
if provider == "gemini":
|
||||||
|
token_value = self.config_data.get("gemini_token")
|
||||||
|
model_list = await fetch_gemini_models(token_value)
|
||||||
|
if "Custom..." not in model_list:
|
||||||
|
model_list.insert(0, "Custom...")
|
||||||
|
model_options = model_list
|
||||||
|
else:
|
||||||
|
# Add predefined models + custom option (avoid duplicating "Custom...")
|
||||||
|
if "Custom..." in available_models:
|
||||||
|
model_options = available_models
|
||||||
|
else:
|
||||||
|
model_options = available_models + ["Custom..."]
|
||||||
|
|
||||||
|
schema_dict[vol.Optional("model", default=dropdown_default)] = (
|
||||||
|
SelectSelector(SelectSelectorConfig(options=model_options))
|
||||||
|
)
|
||||||
|
schema_dict[vol.Optional("custom_model")] = TextSelector(
|
||||||
|
TextSelectorConfig(type="text")
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": token_label,
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_configure_openai_models(self, user_input=None):
|
||||||
|
"""Handle the OpenAI model selection step with dynamic model list."""
|
||||||
|
errors = {}
|
||||||
|
provider = "openai"
|
||||||
|
token = self.config_data.get("openai_token")
|
||||||
|
base_url = self.config_data.get(
|
||||||
|
CONF_OPENAI_BASE_URL, "https://api.openai.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch available models dynamically
|
||||||
|
model_list = await fetch_openai_models(base_url, token)
|
||||||
|
|
||||||
|
# Ensure "Custom..." is always available
|
||||||
|
if "Custom..." not in model_list:
|
||||||
|
model_list.insert(0, "Custom...")
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
selected_model = user_input.get("model")
|
||||||
|
custom_model = user_input.get("custom_model")
|
||||||
|
|
||||||
|
# Initialize models dict if it doesn't exist
|
||||||
|
if "models" not in self.config_data:
|
||||||
|
self.config_data["models"] = {}
|
||||||
|
|
||||||
|
if custom_model and custom_model.strip():
|
||||||
|
self.config_data["models"][provider] = custom_model.strip()
|
||||||
|
elif selected_model and selected_model != "Custom...":
|
||||||
|
self.config_data["models"][provider] = selected_model
|
||||||
|
else:
|
||||||
|
self.config_data["models"][provider] = ""
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"AI Agent HA ({PROVIDERS[provider]})",
|
||||||
|
data=self.config_data,
|
||||||
|
)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception in OpenAI model selection")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
schema_dict = {
|
||||||
|
vol.Optional("model", default="Custom..."): SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_list)
|
||||||
|
),
|
||||||
|
vol.Optional("custom_model"): TextSelector(TextSelectorConfig(type="text")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_openai_models",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_configure_openai_compatible_models(self, user_input=None):
|
||||||
|
"""Handle the OpenAI-Compatible model selection step with dynamic model list."""
|
||||||
|
errors = {}
|
||||||
|
provider = "openai_compatible"
|
||||||
|
base_url = self.config_data.get(CONF_OPENAI_COMPATIBLE_URL, "")
|
||||||
|
api_key = self.config_data.get("openai_compatible_api_key") or ""
|
||||||
|
|
||||||
|
# Fetch available models dynamically if the endpoint supports it
|
||||||
|
model_list = await fetch_openai_compatible_models(base_url, api_key or None)
|
||||||
|
|
||||||
|
# Ensure "Custom..." is always available
|
||||||
|
if "Custom..." not in model_list:
|
||||||
|
model_list.insert(0, "Custom...")
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
selected_model = user_input.get("model")
|
||||||
|
custom_model = user_input.get("custom_model")
|
||||||
|
|
||||||
|
# Initialize models dict if it doesn't exist
|
||||||
|
if "models" not in self.config_data:
|
||||||
|
self.config_data["models"] = {}
|
||||||
|
|
||||||
|
if custom_model and custom_model.strip():
|
||||||
|
self.config_data["models"][provider] = custom_model.strip()
|
||||||
|
elif selected_model and selected_model != "Custom...":
|
||||||
|
self.config_data["models"][provider] = selected_model
|
||||||
|
else:
|
||||||
|
self.config_data["models"][provider] = ""
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"AI Agent HA ({PROVIDERS[provider]})",
|
||||||
|
data=self.config_data,
|
||||||
|
)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Unexpected exception in OpenAI-Compatible model selection"
|
||||||
|
)
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
schema_dict = {
|
||||||
|
vol.Optional("model", default="Custom..."): SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_list)
|
||||||
|
),
|
||||||
|
vol.Optional("custom_model"): TextSelector(TextSelectorConfig(type="text")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_openai_compatible_models",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidApiKey(HomeAssistantError):
|
||||||
|
"""Error to indicate there is an invalid API key."""
|
||||||
|
|
||||||
|
|
||||||
|
class AiAgentHaOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle options flow for AI Agent HA."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.options_data = {}
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle the initial options step - provider selection."""
|
||||||
|
current_provider = self.config_entry.data.get("ai_provider", DEFAULT_PROVIDER)
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Store selected provider and move to configure step
|
||||||
|
self.options_data = {
|
||||||
|
"ai_provider": user_input["ai_provider"],
|
||||||
|
"current_provider": current_provider,
|
||||||
|
}
|
||||||
|
return await self.async_step_configure_options()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
"ai_provider", default=current_provider
|
||||||
|
): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[
|
||||||
|
{"value": k, "label": v} for k, v in PROVIDERS.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description_placeholders={"current_provider": PROVIDERS[current_provider]},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_configure_options(self, user_input=None):
|
||||||
|
"""Handle the configuration step for the selected provider in options."""
|
||||||
|
errors = {}
|
||||||
|
provider = self.options_data["ai_provider"]
|
||||||
|
current_provider = self.options_data["current_provider"]
|
||||||
|
token_field = TOKEN_FIELD_NAMES[provider]
|
||||||
|
token_label = TOKEN_LABELS[provider]
|
||||||
|
|
||||||
|
# Get current configuration
|
||||||
|
current_models = self.config_entry.data.get("models", {})
|
||||||
|
current_model = current_models.get(provider, DEFAULT_MODELS[provider])
|
||||||
|
# For Alter provider, if model is empty, default to "Custom..." for the dropdown
|
||||||
|
if provider == "alter" and not current_model:
|
||||||
|
current_model = "Custom..."
|
||||||
|
current_token = self.config_entry.data.get(token_field, "")
|
||||||
|
available_models = AVAILABLE_MODELS.get(provider, [DEFAULT_MODELS[provider]])
|
||||||
|
|
||||||
|
# Use current token if provider hasn't changed, otherwise empty
|
||||||
|
display_token = current_token if provider == current_provider else ""
|
||||||
|
|
||||||
|
# Determine if current model is a custom model (not in available models list)
|
||||||
|
# and prepare model dropdown and custom model field defaults
|
||||||
|
model_options = available_models
|
||||||
|
if "Custom..." not in model_options:
|
||||||
|
model_options = model_options + ["Custom..."]
|
||||||
|
|
||||||
|
# Check if current_model is a custom model (not in the available models)
|
||||||
|
# Remove "Custom..." from the check since it's the selector option, not a real model
|
||||||
|
available_models_without_custom = [
|
||||||
|
m for m in available_models if m != "Custom..."
|
||||||
|
]
|
||||||
|
is_custom_model = (
|
||||||
|
current_model
|
||||||
|
and current_model not in available_models_without_custom
|
||||||
|
and current_model != "Custom..."
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_custom_model:
|
||||||
|
# Current model is a custom model - show "Custom..." in dropdown and populate custom field
|
||||||
|
model_default = "Custom..."
|
||||||
|
custom_model_default = current_model
|
||||||
|
else:
|
||||||
|
# Current model is a standard model or empty
|
||||||
|
model_default = current_model if current_model else "Custom..."
|
||||||
|
custom_model_default = ""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
token_value = user_input.get(token_field)
|
||||||
|
if not token_value:
|
||||||
|
errors[token_field] = "required"
|
||||||
|
else:
|
||||||
|
# Prepare the updated configuration
|
||||||
|
updated_data = dict(self.config_entry.data)
|
||||||
|
updated_data["ai_provider"] = provider
|
||||||
|
updated_data[token_field] = token_value
|
||||||
|
|
||||||
|
# Update model configuration
|
||||||
|
selected_model = user_input.get("model")
|
||||||
|
custom_model = user_input.get("custom_model")
|
||||||
|
|
||||||
|
# For zai, update endpoint type
|
||||||
|
if provider == "zai":
|
||||||
|
endpoint_type = user_input.get("zai_endpoint", "general")
|
||||||
|
updated_data["zai_endpoint"] = endpoint_type
|
||||||
|
|
||||||
|
# For OpenAI, update Base URL (default to official if blank)
|
||||||
|
if provider == "openai":
|
||||||
|
base_url = (user_input.get(CONF_OPENAI_BASE_URL) or "").strip()
|
||||||
|
updated_data[CONF_OPENAI_BASE_URL] = (
|
||||||
|
base_url or "https://api.openai.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# For OpenAI-Compatible, update the optional API key
|
||||||
|
if provider == "openai_compatible":
|
||||||
|
updated_data["openai_compatible_api_key"] = (
|
||||||
|
user_input.get("openai_compatible_api_key") or ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# Initialize models dict if it doesn't exist
|
||||||
|
if "models" not in updated_data:
|
||||||
|
updated_data["models"] = {}
|
||||||
|
|
||||||
|
if custom_model and custom_model.strip():
|
||||||
|
# Use custom model if provided and not empty
|
||||||
|
updated_data["models"][provider] = custom_model.strip()
|
||||||
|
elif selected_model and selected_model != "Custom...":
|
||||||
|
# Use selected model if it's not the "Custom..." option
|
||||||
|
updated_data["models"][provider] = selected_model
|
||||||
|
else:
|
||||||
|
# For local_ollama, openai_compatible, alter, and zai providers, allow empty model name
|
||||||
|
if provider in (
|
||||||
|
"local_ollama",
|
||||||
|
"openai_compatible",
|
||||||
|
"alter",
|
||||||
|
"zai",
|
||||||
|
):
|
||||||
|
updated_data["models"][provider] = ""
|
||||||
|
else:
|
||||||
|
# Ensure we keep the current model or use default for other providers
|
||||||
|
if provider not in updated_data["models"]:
|
||||||
|
updated_data["models"][provider] = DEFAULT_MODELS[
|
||||||
|
provider
|
||||||
|
]
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
f"Options flow - Final model config for {provider}: {updated_data['models'].get(provider)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the config entry
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self.config_entry, data=updated_data
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(title="", data={})
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception in options flow")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
# Build schema for the selected provider in options
|
||||||
|
if provider == "zai":
|
||||||
|
current_endpoint = self.config_entry.data.get("zai_endpoint", "general")
|
||||||
|
model_options = AVAILABLE_MODELS.get("zai", ["glm-4.7"])
|
||||||
|
# Ensure "Custom..." is in model options
|
||||||
|
if "Custom..." not in model_options:
|
||||||
|
model_options = model_options + ["Custom..."]
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(token_field, default=display_token): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
vol.Optional("zai_endpoint", default=current_endpoint): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[
|
||||||
|
{"value": "general", "label": "General Purpose"},
|
||||||
|
{"value": "coding", "label": "Coding (3× usage, 1/7 cost)"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Optional("model", default=model_default): SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_options)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
"custom_model", default=custom_model_default
|
||||||
|
): TextSelector(TextSelectorConfig(type="text")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_options",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": token_label,
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider == "local_ollama":
|
||||||
|
# For local_ollama provider, we need both URL and optional model name
|
||||||
|
current_url = self.config_entry.data.get(CONF_LOCAL_OLLAMA_URL, "")
|
||||||
|
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(CONF_LOCAL_OLLAMA_URL, default=current_url): TextSelector(
|
||||||
|
TextSelectorConfig(type="text")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add model selection
|
||||||
|
model_options = AVAILABLE_MODELS.get("local_ollama", ["Custom..."])
|
||||||
|
# Ensure "Custom..." is in model options
|
||||||
|
if "Custom..." not in model_options:
|
||||||
|
model_options = model_options + ["Custom..."]
|
||||||
|
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_options)
|
||||||
|
)
|
||||||
|
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
|
||||||
|
TextSelector(TextSelectorConfig(type="text"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_options",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": "Local Ollama API URL", # nosec B105 - UI label string shown next to the URL field, not a credential
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider == "openai_compatible":
|
||||||
|
# For openai_compatible provider, we need URL + optional API key + model
|
||||||
|
current_url = self.config_entry.data.get(CONF_OPENAI_COMPATIBLE_URL, "")
|
||||||
|
current_api_key = (
|
||||||
|
self.config_entry.data.get("openai_compatible_api_key") or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch available models dynamically if the endpoint supports it
|
||||||
|
model_list = await fetch_openai_compatible_models(
|
||||||
|
current_url, current_api_key or None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure "Custom..." is always available
|
||||||
|
if "Custom..." not in model_list:
|
||||||
|
model_list.insert(0, "Custom...")
|
||||||
|
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(
|
||||||
|
CONF_OPENAI_COMPATIBLE_URL, default=current_url
|
||||||
|
): TextSelector(TextSelectorConfig(type="text")),
|
||||||
|
vol.Optional(
|
||||||
|
"openai_compatible_api_key", default=current_api_key
|
||||||
|
): TextSelector(TextSelectorConfig(type="password")),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add model selection with dynamic list
|
||||||
|
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_list)
|
||||||
|
)
|
||||||
|
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
|
||||||
|
TextSelector(TextSelectorConfig(type="text"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_options",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": "Local OpenAI-Compatible URL", # nosec B105 - UI label for config form, not a credential
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider == "openai":
|
||||||
|
# For OpenAI provider, we need token and optional Base URL
|
||||||
|
# Pre-fill with official endpoint if not set
|
||||||
|
current_base_url = (
|
||||||
|
self.config_entry.data.get(CONF_OPENAI_BASE_URL)
|
||||||
|
or "https://api.openai.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch available models dynamically
|
||||||
|
current_token = self.config_entry.data.get("openai_token", "")
|
||||||
|
model_list = await fetch_openai_models(current_base_url, current_token)
|
||||||
|
|
||||||
|
# Ensure "Custom..." is always available
|
||||||
|
if "Custom..." not in model_list:
|
||||||
|
model_list.insert(0, "Custom...")
|
||||||
|
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(token_field, default=display_token): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_OPENAI_BASE_URL, default=current_base_url
|
||||||
|
): TextSelector(TextSelectorConfig(type="text")),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add model selection with dynamic list
|
||||||
|
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_list)
|
||||||
|
)
|
||||||
|
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
|
||||||
|
TextSelector(TextSelectorConfig(type="text"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_options",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": token_label,
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build schema for other providers
|
||||||
|
schema_dict = {
|
||||||
|
vol.Required(token_field, default=display_token): TextSelector(
|
||||||
|
TextSelectorConfig(type="password")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add model selection if available
|
||||||
|
if available_models:
|
||||||
|
# For Gemini, fetch models dynamically
|
||||||
|
if provider == "gemini":
|
||||||
|
current_token = self.config_entry.data.get("gemini_token", "")
|
||||||
|
model_list = await fetch_gemini_models(current_token)
|
||||||
|
if "Custom..." not in model_list:
|
||||||
|
model_list.insert(0, "Custom...")
|
||||||
|
model_options = model_list
|
||||||
|
# model_options already has "Custom..." added above for other providers
|
||||||
|
|
||||||
|
schema_dict[vol.Optional("model", default=model_default)] = SelectSelector(
|
||||||
|
SelectSelectorConfig(options=model_options)
|
||||||
|
)
|
||||||
|
schema_dict[vol.Optional("custom_model", default=custom_model_default)] = (
|
||||||
|
TextSelector(TextSelectorConfig(type="text"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="configure_options",
|
||||||
|
data_schema=vol.Schema(schema_dict),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"token_label": token_label,
|
||||||
|
"provider": PROVIDERS[provider],
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Constants for the AI Agent HA integration."""
|
||||||
|
|
||||||
|
DOMAIN = "ai_agent_ha"
|
||||||
|
CONF_API_KEY = "api_key"
|
||||||
|
CONF_WEATHER_ENTITY = "weather_entity"
|
||||||
|
|
||||||
|
# AI Provider configuration keys
|
||||||
|
CONF_LLAMA_TOKEN = "llama_token" # nosec B105
|
||||||
|
CONF_OPENAI_TOKEN = "openai_token" # nosec B105
|
||||||
|
CONF_OPENAI_BASE_URL = (
|
||||||
|
"openai_base_url" # nosec B105 - configuration key, not a credential
|
||||||
|
)
|
||||||
|
CONF_GEMINI_TOKEN = "gemini_token" # nosec B105
|
||||||
|
CONF_OPENROUTER_TOKEN = "openrouter_token" # nosec B105
|
||||||
|
CONF_ANTHROPIC_TOKEN = "anthropic_token" # nosec B105
|
||||||
|
CONF_ALTER_TOKEN = "alter_token" # nosec B105
|
||||||
|
CONF_ZAI_TOKEN = "zai_token" # nosec B105
|
||||||
|
CONF_LOCAL_OLLAMA_URL = "local_ollama_url"
|
||||||
|
CONF_LOCAL_OLLAMA_MODEL = "local_ollama_model"
|
||||||
|
CONF_OPENAI_COMPATIBLE_URL = "openai_compatible_url"
|
||||||
|
CONF_LOCAL_URL = "local_url" # legacy alias for local_ollama_url
|
||||||
|
|
||||||
|
# Available AI providers
|
||||||
|
AI_PROVIDERS = [
|
||||||
|
"llama",
|
||||||
|
"openai",
|
||||||
|
"gemini",
|
||||||
|
"openrouter",
|
||||||
|
"anthropic",
|
||||||
|
"alter",
|
||||||
|
"zai",
|
||||||
|
"local_ollama",
|
||||||
|
"openai_compatible",
|
||||||
|
]
|
||||||
|
|
||||||
|
# AI Provider constants
|
||||||
|
CONF_MODELS = "models"
|
||||||
|
|
||||||
|
# Supported AI providers
|
||||||
|
DEFAULT_AI_PROVIDER = "openai"
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
"""
|
||||||
|
Dashboard templates and examples for AI agent to use when creating dashboards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Basic dashboard templates for different use cases
|
||||||
|
DASHBOARD_TEMPLATES = {
|
||||||
|
"simple_lights": {
|
||||||
|
"title": "Lights Dashboard",
|
||||||
|
"url_path": "lights",
|
||||||
|
"icon": "mdi:lightbulb",
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"title": "All Lights",
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Living Room Lights",
|
||||||
|
"entities": [], # To be filled with actual light entities
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light",
|
||||||
|
"entity": "", # To be filled with main light entity
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "Security Dashboard",
|
||||||
|
"url_path": "security",
|
||||||
|
"icon": "mdi:security",
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"title": "Security Overview",
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Sensors",
|
||||||
|
"entities": [], # To be filled with sensor entities
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Cameras",
|
||||||
|
"entities": [], # To be filled with camera entities
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "alarm-panel",
|
||||||
|
"entity": "", # To be filled with alarm panel entity
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"climate": {
|
||||||
|
"title": "Climate Control",
|
||||||
|
"url_path": "climate",
|
||||||
|
"icon": "mdi:thermometer",
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"title": "Temperature & Humidity",
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "thermostat",
|
||||||
|
"entity": "", # To be filled with climate.* thermostat entity (optional)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "history-graph",
|
||||||
|
"title": "Temperature History",
|
||||||
|
"entities": [], # To be filled with temperature sensor entities
|
||||||
|
"hours_to_show": 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Temperature Sensors",
|
||||||
|
"entities": [], # To be filled with temperature sensor entities (device_class: temperature)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "history-graph",
|
||||||
|
"title": "Humidity History",
|
||||||
|
"entities": [], # To be filled with humidity sensor entities
|
||||||
|
"hours_to_show": 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Humidity Sensors",
|
||||||
|
"entities": [], # To be filled with humidity sensor entities (device_class: humidity)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "weather-forecast",
|
||||||
|
"entity": "", # To be filled with weather entity (optional)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"title": "Media Center",
|
||||||
|
"url_path": "media",
|
||||||
|
"icon": "mdi:play",
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"title": "Media Players",
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"type": "media-control",
|
||||||
|
"entity": "", # To be filled with media player entity
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "All Media Players",
|
||||||
|
"entities": [], # To be filled with media player entities
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"energy": {
|
||||||
|
"title": "Energy Monitoring",
|
||||||
|
"url_path": "energy",
|
||||||
|
"icon": "mdi:lightning-bolt",
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"title": "Energy Usage",
|
||||||
|
"cards": [
|
||||||
|
{"type": "energy-distribution", "title": "Energy Distribution"},
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Power Sensors",
|
||||||
|
"entities": [], # To be filled with power sensor entities
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "history-graph",
|
||||||
|
"title": "Power Usage",
|
||||||
|
"entities": [], # To be filled with power entities
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Card type examples and their typical use cases
|
||||||
|
CARD_EXAMPLES = {
|
||||||
|
"entities": {
|
||||||
|
"description": "Shows a list of entities with their states",
|
||||||
|
"example": {
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Living Room",
|
||||||
|
"entities": [
|
||||||
|
"light.living_room_main",
|
||||||
|
"switch.living_room_fan",
|
||||||
|
"sensor.living_room_temperature",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"glance": {
|
||||||
|
"description": "Shows entities in a compact grid format",
|
||||||
|
"example": {
|
||||||
|
"type": "glance",
|
||||||
|
"title": "Quick Overview",
|
||||||
|
"entities": [
|
||||||
|
"binary_sensor.front_door",
|
||||||
|
"binary_sensor.back_door",
|
||||||
|
"binary_sensor.garage_door",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"thermostat": {
|
||||||
|
"description": "Controls and displays thermostat information",
|
||||||
|
"example": {"type": "thermostat", "entity": "climate.main_thermostat"},
|
||||||
|
},
|
||||||
|
"weather-forecast": {
|
||||||
|
"description": "Shows weather information and forecast",
|
||||||
|
"example": {
|
||||||
|
"type": "weather-forecast",
|
||||||
|
"entity": "weather.home",
|
||||||
|
"name": "Weather",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"media-control": {
|
||||||
|
"description": "Controls media players with full interface",
|
||||||
|
"example": {"type": "media-control", "entity": "media_player.living_room_tv"},
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"description": "Dedicated light control card",
|
||||||
|
"example": {"type": "light", "entity": "light.living_room_main"},
|
||||||
|
},
|
||||||
|
"alarm-panel": {
|
||||||
|
"description": "Security alarm panel interface",
|
||||||
|
"example": {"type": "alarm-panel", "entity": "alarm_control_panel.home_alarm"},
|
||||||
|
},
|
||||||
|
"picture-entity": {
|
||||||
|
"description": "Shows entity state with a background image",
|
||||||
|
"example": {
|
||||||
|
"type": "picture-entity",
|
||||||
|
"entity": "light.living_room",
|
||||||
|
"image": "/local/living_room.jpg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"history-graph": {
|
||||||
|
"description": "Shows historical data as a graph",
|
||||||
|
"example": {
|
||||||
|
"type": "history-graph",
|
||||||
|
"title": "Temperature History",
|
||||||
|
"entities": ["sensor.temperature_indoor", "sensor.temperature_outdoor"],
|
||||||
|
"hours_to_show": 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gauge": {
|
||||||
|
"description": "Shows a single entity value as a gauge",
|
||||||
|
"example": {
|
||||||
|
"type": "gauge",
|
||||||
|
"entity": "sensor.cpu_temperature",
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"name": "CPU Temperature",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Predefined color schemes and icons
|
||||||
|
COMMON_ICONS = {
|
||||||
|
"lights": "mdi:lightbulb",
|
||||||
|
"security": "mdi:security",
|
||||||
|
"climate": "mdi:thermometer",
|
||||||
|
"energy": "mdi:lightning-bolt",
|
||||||
|
"media": "mdi:play",
|
||||||
|
"kitchen": "mdi:chef-hat",
|
||||||
|
"bedroom": "mdi:bed",
|
||||||
|
"bathroom": "mdi:shower",
|
||||||
|
"living_room": "mdi:sofa",
|
||||||
|
"garage": "mdi:garage",
|
||||||
|
"garden": "mdi:flower",
|
||||||
|
"office": "mdi:desk",
|
||||||
|
"basement": "mdi:stairs-down",
|
||||||
|
"attic": "mdi:stairs-up",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_for_entities(entities, dashboard_type="general"):
|
||||||
|
"""Generate a dashboard template based on available entities."""
|
||||||
|
template = {
|
||||||
|
"title": f"{dashboard_type.title()} Dashboard",
|
||||||
|
"url_path": dashboard_type.lower().replace(" ", "-"),
|
||||||
|
"icon": COMMON_ICONS.get(dashboard_type, "mdi:view-dashboard"),
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"views": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Group entities by domain and by device_class for sensors
|
||||||
|
entity_groups = {}
|
||||||
|
sensor_by_device_class = {}
|
||||||
|
|
||||||
|
for entity in entities:
|
||||||
|
if isinstance(entity, dict) and "entity_id" in entity:
|
||||||
|
entity_id = entity["entity_id"]
|
||||||
|
# Get device_class from attributes if available
|
||||||
|
device_class = entity.get("attributes", {}).get("device_class")
|
||||||
|
else:
|
||||||
|
entity_id = str(entity)
|
||||||
|
device_class = None
|
||||||
|
|
||||||
|
domain = entity_id.split(".")[0]
|
||||||
|
if domain not in entity_groups:
|
||||||
|
entity_groups[domain] = []
|
||||||
|
entity_groups[domain].append(entity_id)
|
||||||
|
|
||||||
|
# Categorize sensors by device_class
|
||||||
|
if domain == "sensor" and device_class:
|
||||||
|
if device_class not in sensor_by_device_class:
|
||||||
|
sensor_by_device_class[device_class] = []
|
||||||
|
sensor_by_device_class[device_class].append(entity_id)
|
||||||
|
|
||||||
|
# Create view with cards for each domain
|
||||||
|
view_cards = []
|
||||||
|
|
||||||
|
# Lights
|
||||||
|
if "light" in entity_groups:
|
||||||
|
view_cards.append(
|
||||||
|
{"type": "entities", "title": "Lights", "entities": entity_groups["light"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Climate - handle both climate entities and temperature/humidity sensors
|
||||||
|
if "climate" in entity_groups:
|
||||||
|
for climate_entity in entity_groups["climate"]:
|
||||||
|
view_cards.append({"type": "thermostat", "entity": climate_entity})
|
||||||
|
|
||||||
|
# Temperature sensors - create history graphs for climate dashboards
|
||||||
|
if "temperature" in sensor_by_device_class:
|
||||||
|
temp_sensors = sensor_by_device_class["temperature"]
|
||||||
|
# Add history graph for temperature visualization
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "history-graph",
|
||||||
|
"title": "Temperature History",
|
||||||
|
"entities": temp_sensors,
|
||||||
|
"hours_to_show": 24,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Add entity card for current values
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Temperature Sensors",
|
||||||
|
"entities": temp_sensors,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Humidity sensors - create gauge cards for climate dashboards
|
||||||
|
if "humidity" in sensor_by_device_class:
|
||||||
|
humidity_sensors = sensor_by_device_class["humidity"]
|
||||||
|
# Add history graph for humidity visualization
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "history-graph",
|
||||||
|
"title": "Humidity History",
|
||||||
|
"entities": humidity_sensors,
|
||||||
|
"hours_to_show": 24,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Add entity card for current values
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Humidity Sensors",
|
||||||
|
"entities": humidity_sensors,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Media players
|
||||||
|
if "media_player" in entity_groups:
|
||||||
|
for media_entity in entity_groups["media_player"]:
|
||||||
|
view_cards.append({"type": "media-control", "entity": media_entity})
|
||||||
|
|
||||||
|
# Security entities
|
||||||
|
if "binary_sensor" in entity_groups:
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Sensors",
|
||||||
|
"entities": entity_groups["binary_sensor"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "alarm_control_panel" in entity_groups:
|
||||||
|
for alarm_entity in entity_groups["alarm_control_panel"]:
|
||||||
|
view_cards.append({"type": "alarm-panel", "entity": alarm_entity})
|
||||||
|
|
||||||
|
# Other Sensors (not temperature or humidity)
|
||||||
|
if "sensor" in entity_groups:
|
||||||
|
# Filter out temperature and humidity sensors that we already handled
|
||||||
|
other_sensors = [
|
||||||
|
s
|
||||||
|
for s in entity_groups["sensor"]
|
||||||
|
if s not in sensor_by_device_class.get("temperature", [])
|
||||||
|
and s not in sensor_by_device_class.get("humidity", [])
|
||||||
|
]
|
||||||
|
if other_sensors:
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Other Sensors",
|
||||||
|
"entities": other_sensors[:10], # Limit to first 10
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Switches
|
||||||
|
if "switch" in entity_groups:
|
||||||
|
view_cards.append(
|
||||||
|
{
|
||||||
|
"type": "entities",
|
||||||
|
"title": "Switches",
|
||||||
|
"entities": entity_groups["switch"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Weather
|
||||||
|
if "weather" in entity_groups:
|
||||||
|
view_cards.append(
|
||||||
|
{"type": "weather-forecast", "entity": entity_groups["weather"][0]}
|
||||||
|
)
|
||||||
|
|
||||||
|
template["views"] = [{"title": "Overview", "cards": view_cards}]
|
||||||
|
|
||||||
|
return template
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
panel_custom:
|
||||||
|
- name: AI Agent HA
|
||||||
|
sidebar_title: AI Agent HA
|
||||||
|
sidebar_icon: mdi:robot
|
||||||
|
url_path: ai_agent_ha
|
||||||
|
module_url: /local/ai_agent_ha/frontend/ai_agent_ha-panel.js
|
||||||
|
trust_external: true
|
||||||
|
require_admin: false
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"domain": "ai_agent_ha",
|
||||||
|
"name": "AI Agent HA",
|
||||||
|
"after_dependencies": [
|
||||||
|
"history",
|
||||||
|
"recorder",
|
||||||
|
"lovelace"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@sbenodiz"
|
||||||
|
],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"documentation": "https://github.com/sbenodiz/ai_agent_ha",
|
||||||
|
"integration_type": "service",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"issue_tracker": "https://github.com/sbenodiz/ai_agent_ha/issues",
|
||||||
|
"requirements": [
|
||||||
|
"aiohttp>=3.8.0"
|
||||||
|
],
|
||||||
|
"version": "1.14.1"
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
query:
|
||||||
|
name: "Query AI Agent with Home Assistant context"
|
||||||
|
description: "Run a custom AI prompt against your Home Assistant state dump."
|
||||||
|
fields:
|
||||||
|
prompt:
|
||||||
|
description: "The question or instruction to send to AI model."
|
||||||
|
example: "Turn on all the lights in the living room"
|
||||||
|
debug:
|
||||||
|
description: "Include a debug trace of the HA↔AI conversation (true/false)."
|
||||||
|
example: true
|
||||||
|
default: false
|
||||||
|
provider:
|
||||||
|
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
|
||||||
|
example: "openai"
|
||||||
|
default: "openai"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "openai"
|
||||||
|
- "llama"
|
||||||
|
- "gemini"
|
||||||
|
- "openrouter"
|
||||||
|
- "anthropic"
|
||||||
|
- "alter"
|
||||||
|
- "zai"
|
||||||
|
- "local"
|
||||||
|
|
||||||
|
create_dashboard:
|
||||||
|
name: "Create Dashboard via AI Agent"
|
||||||
|
description: "Create a new Home Assistant dashboard using AI assistance."
|
||||||
|
fields:
|
||||||
|
dashboard_config:
|
||||||
|
description: "The dashboard configuration as a JSON object."
|
||||||
|
example: '{"title": "My Dashboard", "url_path": "my-dashboard", "views": []}'
|
||||||
|
provider:
|
||||||
|
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
|
||||||
|
example: "openai"
|
||||||
|
default: "openai"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "openai"
|
||||||
|
- "llama"
|
||||||
|
- "gemini"
|
||||||
|
- "openrouter"
|
||||||
|
- "anthropic"
|
||||||
|
- "alter"
|
||||||
|
- "zai"
|
||||||
|
- "local"
|
||||||
|
|
||||||
|
create_automation:
|
||||||
|
name: "Create Automation via AI Agent"
|
||||||
|
description: "Create a new Home Assistant automation using AI assistance."
|
||||||
|
fields:
|
||||||
|
automation:
|
||||||
|
description: "The automation configuration as a JSON object."
|
||||||
|
example: '{"alias": "Turn off lights at 9 PM", "trigger": [{"platform": "time", "at": "21:00:00"}], "action": [{"service": "light.turn_off", "target": {"entity_id": "light.living_room"}}]}'
|
||||||
|
provider:
|
||||||
|
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
|
||||||
|
example: "openai"
|
||||||
|
default: "openai"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "openai"
|
||||||
|
- "llama"
|
||||||
|
- "gemini"
|
||||||
|
- "openrouter"
|
||||||
|
- "anthropic"
|
||||||
|
- "alter"
|
||||||
|
- "zai"
|
||||||
|
- "local"
|
||||||
|
|
||||||
|
update_dashboard:
|
||||||
|
name: "Update Dashboard via AI Agent"
|
||||||
|
description: "Update an existing Home Assistant dashboard using AI assistance."
|
||||||
|
fields:
|
||||||
|
dashboard_url:
|
||||||
|
description: "The URL path of dashboard to update."
|
||||||
|
example: "my-dashboard"
|
||||||
|
dashboard_config:
|
||||||
|
description: "The updated dashboard configuration as a JSON object."
|
||||||
|
example: '{"title": "Updated Dashboard", "views": []}'
|
||||||
|
provider:
|
||||||
|
description: "The AI provider to use (openai, llama, gemini, openrouter, anthropic, alter, zai, local)"
|
||||||
|
example: "openai"
|
||||||
|
default: "openai"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "openai"
|
||||||
|
- "llama"
|
||||||
|
- "gemini"
|
||||||
|
- "openrouter"
|
||||||
|
- "anthropic"
|
||||||
|
- "alter"
|
||||||
|
- "zai"
|
||||||
|
- "local"
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Choose AI Provider",
|
||||||
|
"description": "Select your AI provider",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "AI Provider"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Choose your preferred AI provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure": {
|
||||||
|
"title": "Configure {provider}",
|
||||||
|
"description": "Enter your {token_label} and optionally select a model",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"anthropic_token": "Anthropic API Key",
|
||||||
|
"alter_token": "Alter API Key",
|
||||||
|
"model": "Model (Optional)",
|
||||||
|
"custom_model": "Custom Model (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Enter your Llama API token",
|
||||||
|
"openai_token": "Enter your OpenAI API key",
|
||||||
|
"gemini_token": "Enter your Google Gemini API key",
|
||||||
|
"openrouter_token": "Enter your OpenRouter API key",
|
||||||
|
"anthropic_token": "Enter your Anthropic API key",
|
||||||
|
"alter_token": "Enter your Alter API key",
|
||||||
|
"model": "Choose a predefined model or select 'Custom...' to enter your own",
|
||||||
|
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Invalid API key format",
|
||||||
|
"unknown": "Unknown error occurred",
|
||||||
|
"llama_token": "Llama API token is required",
|
||||||
|
"openai_token": "OpenAI API key is required",
|
||||||
|
"gemini_token": "Google Gemini API key is required",
|
||||||
|
"openrouter_token": "OpenRouter API key is required",
|
||||||
|
"anthropic_token": "Anthropic API key is required",
|
||||||
|
"alter_token": "Alter API key is required"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "AI Agent HA is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "AI Provider Settings",
|
||||||
|
"description": "Current provider: {current_provider}. Select a provider to configure",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "AI Provider"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Choose your preferred AI provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure_options": {
|
||||||
|
"title": "Configure {provider}",
|
||||||
|
"description": "Update your {token_label} and model settings",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"anthropic_token": "Anthropic API Key",
|
||||||
|
"alter_token": "Alter API Key",
|
||||||
|
"model": "Model",
|
||||||
|
"custom_model": "Custom Model (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Enter your Llama API token",
|
||||||
|
"openai_token": "Enter your OpenAI API key",
|
||||||
|
"gemini_token": "Enter your Google Gemini API key",
|
||||||
|
"openrouter_token": "Enter your OpenRouter API key",
|
||||||
|
"anthropic_token": "Enter your Anthropic API key",
|
||||||
|
"alter_token": "Enter your Alter API key",
|
||||||
|
"model": "Choose a model or select 'Custom...' to enter your own",
|
||||||
|
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Agent d'IA HA - Seleccionar proveïdor",
|
||||||
|
"description": "Tria el teu proveïdor d'IA",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "Proveïdor d'IA"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Selecciona el teu proveïdor d'IA preferit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure": {
|
||||||
|
"title": "Agent d'IA HA - Configurar {provider}",
|
||||||
|
"description": "Configura les teves credencials d'API i el model de {provider}",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Token de l'API de Llama",
|
||||||
|
"openai_token": "Clau de l'API d'OpenAI",
|
||||||
|
"gemini_token": "Clau de l'API de Google Gemini",
|
||||||
|
"openrouter_token": "Clau de l'API d'OpenRouter",
|
||||||
|
"model": "Model (Opcional)",
|
||||||
|
"custom_model": "Nom del model personalitzat (Opcional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Introdueix el teu token de l'API de Llama",
|
||||||
|
"openai_token": "Introdueix la teva clau de l'API d'OpenAI",
|
||||||
|
"gemini_token": "Introdueix la teva clau de l'API de Google Gemini",
|
||||||
|
"openrouter_token": "Introdueix la teva clau de l'API d'OpenRouter",
|
||||||
|
"model": "Tria un model predefinit o selecciona 'Personalitzat...' per utilitzar un model personalitzat",
|
||||||
|
"custom_model": "Introdueix un nom de model personalitzat (anul·la la selecció desplegable anterior)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Clau o token d'API no vàlid",
|
||||||
|
"llama_token": "Es requereix el token de l'API de Llama",
|
||||||
|
"openai_token": "Es requereix la clau de l'API d'OpenAI",
|
||||||
|
"gemini_token": "Es requereix la clau de l'API de Google Gemini",
|
||||||
|
"openrouter_token": "Es requereix la clau de l'API d'OpenRouter",
|
||||||
|
"unknown": "S'ha produït un error inesperat"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "L'Agent d'IA HA ja està configurat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Agent d'IA HA - Seleccionar proveïdor",
|
||||||
|
"description": "Tria el teu proveïdor d'IA (Actual: {current_provider})",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "Proveïdor d'IA"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Selecciona el teu proveïdor d'IA preferit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure_options": {
|
||||||
|
"title": "Agent d'IA HA - Configurar {provider}",
|
||||||
|
"description": "Actualitza les teves credencials d'API i el model de {provider}",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Token de l'API de Llama",
|
||||||
|
"openai_token": "Clau de l'API d'OpenAI",
|
||||||
|
"gemini_token": "Clau de l'API de Google Gemini",
|
||||||
|
"openrouter_token": "Clau de l'API d'OpenRouter",
|
||||||
|
"model": "Model (Opcional)",
|
||||||
|
"custom_model": "Nom del model personalitzat (Opcional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Introdueix el teu token de l'API de Llama",
|
||||||
|
"openai_token": "Introdueix la teva clau de l'API d'OpenAI",
|
||||||
|
"gemini_token": "Introdueix la teva clau de l'API de Google Gemini",
|
||||||
|
"openrouter_token": "Introdueix la teva clau de l'API d'OpenRouter",
|
||||||
|
"model": "Tria un model predefinit o selecciona 'Personalitzat...' per utilitzar un model personalitzat",
|
||||||
|
"custom_model": "Introdueix un nom de model personalitzat (anul·la la selecció desplegable anterior)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "KI-Anbieter wählen",
|
||||||
|
"description": "Wähle deinen KI-Anbieter aus",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "KI-Anbieter"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Wähle deinen bevorzugten KI-Anbieter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure": {
|
||||||
|
"title": "{provider} konfigurieren",
|
||||||
|
"description": "Gib deinen {token_label} ein und wähle optional ein Model",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"anthropic_token": "Anthropic API Key",
|
||||||
|
"model": "Model (Optional)",
|
||||||
|
"custom_model": "Custom Model (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Gib deinen Llama API Token ein",
|
||||||
|
"openai_token": "Gib deinen OpenAI API Key ein",
|
||||||
|
"gemini_token": "Gib deinen Google Gemini API Key ein",
|
||||||
|
"openrouter_token": "Gib deinen OpenRouter API Key ein",
|
||||||
|
"anthropic_token": "Gib deinen Anthropic API Key ein",
|
||||||
|
"model": "Wähle ein vordefiniertes Model oder 'Custom...' zum manuellen Eintrag",
|
||||||
|
"custom_model": "Gib einen benutzerdefinierten Model-Namen ein (nur relevant bei Auswahl von 'Custom...')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Ungültiges API-Key-Format",
|
||||||
|
"unknown": "Unbekannter Fehler aufgetreten",
|
||||||
|
"llama_token": "Llama API Token ist erforderlich",
|
||||||
|
"openai_token": "OpenAI API Key ist erforderlich",
|
||||||
|
"gemini_token": "Google Gemini API Key ist erforderlich",
|
||||||
|
"openrouter_token": "OpenRouter API Key ist erforderlich",
|
||||||
|
"anthropic_token": "Anthropic API Key ist erforderlich"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "AI Agent HA ist bereits konfiguriert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Einstellungen für KI-Anbieter",
|
||||||
|
"description": "Aktueller Anbieter: {current_provider}. Wähle einen Anbieter zur Konfiguration",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "KI-Anbieter"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Wähle deinen bevorzugten KI-Anbieter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure_options": {
|
||||||
|
"title": "{provider} konfigurieren",
|
||||||
|
"description": "Aktualisiere deinen {token_label} und Model-Einstellungen",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"anthropic_token": "Anthropic API Key",
|
||||||
|
"model": "Model",
|
||||||
|
"custom_model": "Custom Model (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Gib deinen Llama API Token ein",
|
||||||
|
"openai_token": "Gib deinen OpenAI API Key ein",
|
||||||
|
"gemini_token": "Gib deinen Google Gemini API Key ein",
|
||||||
|
"openrouter_token": "Gib deinen OpenRouter API Key ein",
|
||||||
|
"anthropic_token": "Gib deinen Anthropic API Key ein",
|
||||||
|
"model": "Wähle ein Model oder 'Custom...' zum manuellen Eintrag",
|
||||||
|
"custom_model": "Gib einen benutzerdefinierten Model-Namen ein (nur relevant bei Auswahl von 'Custom...')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Choose AI Provider",
|
||||||
|
"description": "Select your AI provider",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "AI Provider"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Choose your preferred AI provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure": {
|
||||||
|
"title": "Configure {provider}",
|
||||||
|
"description": "Enter your {token_label} and optionally select a model",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"anthropic_token": "Anthropic API Key",
|
||||||
|
"alter_token": "Alter API Key",
|
||||||
|
"model": "Model (Optional)",
|
||||||
|
"custom_model": "Custom Model (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Enter your Llama API token",
|
||||||
|
"openai_token": "Enter your OpenAI API key",
|
||||||
|
"gemini_token": "Enter your Google Gemini API key",
|
||||||
|
"openrouter_token": "Enter your OpenRouter API key",
|
||||||
|
"anthropic_token": "Enter your Anthropic API key",
|
||||||
|
"alter_token": "Enter your Alter API key",
|
||||||
|
"model": "Choose a predefined model or select 'Custom...' to enter your own",
|
||||||
|
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Invalid API key format",
|
||||||
|
"unknown": "Unknown error occurred",
|
||||||
|
"llama_token": "Llama API token is required",
|
||||||
|
"openai_token": "OpenAI API key is required",
|
||||||
|
"gemini_token": "Google Gemini API key is required",
|
||||||
|
"openrouter_token": "OpenRouter API key is required",
|
||||||
|
"anthropic_token": "Anthropic API key is required",
|
||||||
|
"alter_token": "Alter API key is required"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "AI Agent HA is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "AI Provider Settings",
|
||||||
|
"description": "Current provider: {current_provider}. Select a provider to configure",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "AI Provider"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Choose your preferred AI provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure_options": {
|
||||||
|
"title": "Configure {provider}",
|
||||||
|
"description": "Update your {token_label} and model settings",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"anthropic_token": "Anthropic API Key",
|
||||||
|
"alter_token": "Alter API Key",
|
||||||
|
"model": "Model",
|
||||||
|
"custom_model": "Custom Model (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Enter your Llama API token",
|
||||||
|
"openai_token": "Enter your OpenAI API key",
|
||||||
|
"gemini_token": "Enter your Google Gemini API key",
|
||||||
|
"openrouter_token": "Enter your OpenRouter API key",
|
||||||
|
"anthropic_token": "Enter your Anthropic API key",
|
||||||
|
"alter_token": "Enter your Alter API key",
|
||||||
|
"model": "Choose a model or select 'Custom...' to enter your own",
|
||||||
|
"custom_model": "Enter a custom model name (only used if 'Custom...' is selected above)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "AI Agent HA - Select Provider",
|
||||||
|
"description": "Choose your AI provider",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "AI Provider"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Select your preferred AI provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure": {
|
||||||
|
"title": "AI Agent HA - Configure {provider}",
|
||||||
|
"description": "Configure your {provider} API credentials and model",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"model": "Model (Optional)",
|
||||||
|
"custom_model": "Custom Model Name (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Enter your Llama API token",
|
||||||
|
"openai_token": "Enter your OpenAI API key",
|
||||||
|
"gemini_token": "Enter your Google Gemini API key",
|
||||||
|
"openrouter_token": "Enter your OpenRouter API key",
|
||||||
|
"model": "Choose a predefined model or select 'Custom...' to use a custom model",
|
||||||
|
"custom_model": "Enter a custom model name (overrides the dropdown selection above)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Invalid API key or token",
|
||||||
|
"llama_token": "Llama API token is required",
|
||||||
|
"openai_token": "OpenAI API key is required",
|
||||||
|
"gemini_token": "Google Gemini API key is required",
|
||||||
|
"openrouter_token": "OpenRouter API key is required",
|
||||||
|
"unknown": "Unexpected error occurred"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "AI Agent HA is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "AI Agent HA - Select Provider",
|
||||||
|
"description": "Choose your AI provider (Current: {current_provider})",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "AI Provider"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Select your preferred AI provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure_options": {
|
||||||
|
"title": "AI Agent HA - Configure {provider}",
|
||||||
|
"description": "Update your {provider} API credentials and model",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Llama API Token",
|
||||||
|
"openai_token": "OpenAI API Key",
|
||||||
|
"gemini_token": "Google Gemini API Key",
|
||||||
|
"openrouter_token": "OpenRouter API Key",
|
||||||
|
"model": "Model (Optional)",
|
||||||
|
"custom_model": "Custom Model Name (Optional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Enter your Llama API token",
|
||||||
|
"openai_token": "Enter your OpenAI API key",
|
||||||
|
"gemini_token": "Enter your Google Gemini API key",
|
||||||
|
"openrouter_token": "Enter your OpenRouter API key",
|
||||||
|
"model": "Choose a predefined model or select 'Custom...' to use a custom model",
|
||||||
|
"custom_model": "Enter a custom model name (overrides the dropdown selection above)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Agente de IA HA - Seleccionar proveedor",
|
||||||
|
"description": "Elige tu proveedor de IA",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "Proveedor de IA"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Selecciona tu proveedor de IA preferido"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure": {
|
||||||
|
"title": "Agente de IA HA - Configurar {provider}",
|
||||||
|
"description": "Configura tus credenciales de API y modelo de {provider}",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Token de la API de Llama",
|
||||||
|
"openai_token": "Clave de la API de OpenAI",
|
||||||
|
"gemini_token": "Clave de la API de Google Gemini",
|
||||||
|
"openrouter_token": "Clave de la API de OpenRouter",
|
||||||
|
"model": "Modelo (Opcional)",
|
||||||
|
"custom_model": "Nombre de modelo personalizado (Opcional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Introduce tu token de la API de Llama",
|
||||||
|
"openai_token": "Introduce tu clave de la API de OpenAI",
|
||||||
|
"gemini_token": "Introduce tu clave de la API de Google Gemini",
|
||||||
|
"openrouter_token": "Introduce tu clave de la API de OpenRouter",
|
||||||
|
"model": "Elige un modelo predefinido o selecciona 'Personalizado...' para usar un modelo personalizado",
|
||||||
|
"custom_model": "Introduce un nombre de modelo personalizado (anula la selección desplegable anterior)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_api_key": "Clave o token de API no válido",
|
||||||
|
"llama_token": "Se requiere el token de la API de Llama",
|
||||||
|
"openai_token": "Se requiere la clave de la API de OpenAI",
|
||||||
|
"gemini_token": "Se requiere la clave de la API de Google Gemini",
|
||||||
|
"openrouter_token": "Se requiere la clave de la API de OpenRouter",
|
||||||
|
"unknown": "Ha ocurrido un error inesperado"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "El Agente de IA HA ya está configurado"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Agente de IA HA - Seleccionar proveedor",
|
||||||
|
"description": "Elige tu proveedor de IA (Actual: {current_provider})",
|
||||||
|
"data": {
|
||||||
|
"ai_provider": "Proveedor de IA"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ai_provider": "Selecciona tu proveedor de IA preferido"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configure_options": {
|
||||||
|
"title": "Agente de IA HA - Configurar {provider}",
|
||||||
|
"description": "Actualiza tus credenciales de API y modelo de {provider}",
|
||||||
|
"data": {
|
||||||
|
"llama_token": "Token de la API de Llama",
|
||||||
|
"openai_token": "Clave de la API de OpenAI",
|
||||||
|
"gemini_token": "Clave de la API de Google Gemini",
|
||||||
|
"openrouter_token": "Clave de la API de OpenRouter",
|
||||||
|
"model": "Modelo (Opcional)",
|
||||||
|
"custom_model": "Nombre de modelo personalizado (Opcional)"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"llama_token": "Introduce tu token de la API de Llama",
|
||||||
|
"openai_token": "Introduce tu clave de la API de OpenAI",
|
||||||
|
"gemini_token": "Introduce tu clave de la API de Google Gemini",
|
||||||
|
"openrouter_token": "Introduce tu clave de la API de OpenRouter",
|
||||||
|
"model": "Elige un modelo predefinido o selecciona 'Personalizado...' para usar un modelo personalizado",
|
||||||
|
"custom_model": "Introduce un nombre de modelo personalizado (anula la selección desplegable anterior)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
"""The Alarmo Integration."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
from homeassistant.core import (
|
||||||
|
HomeAssistant,
|
||||||
|
asyncio,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_CODE,
|
||||||
|
ATTR_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.helpers.service import (
|
||||||
|
async_register_admin_service,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_send,
|
||||||
|
async_dispatcher_connect,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
from homeassistant.components.alarm_control_panel import DOMAIN as PLATFORM
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
from .card import async_register_card
|
||||||
|
from .mqtt import MqttHandler
|
||||||
|
from .event import EventHandler
|
||||||
|
from .panel import (
|
||||||
|
async_register_panel,
|
||||||
|
async_unregister_panel,
|
||||||
|
)
|
||||||
|
from .store import async_get_registry
|
||||||
|
from .sensors import (
|
||||||
|
ATTR_GROUP,
|
||||||
|
ATTR_ENTITIES,
|
||||||
|
ATTR_NEW_ENTITY_ID,
|
||||||
|
SensorHandler,
|
||||||
|
)
|
||||||
|
from .websockets import async_register_websockets
|
||||||
|
from .automations import AutomationHandler
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Max number of threads to start when checking user codes.
|
||||||
|
MAX_WORKERS = 4
|
||||||
|
# Number of rounds of hashing when computing user hashes.
|
||||||
|
BCRYPT_NUM_ROUNDS = 10
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Track states and offer events for sensors."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Alarmo integration from a config entry."""
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
store = await async_get_registry(hass)
|
||||||
|
coordinator = AlarmoCoordinator(hass, session, entry, store)
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(const.DOMAIN, coordinator.id)},
|
||||||
|
name=const.NAME,
|
||||||
|
model=const.NAME,
|
||||||
|
sw_version=const.VERSION,
|
||||||
|
manufacturer=const.MANUFACTURER,
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data.setdefault(const.DOMAIN, {})
|
||||||
|
hass.data[const.DOMAIN] = {"coordinator": coordinator, "areas": {}, "master": None}
|
||||||
|
|
||||||
|
if entry.unique_id is None:
|
||||||
|
hass.config_entries.async_update_entry(entry, unique_id=coordinator.id, data={})
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, [PLATFORM])
|
||||||
|
|
||||||
|
# Register the panel (frontend)
|
||||||
|
await async_register_panel(hass)
|
||||||
|
await async_register_card(hass)
|
||||||
|
|
||||||
|
# Websocket support
|
||||||
|
await async_register_websockets(hass)
|
||||||
|
|
||||||
|
# Register custom services
|
||||||
|
register_services(hass)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload Alarmo config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[hass.config_entries.async_forward_entry_unload(entry, PLATFORM)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not unload_ok:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async_unregister_panel(hass)
|
||||||
|
coordinator = hass.data[const.DOMAIN]["coordinator"]
|
||||||
|
await coordinator.async_unload()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass, entry):
|
||||||
|
"""Remove Alarmo config entry."""
|
||||||
|
async_unregister_panel(hass)
|
||||||
|
coordinator = hass.data[const.DOMAIN]["coordinator"]
|
||||||
|
await coordinator.async_delete_config()
|
||||||
|
del hass.data[const.DOMAIN]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Handle migration of config entry."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmoCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Define an object to hold Alarmo device."""
|
||||||
|
|
||||||
|
def __init__(self, hass, session, entry, store):
|
||||||
|
"""Initialize."""
|
||||||
|
self.id = entry.unique_id
|
||||||
|
self.hass = hass
|
||||||
|
self.entry = entry
|
||||||
|
self.store = store
|
||||||
|
self._subscriptions = []
|
||||||
|
|
||||||
|
self._subscriptions.append(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass, "alarmo_platform_loaded", self.setup_alarm_entities
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.register_events()
|
||||||
|
|
||||||
|
super().__init__(hass, _LOGGER, config_entry=entry, name=const.DOMAIN)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def setup_alarm_entities(self):
|
||||||
|
"""Set up alarm_control_panel entities based on areas in storage."""
|
||||||
|
self.hass.data[const.DOMAIN]["sensor_handler"] = SensorHandler(self.hass)
|
||||||
|
self.hass.data[const.DOMAIN]["automation_handler"] = AutomationHandler(
|
||||||
|
self.hass
|
||||||
|
)
|
||||||
|
self.hass.data[const.DOMAIN]["mqtt_handler"] = MqttHandler(self.hass)
|
||||||
|
self.hass.data[const.DOMAIN]["event_handler"] = EventHandler(self.hass)
|
||||||
|
|
||||||
|
areas = self.store.async_get_areas()
|
||||||
|
config = self.store.async_get_config()
|
||||||
|
|
||||||
|
for item in areas.values():
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_register_entity", item)
|
||||||
|
|
||||||
|
if len(areas) > 1 and config["master"]["enabled"]:
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_register_master", config["master"])
|
||||||
|
|
||||||
|
async def async_update_config(self, data):
|
||||||
|
"""Update the main configuration."""
|
||||||
|
if "master" in data:
|
||||||
|
old_config = self.store.async_get_config()
|
||||||
|
if old_config[const.ATTR_MASTER] != data["master"]:
|
||||||
|
if self.hass.data[const.DOMAIN]["master"]:
|
||||||
|
await self.async_remove_entity("master")
|
||||||
|
if data["master"]["enabled"]:
|
||||||
|
async_dispatcher_send(
|
||||||
|
self.hass, "alarmo_register_master", data["master"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
automations = self.hass.data[const.DOMAIN][
|
||||||
|
"automation_handler"
|
||||||
|
].get_automations_by_area(None)
|
||||||
|
if len(automations):
|
||||||
|
for el in automations:
|
||||||
|
self.store.async_delete_automation(el)
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_automations_updated")
|
||||||
|
|
||||||
|
self.store.async_update_config(data)
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_config_updated")
|
||||||
|
|
||||||
|
async def async_update_area_config(
|
||||||
|
self,
|
||||||
|
area_id: str | None = None,
|
||||||
|
data: dict = {},
|
||||||
|
):
|
||||||
|
"""Update area configuration."""
|
||||||
|
if const.ATTR_REMOVE in data:
|
||||||
|
# delete an area
|
||||||
|
res = self.store.async_get_area(area_id)
|
||||||
|
if not res:
|
||||||
|
return
|
||||||
|
sensors = self.store.async_get_sensors()
|
||||||
|
sensors = dict(filter(lambda el: el[1]["area"] == area_id, sensors.items()))
|
||||||
|
if sensors:
|
||||||
|
for el in sensors.keys():
|
||||||
|
self.store.async_delete_sensor(el)
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_sensors_updated")
|
||||||
|
|
||||||
|
automations = self.hass.data[const.DOMAIN][
|
||||||
|
"automation_handler"
|
||||||
|
].get_automations_by_area(area_id)
|
||||||
|
if len(automations):
|
||||||
|
for el in automations:
|
||||||
|
self.store.async_delete_automation(el)
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_automations_updated")
|
||||||
|
|
||||||
|
self.store.async_delete_area(area_id)
|
||||||
|
await self.async_remove_entity(area_id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
len(self.store.async_get_areas()) == 1
|
||||||
|
and self.hass.data[const.DOMAIN]["master"]
|
||||||
|
):
|
||||||
|
await self.async_remove_entity("master")
|
||||||
|
|
||||||
|
elif self.store.async_get_area(area_id):
|
||||||
|
# modify an area
|
||||||
|
entry = self.store.async_update_area(area_id, data)
|
||||||
|
if "name" not in data:
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_config_updated", area_id)
|
||||||
|
else:
|
||||||
|
await self.async_remove_entity(area_id)
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_register_entity", entry)
|
||||||
|
else:
|
||||||
|
# create an area
|
||||||
|
entry = self.store.async_create_area(data)
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_register_entity", entry)
|
||||||
|
|
||||||
|
config = self.store.async_get_config()
|
||||||
|
|
||||||
|
if len(self.store.async_get_areas()) == 2 and config["master"]["enabled"]:
|
||||||
|
async_dispatcher_send(
|
||||||
|
self.hass, "alarmo_register_master", config["master"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def async_update_sensor_config(self, entity_id: str, data: dict):
|
||||||
|
"""Update sensor configuration."""
|
||||||
|
group = None
|
||||||
|
if ATTR_GROUP in data:
|
||||||
|
group = data[ATTR_GROUP]
|
||||||
|
del data[ATTR_GROUP]
|
||||||
|
|
||||||
|
if ATTR_NEW_ENTITY_ID in data:
|
||||||
|
# delete old sensor entry when changing the entity_id
|
||||||
|
new_entity_id = data[ATTR_NEW_ENTITY_ID]
|
||||||
|
del data[ATTR_NEW_ENTITY_ID]
|
||||||
|
self.store.async_delete_sensor(entity_id)
|
||||||
|
self.assign_sensor_to_group(new_entity_id, group)
|
||||||
|
self.assign_sensor_to_group(entity_id, None)
|
||||||
|
entity_id = new_entity_id
|
||||||
|
|
||||||
|
if const.ATTR_REMOVE in data:
|
||||||
|
self.store.async_delete_sensor(entity_id)
|
||||||
|
self.assign_sensor_to_group(entity_id, None)
|
||||||
|
elif self.store.async_get_sensor(entity_id):
|
||||||
|
self.store.async_update_sensor(entity_id, data)
|
||||||
|
self.assign_sensor_to_group(entity_id, group)
|
||||||
|
else:
|
||||||
|
self.store.async_create_sensor(entity_id, data)
|
||||||
|
self.assign_sensor_to_group(entity_id, group)
|
||||||
|
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_sensors_updated")
|
||||||
|
|
||||||
|
async def _validate_user_code(self, user_id: str, data: dict):
|
||||||
|
user_with_code = await self.async_authenticate_user(data[ATTR_CODE])
|
||||||
|
if user_id:
|
||||||
|
if const.ATTR_OLD_CODE not in data:
|
||||||
|
return "No code provided"
|
||||||
|
if not await self.async_authenticate_user(
|
||||||
|
data[const.ATTR_OLD_CODE], user_id
|
||||||
|
):
|
||||||
|
return "Wrong code provided"
|
||||||
|
if user_with_code and user_with_code[const.ATTR_USER_ID] != user_id:
|
||||||
|
return "User with same code already exists"
|
||||||
|
elif user_with_code:
|
||||||
|
return "User with same code already exists"
|
||||||
|
return
|
||||||
|
|
||||||
|
def _validate_user_name(self, user_id: str, data: dict):
|
||||||
|
if not data[ATTR_NAME]:
|
||||||
|
return "User name must not be empty"
|
||||||
|
for user in self.store.async_get_users().values():
|
||||||
|
if (
|
||||||
|
data[ATTR_NAME] == user[ATTR_NAME]
|
||||||
|
and user_id != user[const.ATTR_USER_ID]
|
||||||
|
):
|
||||||
|
return "User with same name already exists"
|
||||||
|
return
|
||||||
|
|
||||||
|
async def async_update_user_config(
|
||||||
|
self, user_id: str | None = None, data: dict = {}
|
||||||
|
):
|
||||||
|
"""Update user configuration."""
|
||||||
|
if const.ATTR_REMOVE in data:
|
||||||
|
self.store.async_delete_user(user_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ATTR_NAME in data:
|
||||||
|
err = self._validate_user_name(user_id, data)
|
||||||
|
if err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
return err
|
||||||
|
if ATTR_CODE in data:
|
||||||
|
err = await self._validate_user_code(user_id, data)
|
||||||
|
if err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
return err
|
||||||
|
|
||||||
|
if data.get(ATTR_CODE):
|
||||||
|
data[const.ATTR_CODE_FORMAT] = (
|
||||||
|
"number" if data[ATTR_CODE].isdigit() else "text"
|
||||||
|
)
|
||||||
|
data[const.ATTR_CODE_LENGTH] = len(data[ATTR_CODE])
|
||||||
|
|
||||||
|
def _hash_code(code):
|
||||||
|
hashed = bcrypt.hashpw(
|
||||||
|
code.encode("utf-8"),
|
||||||
|
bcrypt.gensalt(rounds=BCRYPT_NUM_ROUNDS),
|
||||||
|
)
|
||||||
|
return base64.b64encode(hashed).decode()
|
||||||
|
|
||||||
|
data[ATTR_CODE] = await self.hass.async_add_executor_job(
|
||||||
|
_hash_code, data[ATTR_CODE]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
self.store.async_create_user(data)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if ATTR_CODE in data:
|
||||||
|
del data[const.ATTR_OLD_CODE]
|
||||||
|
self.store.async_update_user(user_id, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
async def async_authenticate_user(self, code, user_id=None):
|
||||||
|
"""Run user authentication in the executor.
|
||||||
|
|
||||||
|
This wrapper ensures that the blocking bcrypt-based authentication
|
||||||
|
is executed outside of the event loop, making it safe to call
|
||||||
|
from async context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _authenticate_user_sync():
|
||||||
|
"""Perform user authentication using blocking bcrypt operations."""
|
||||||
|
|
||||||
|
def check_user_code(user, code):
|
||||||
|
"""Returns None or the supplied user object if the code matches."""
|
||||||
|
if not user[const.ATTR_ENABLED]:
|
||||||
|
return None
|
||||||
|
elif not user[ATTR_CODE] and not code:
|
||||||
|
return user
|
||||||
|
elif user[ATTR_CODE]:
|
||||||
|
try:
|
||||||
|
hash = base64.b64decode(user[ATTR_CODE])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if bcrypt.checkpw((code or "").encode("utf-8"), hash):
|
||||||
|
return user
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
return check_user_code(self.store.async_get_user(user_id), code)
|
||||||
|
|
||||||
|
users = self.store.async_get_users()
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(
|
||||||
|
max_workers=MAX_WORKERS
|
||||||
|
) as executor:
|
||||||
|
futures = [
|
||||||
|
executor.submit(check_user_code, user, code)
|
||||||
|
for user in users.values()
|
||||||
|
]
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
if future.result():
|
||||||
|
executor.shutdown(wait=False, cancel_futures=True)
|
||||||
|
return future.result()
|
||||||
|
|
||||||
|
return await self.hass.async_add_executor_job(_authenticate_user_sync)
|
||||||
|
|
||||||
|
def async_update_automation_config(
|
||||||
|
self,
|
||||||
|
automation_id: str | None = None,
|
||||||
|
data: dict = {},
|
||||||
|
):
|
||||||
|
"""Update automation configuration."""
|
||||||
|
if const.ATTR_REMOVE in data:
|
||||||
|
self.store.async_delete_automation(automation_id)
|
||||||
|
elif not automation_id:
|
||||||
|
self.store.async_create_automation(data)
|
||||||
|
else:
|
||||||
|
self.store.async_update_automation(automation_id, data)
|
||||||
|
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_automations_updated")
|
||||||
|
|
||||||
|
def register_events(self):
|
||||||
|
"""Register event handlers."""
|
||||||
|
|
||||||
|
# handle push notifications with action buttons
|
||||||
|
@callback
|
||||||
|
async def async_handle_push_event(event):
|
||||||
|
if not event.data:
|
||||||
|
return
|
||||||
|
action = (
|
||||||
|
event.data.get("actionName")
|
||||||
|
if "actionName" in event.data
|
||||||
|
else event.data.get("action")
|
||||||
|
)
|
||||||
|
|
||||||
|
if action not in const.EVENT_ACTIONS:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.hass.data[const.DOMAIN]["master"]:
|
||||||
|
alarm_entity = self.hass.data[const.DOMAIN]["master"]
|
||||||
|
elif len(self.hass.data[const.DOMAIN]["areas"]) == 1:
|
||||||
|
alarm_entity = next(
|
||||||
|
iter(self.hass.data[const.DOMAIN]["areas"].values())
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Cannot process the push action, since there are multiple areas."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
arm_mode = (
|
||||||
|
alarm_entity._revert_state
|
||||||
|
if alarm_entity._revert_state in const.ARM_MODES
|
||||||
|
else alarm_entity._arm_mode
|
||||||
|
)
|
||||||
|
res = re.search(r"^ALARMO_ARM_", action)
|
||||||
|
if res:
|
||||||
|
arm_mode = action.replace("ALARMO_", "").lower().replace("arm", "armed")
|
||||||
|
if not arm_mode:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Cannot process the push action, since the arm mode is not known."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if action == const.EVENT_ACTION_FORCE_ARM:
|
||||||
|
_LOGGER.info("Received request for force arming")
|
||||||
|
await alarm_entity.async_handle_arm_request(
|
||||||
|
arm_mode, skip_code=True, bypass_open_sensors=True
|
||||||
|
)
|
||||||
|
elif action == const.EVENT_ACTION_RETRY_ARM:
|
||||||
|
_LOGGER.info("Received request for retry arming")
|
||||||
|
await alarm_entity.async_handle_arm_request(arm_mode, skip_code=True)
|
||||||
|
elif action == const.EVENT_ACTION_DISARM:
|
||||||
|
_LOGGER.info("Received request for disarming")
|
||||||
|
await alarm_entity.async_alarm_disarm(None, skip_code=True)
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Received request for arming with mode %s",
|
||||||
|
arm_mode,
|
||||||
|
)
|
||||||
|
await alarm_entity.async_handle_arm_request(arm_mode, skip_code=True)
|
||||||
|
|
||||||
|
self._subscriptions.append(
|
||||||
|
self.hass.bus.async_listen(const.PUSH_EVENT, async_handle_push_event)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_remove_entity(self, area_id: str):
|
||||||
|
"""Remove an alarm_control_panel entity."""
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
if area_id == "master":
|
||||||
|
entity = self.hass.data[const.DOMAIN]["master"]
|
||||||
|
entity_registry.async_remove(entity.entity_id)
|
||||||
|
self.hass.data[const.DOMAIN]["master"] = None
|
||||||
|
else:
|
||||||
|
entity = self.hass.data[const.DOMAIN]["areas"][area_id]
|
||||||
|
entity_registry.async_remove(entity.entity_id)
|
||||||
|
self.hass.data[const.DOMAIN]["areas"].pop(area_id, None)
|
||||||
|
|
||||||
|
def async_get_sensor_groups(self):
|
||||||
|
"""Fetch a list of sensor groups (websocket API hook)."""
|
||||||
|
groups = self.store.async_get_sensor_groups()
|
||||||
|
return list(groups.values())
|
||||||
|
|
||||||
|
def async_get_group_for_sensor(self, entity_id: str):
|
||||||
|
"""Fetch the group ID for a given sensor."""
|
||||||
|
groups = self.async_get_sensor_groups()
|
||||||
|
result = next((el for el in groups if entity_id in el[ATTR_ENTITIES]), None)
|
||||||
|
return result["group_id"] if result else None
|
||||||
|
|
||||||
|
def assign_sensor_to_group(self, entity_id: str, group_id: str):
|
||||||
|
"""Assign a sensor to a group."""
|
||||||
|
updated = False
|
||||||
|
old_group = self.async_get_group_for_sensor(entity_id)
|
||||||
|
if old_group and group_id != old_group:
|
||||||
|
# remove sensor from group
|
||||||
|
el = self.store.async_get_sensor_group(old_group)
|
||||||
|
if len(el[ATTR_ENTITIES]) > 2:
|
||||||
|
self.store.async_update_sensor_group(
|
||||||
|
old_group,
|
||||||
|
{ATTR_ENTITIES: [x for x in el[ATTR_ENTITIES] if x != entity_id]},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.store.async_delete_sensor_group(old_group)
|
||||||
|
updated = True
|
||||||
|
if group_id:
|
||||||
|
# add sensor to group
|
||||||
|
group = self.store.async_get_sensor_group(group_id)
|
||||||
|
if not group:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Failed to assign entity %s to group %s",
|
||||||
|
entity_id,
|
||||||
|
group_id,
|
||||||
|
)
|
||||||
|
elif entity_id not in group[ATTR_ENTITIES]:
|
||||||
|
self.store.async_update_sensor_group(
|
||||||
|
group_id, {ATTR_ENTITIES: group[ATTR_ENTITIES] + [entity_id]}
|
||||||
|
)
|
||||||
|
updated = True
|
||||||
|
if updated:
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_sensors_updated")
|
||||||
|
|
||||||
|
def async_update_sensor_group_config(
|
||||||
|
self,
|
||||||
|
group_id: str | None = None,
|
||||||
|
data: dict = {},
|
||||||
|
):
|
||||||
|
"""Update sensor group configuration."""
|
||||||
|
if const.ATTR_REMOVE in data:
|
||||||
|
self.store.async_delete_sensor_group(group_id)
|
||||||
|
elif not group_id:
|
||||||
|
self.store.async_create_sensor_group(data)
|
||||||
|
else:
|
||||||
|
self.store.async_update_sensor_group(group_id, data)
|
||||||
|
|
||||||
|
async_dispatcher_send(self.hass, "alarmo_sensors_updated")
|
||||||
|
|
||||||
|
async def async_unload(self):
|
||||||
|
"""Remove all alarmo objects."""
|
||||||
|
# remove alarm_control_panel entities
|
||||||
|
areas = list(self.hass.data[const.DOMAIN]["areas"].keys())
|
||||||
|
for area in areas:
|
||||||
|
await self.async_remove_entity(area)
|
||||||
|
if self.hass.data[const.DOMAIN]["master"]:
|
||||||
|
await self.async_remove_entity("master")
|
||||||
|
|
||||||
|
del self.hass.data[const.DOMAIN]["sensor_handler"]
|
||||||
|
del self.hass.data[const.DOMAIN]["automation_handler"]
|
||||||
|
del self.hass.data[const.DOMAIN]["mqtt_handler"]
|
||||||
|
del self.hass.data[const.DOMAIN]["event_handler"]
|
||||||
|
|
||||||
|
# remove subscriptions for coordinator
|
||||||
|
while len(self._subscriptions):
|
||||||
|
self._subscriptions.pop()()
|
||||||
|
|
||||||
|
async def async_delete_config(self):
|
||||||
|
"""Wipe alarmo storage."""
|
||||||
|
await self.store.async_delete()
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def register_services(hass):
|
||||||
|
"""Register services used by alarmo component."""
|
||||||
|
coordinator = hass.data[const.DOMAIN]["coordinator"]
|
||||||
|
|
||||||
|
async def async_srv_toggle_user(call):
|
||||||
|
"""Enable a user by service call."""
|
||||||
|
name = call.data.get(ATTR_NAME)
|
||||||
|
enable = True if call.service == const.SERVICE_ENABLE_USER else False
|
||||||
|
users = coordinator.store.async_get_users()
|
||||||
|
user = next(
|
||||||
|
(item for item in list(users.values()) if item[ATTR_NAME] == name), None
|
||||||
|
)
|
||||||
|
if user is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Failed to %s user, no match for name '%s'",
|
||||||
|
"enable" if enable else "disable",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
coordinator.store.async_update_user(
|
||||||
|
user[const.ATTR_USER_ID], {const.ATTR_ENABLED: enable}
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"User user '%s' was %s", name, "enabled" if enable else "disabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
async_register_admin_service(
|
||||||
|
hass,
|
||||||
|
const.DOMAIN,
|
||||||
|
const.SERVICE_ENABLE_USER,
|
||||||
|
async_srv_toggle_user,
|
||||||
|
schema=const.SERVICE_TOGGLE_USER_SCHEMA,
|
||||||
|
)
|
||||||
|
async_register_admin_service(
|
||||||
|
hass,
|
||||||
|
const.DOMAIN,
|
||||||
|
const.SERVICE_DISABLE_USER,
|
||||||
|
async_srv_toggle_user,
|
||||||
|
schema=const.SERVICE_TOGGLE_USER_SCHEMA,
|
||||||
|
)
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
"""Automations."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.core import (
|
||||||
|
HomeAssistant,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_TYPE,
|
||||||
|
ATTR_SERVICE,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_SERVICE_DATA,
|
||||||
|
)
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.template import Template, is_template_string
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.translation import async_get_translations
|
||||||
|
from homeassistant.components.binary_sensor.device_condition import (
|
||||||
|
ENTITY_CONDITIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
from .helpers import (
|
||||||
|
friendly_name_for_entity_id,
|
||||||
|
)
|
||||||
|
from .sensors import (
|
||||||
|
STATE_OPEN,
|
||||||
|
STATE_CLOSED,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
from .alarm_control_panel import AlarmoBaseEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EVENT_ARM_FAILURE = "arm_failure"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_area(trigger, area_id, hass):
|
||||||
|
"""Validate area for trigger."""
|
||||||
|
if const.ATTR_AREA not in trigger:
|
||||||
|
return False
|
||||||
|
elif trigger[const.ATTR_AREA]:
|
||||||
|
return trigger[const.ATTR_AREA] == area_id
|
||||||
|
elif len(hass.data[const.DOMAIN]["areas"]) == 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return area_id is None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_modes(trigger, mode):
|
||||||
|
"""Validate modes for trigger."""
|
||||||
|
if const.ATTR_MODES not in trigger:
|
||||||
|
return False
|
||||||
|
elif not trigger[const.ATTR_MODES]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return mode in trigger[const.ATTR_MODES]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_trigger(trigger, to_state, from_state=None):
|
||||||
|
"""Validate trigger condition."""
|
||||||
|
if const.ATTR_EVENT not in trigger:
|
||||||
|
return False
|
||||||
|
elif trigger[const.ATTR_EVENT] == "untriggered" and from_state == "triggered":
|
||||||
|
return True
|
||||||
|
elif trigger[const.ATTR_EVENT] == to_state:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationHandler:
|
||||||
|
"""Handle automations."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant):
|
||||||
|
"""Initialize automation handler."""
|
||||||
|
self.hass = hass
|
||||||
|
self._config = None
|
||||||
|
self._subscriptions = []
|
||||||
|
self._sensorTranslationCache = {}
|
||||||
|
self._alarmTranslationCache = {}
|
||||||
|
self._sensorTranslationLang = None
|
||||||
|
self._alarmTranslationLang = None
|
||||||
|
|
||||||
|
def async_update_config():
|
||||||
|
"""Automation config updated, reload the configuration."""
|
||||||
|
self._config = self.hass.data[const.DOMAIN][
|
||||||
|
"coordinator"
|
||||||
|
].store.async_get_automations()
|
||||||
|
|
||||||
|
self._subscriptions.append(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass, "alarmo_automations_updated", async_update_config
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async_update_config()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def async_alarm_state_changed(
|
||||||
|
area_id: str, old_state: str, new_state: str
|
||||||
|
):
|
||||||
|
if not old_state:
|
||||||
|
# ignore automations at startup/restoring
|
||||||
|
return
|
||||||
|
|
||||||
|
if area_id:
|
||||||
|
alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
|
||||||
|
else:
|
||||||
|
alarm_entity = self.hass.data[const.DOMAIN]["master"]
|
||||||
|
|
||||||
|
if not alarm_entity:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"state of %s is updated from %s to %s",
|
||||||
|
alarm_entity.entity_id,
|
||||||
|
old_state,
|
||||||
|
new_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_state in const.ARM_MODES:
|
||||||
|
# we don't distinguish between armed modes for automations
|
||||||
|
# they are handled separately
|
||||||
|
new_state = "armed"
|
||||||
|
|
||||||
|
for automation_id, config in self._config.items():
|
||||||
|
if not config[const.ATTR_ENABLED]:
|
||||||
|
continue
|
||||||
|
for trigger in config[const.ATTR_TRIGGERS]:
|
||||||
|
if (
|
||||||
|
validate_area(trigger, area_id, self.hass)
|
||||||
|
and validate_modes(trigger, alarm_entity._arm_mode)
|
||||||
|
and validate_trigger(trigger, new_state, old_state)
|
||||||
|
):
|
||||||
|
await self.async_execute_automation(automation_id, alarm_entity)
|
||||||
|
|
||||||
|
self._subscriptions.append(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, "alarmo_state_updated", async_alarm_state_changed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def async_handle_event(event: str, area_id: str, args: dict = {}):
|
||||||
|
if event != const.EVENT_FAILED_TO_ARM:
|
||||||
|
return
|
||||||
|
if area_id:
|
||||||
|
alarm_entity = self.hass.data[const.DOMAIN]["areas"][area_id]
|
||||||
|
else:
|
||||||
|
alarm_entity = self.hass.data[const.DOMAIN]["master"]
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s has failed to arm",
|
||||||
|
alarm_entity.entity_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for automation_id, config in self._config.items():
|
||||||
|
if not config[const.ATTR_ENABLED]:
|
||||||
|
continue
|
||||||
|
for trigger in config[const.ATTR_TRIGGERS]:
|
||||||
|
if (
|
||||||
|
validate_area(trigger, area_id, self.hass)
|
||||||
|
and validate_modes(trigger, alarm_entity._arm_mode)
|
||||||
|
and validate_trigger(trigger, EVENT_ARM_FAILURE)
|
||||||
|
):
|
||||||
|
await self.async_execute_automation(automation_id, alarm_entity)
|
||||||
|
|
||||||
|
self._subscriptions.append(
|
||||||
|
async_dispatcher_connect(self.hass, "alarmo_event", async_handle_event)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Prepare for removal."""
|
||||||
|
while len(self._subscriptions):
|
||||||
|
self._subscriptions.pop()()
|
||||||
|
|
||||||
|
async def async_execute_automation(
|
||||||
|
self, automation_id: str, alarm_entity: AlarmoBaseEntity
|
||||||
|
):
|
||||||
|
"""Execute the specified automation."""
|
||||||
|
# automation is a dict of AutomationEntry
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Executing automation %s",
|
||||||
|
automation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = self._config[automation_id][const.ATTR_ACTIONS]
|
||||||
|
for action in actions:
|
||||||
|
try:
|
||||||
|
service_data = copy.copy(action[CONF_SERVICE_DATA])
|
||||||
|
|
||||||
|
if action.get(ATTR_ENTITY_ID):
|
||||||
|
service_data[ATTR_ENTITY_ID] = action[ATTR_ENTITY_ID]
|
||||||
|
|
||||||
|
if self._config[automation_id][CONF_TYPE] == const.ATTR_NOTIFICATION:
|
||||||
|
# recursively process all service_data (supports deep nesting)
|
||||||
|
service_data = await self._process_service_data(
|
||||||
|
service_data,
|
||||||
|
alarm_entity,
|
||||||
|
)
|
||||||
|
|
||||||
|
domain, service = action[ATTR_SERVICE].split(".")
|
||||||
|
|
||||||
|
await self.hass.async_create_task(
|
||||||
|
self.hass.services.async_call(
|
||||||
|
domain,
|
||||||
|
service,
|
||||||
|
service_data,
|
||||||
|
blocking=False,
|
||||||
|
context={},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except HomeAssistantError as e:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Execution of action %s failed, reason: %s",
|
||||||
|
automation_id,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _process_service_data(
|
||||||
|
self, obj, alarm_entity, depth: int = 0, max_depth: int = 10
|
||||||
|
):
|
||||||
|
"""Recursively process service_data replacing wildcards and templates safely."""
|
||||||
|
if depth > max_depth:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# Handle strings
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return await self.replace_wildcards_in_string(obj, alarm_entity)
|
||||||
|
|
||||||
|
# Handle dictionaries
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
processed = {}
|
||||||
|
for k, v in obj.items():
|
||||||
|
processed[k] = await self._process_service_data(
|
||||||
|
v,
|
||||||
|
alarm_entity,
|
||||||
|
depth + 1,
|
||||||
|
max_depth,
|
||||||
|
)
|
||||||
|
return processed
|
||||||
|
|
||||||
|
# Handle lists
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [
|
||||||
|
await self._process_service_data(
|
||||||
|
item,
|
||||||
|
alarm_entity,
|
||||||
|
depth + 1,
|
||||||
|
max_depth,
|
||||||
|
)
|
||||||
|
for item in obj
|
||||||
|
]
|
||||||
|
# Other types (int, float, bool, None, etc.)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_automations_by_area(self, area_id: str):
|
||||||
|
"""Get automations for specified area."""
|
||||||
|
result = []
|
||||||
|
for automation_id, config in self._config.items():
|
||||||
|
if any(
|
||||||
|
el[const.ATTR_AREA] == area_id for el in config[const.ATTR_TRIGGERS]
|
||||||
|
):
|
||||||
|
result.append(automation_id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def replace_wildcards_in_string(
|
||||||
|
self, input: str, alarm_entity: AlarmoBaseEntity
|
||||||
|
):
|
||||||
|
"""Look for wildcards in string and replace them with content."""
|
||||||
|
# process wildcard '{{open_sensors}}'
|
||||||
|
res = re.search(r"{{open_sensors(\|lang=([^}]+))?(\|format=short)?}}", input)
|
||||||
|
if res:
|
||||||
|
lang = res.group(2) if res.group(2) else "en"
|
||||||
|
names_only = True if res.group(3) else False
|
||||||
|
|
||||||
|
open_sensors = ""
|
||||||
|
if alarm_entity.open_sensors:
|
||||||
|
parts = []
|
||||||
|
for entity_id, status in alarm_entity.open_sensors.items():
|
||||||
|
if names_only:
|
||||||
|
parts.append(friendly_name_for_entity_id(entity_id, self.hass))
|
||||||
|
else:
|
||||||
|
parts.append(
|
||||||
|
await self.async_get_open_sensor_string(
|
||||||
|
entity_id, status, lang
|
||||||
|
)
|
||||||
|
)
|
||||||
|
open_sensors = ", ".join(parts)
|
||||||
|
input = input.replace(res.group(0), open_sensors)
|
||||||
|
|
||||||
|
# process wildcard '{{bypassed_sensors}}'
|
||||||
|
if "{{bypassed_sensors}}" in input:
|
||||||
|
bypassed_sensors = ""
|
||||||
|
if alarm_entity.bypassed_sensors and len(alarm_entity.bypassed_sensors):
|
||||||
|
parts = []
|
||||||
|
for entity_id in alarm_entity.bypassed_sensors:
|
||||||
|
name = friendly_name_for_entity_id(entity_id, self.hass)
|
||||||
|
parts.append(name)
|
||||||
|
bypassed_sensors = ", ".join(parts)
|
||||||
|
input = input.replace("{{bypassed_sensors}}", bypassed_sensors)
|
||||||
|
|
||||||
|
# process wildcard '{{arm_mode}}'
|
||||||
|
res = re.search(r"{{arm_mode(\|lang=([^}]+))?}}", input)
|
||||||
|
if res:
|
||||||
|
lang = res.group(2) if res.group(2) else "en"
|
||||||
|
arm_mode = await self.async_get_arm_mode_string(alarm_entity.arm_mode, lang)
|
||||||
|
|
||||||
|
input = input.replace(res.group(0), arm_mode)
|
||||||
|
|
||||||
|
# process wildcard '{{changed_by}}'
|
||||||
|
if "{{changed_by}}" in input:
|
||||||
|
changed_by = alarm_entity.changed_by if alarm_entity.changed_by else ""
|
||||||
|
input = input.replace("{{changed_by}}", changed_by)
|
||||||
|
|
||||||
|
# process wildcard '{{delay}}'
|
||||||
|
if "{{delay}}" in input:
|
||||||
|
delay = str(alarm_entity.delay) if alarm_entity.delay else ""
|
||||||
|
input = input.replace("{{delay}}", delay)
|
||||||
|
|
||||||
|
# process HA templates
|
||||||
|
if is_template_string(input):
|
||||||
|
input = Template(input, self.hass).async_render()
|
||||||
|
_LOGGER.debug("Processed HA template, result:(%s) %s", type(input), input)
|
||||||
|
|
||||||
|
return input
|
||||||
|
|
||||||
|
async def async_get_open_sensor_string(
|
||||||
|
self, entity_id: str, state: str, language: str
|
||||||
|
):
|
||||||
|
"""Get translation for sensor states."""
|
||||||
|
if self._sensorTranslationCache and self._sensorTranslationLang == language:
|
||||||
|
translations = self._sensorTranslationCache
|
||||||
|
else:
|
||||||
|
translations = await async_get_translations(
|
||||||
|
self.hass, language, "device_automation", ["binary_sensor"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self._sensorTranslationCache = translations
|
||||||
|
self._sensorTranslationLang = language
|
||||||
|
|
||||||
|
entity = self.hass.states.get(entity_id)
|
||||||
|
|
||||||
|
device_type = (
|
||||||
|
entity.attributes["device_class"]
|
||||||
|
if entity and "device_class" in entity.attributes
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if state == STATE_OPEN:
|
||||||
|
translation_key = (
|
||||||
|
f"component.binary_sensor.device_automation.condition_type.{ENTITY_CONDITIONS[device_type][0]['type']}"
|
||||||
|
if device_type in ENTITY_CONDITIONS
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if translation_key and translation_key in translations:
|
||||||
|
string = translations[translation_key]
|
||||||
|
else:
|
||||||
|
string = "{entity_name} is open"
|
||||||
|
elif state == STATE_CLOSED:
|
||||||
|
translation_key = (
|
||||||
|
f"component.binary_sensor.device_automation.condition_type.{ENTITY_CONDITIONS[device_type][1]['type']}"
|
||||||
|
if device_type in ENTITY_CONDITIONS
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if translation_key and translation_key in translations:
|
||||||
|
string = translations[translation_key]
|
||||||
|
else:
|
||||||
|
string = "{entity_name} is closed"
|
||||||
|
|
||||||
|
elif state == STATE_UNAVAILABLE:
|
||||||
|
string = "{entity_name} is unavailable"
|
||||||
|
|
||||||
|
else:
|
||||||
|
string = "{entity_name} is unknown"
|
||||||
|
|
||||||
|
name = friendly_name_for_entity_id(entity_id, self.hass)
|
||||||
|
string = string.replace("{entity_name}", name)
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
async def async_get_arm_mode_string(self, arm_mode: str, language: str):
|
||||||
|
"""Get translation for alarm arm mode."""
|
||||||
|
if self._alarmTranslationCache and self._alarmTranslationLang == language:
|
||||||
|
translations = self._alarmTranslationCache
|
||||||
|
else:
|
||||||
|
translations = await async_get_translations(
|
||||||
|
self.hass, language, "entity_component", ["alarm_control_panel"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self._alarmTranslationCache = translations
|
||||||
|
self._alarmTranslationLang = language
|
||||||
|
|
||||||
|
translation_key = (
|
||||||
|
f"component.alarm_control_panel.entity_component._.state.{arm_mode}"
|
||||||
|
if arm_mode
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if translation_key and translation_key in translations:
|
||||||
|
return translations[translation_key]
|
||||||
|
elif arm_mode:
|
||||||
|
return " ".join(w.capitalize() for w in arm_mode.split("_"))
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""WebSocket handler and registration for Alarmo card update events."""
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.components.websocket_api import decorators, async_register_command
|
||||||
|
|
||||||
|
|
||||||
|
@decorators.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "alarmo_updated",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@decorators.async_response
|
||||||
|
async def handle_subscribe_updates(hass, connection, msg):
|
||||||
|
"""Handle subscribe updates."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def handle_event(event: str, area_id: str, args: dict = {}):
|
||||||
|
"""Forward events to websocket."""
|
||||||
|
data = dict(**args, **{"event": event, "area_id": area_id})
|
||||||
|
connection.send_message(
|
||||||
|
{"id": msg["id"], "type": "event", "event": {"data": data}}
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.subscriptions[msg["id"]] = async_dispatcher_connect(
|
||||||
|
hass, "alarmo_event", handle_event
|
||||||
|
)
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
async def async_register_card(hass):
|
||||||
|
"""Publish event to lovelace when alarm changes."""
|
||||||
|
async_register_command(hass, handle_subscribe_updates)
|
||||||