Skip to content

Commit 9125c26

Browse files
committed
Add dynamic dropdown and selector options from attributes and templates
Let a dropdown or selector generate its options from a list instead of requiring every option to be hand-written, creating one option per item. Implements Discussion Nerwyn#137 ("Automatically populate dropdown options from effects / entity attribute lists"). Builds on the per-option config.option template variable from Nerwyn#198. Sources (all backwards compatible with the existing explicit list): - options_attribute / options_entity: read the list straight from an entity attribute, e.g. a light's effect_list, with no template. The same can be written as an `options: { attribute, entity_id }` object. select/input_select default to their `options` attribute. - options as a template string: render to a list for computed sources. Parsed from comma/newline strings or JSON/YAML arrays (including nunjucks `| dump`, HTML-escaped) and YAML block sequences; items may be {value,label,icon} objects (value falls back to name/friendly_name/title). - option_template: per-item label/icon/action applied to every generated option, inheriting the parent entity; each item's value is exposed as the `option` variable (alias of config.option). - Zero-config default actions: options generated from a recognized list attribute get a sensible default action (effect_list -> light.turn_on, source_list -> select_source, hvac_modes/preset_modes/... -> the matching service, options -> select_option), so they work without an option template. - Resolved lists are cached on a source signature (including the option template) so long lists stay cheap on frequent state updates, and rebuild when the source or template changes. - Selected option label/icon in the dropdown window render in the option's own context (also fixes the blank selected label from Nerwyn#198). - Editor: an "Options source" picker (manual / entity attribute / template). The source attribute picker only lists multi-item list attributes, the option template has its own focused editor (appearance + action only), and it is pre-filled with a label and the default action for the source. - Docs: Dropdowns/Selectors, a Dynamic Options section, and worked examples (Example 9 light effect_list, 10 media_player source_list, 11 climate hvac_modes selector). - Tests: parseOptionsList / buildTemplatedOption / resolveOptionsAttribute / defaultOptionAction (node:test + tsx). Ref Nerwyn#137
1 parent 83cb14f commit 9125c26

12 files changed

Lines changed: 1817 additions & 49 deletions

README.md

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ You can override the default behavior of each option by changing their action. T
5353

5454
You can also choose to not give any of the dropdown options `Option` values, so that none are ever marked as the selected option. This makes it so that the default dropdown icon and label are always displayed, and the dropdown feature becomes more of a menu for firing different actions rather than one for selecting an option.
5555

56+
Instead of writing out every option by hand, you can also generate them from an entity attribute that contains a list (such as a light's `effect_list`) or from a template. This is useful for entities with long, dynamic attribute lists. See [Dynamic Options](#dynamic-options) below and [Example 9](#example-9) for a worked example.
57+
5658
## Inputs
5759

5860
<img src="https://raw.githubusercontent.com/Nerwyn/custom-card-features/main/assets/inputs_tile.png" width="600"/>
@@ -73,6 +75,8 @@ Like dropdowns, this feature works best with Home Assistant `select/input_select
7375

7476
Since each selector option is a custom feature button, you can override its default behavior by changing its tap action. The `Option` field will be the value to compare against the feature's value, whether that is its entity's state or one of its attributes. If they match and are not undefined, then the the option will be highlighted. The option highlight color defaults to the parent card color (usually the tile card color), but can be changed by setting the CSS attribute `--color` to a different value, either for the entire feature or an individual option.
7577

78+
Like dropdowns, selectors can also generate their options from an entity attribute list or a template. See [Dynamic Options](#dynamic-options) below and [Example 11](#example-11) for a worked example.
79+
7680
## Sliders
7781

7882
<img src="https://raw.githubusercontent.com/Nerwyn/custom-card-features/main/assets/sliders_tile.png" width="600"/>
@@ -166,6 +170,98 @@ Like sliders and spinboxes, selectors have a one second delay before updating th
166170

167171
Dropdowns can also be assigned their own default icon and label. When no option is selected, the default icon and label are used.
168172

173+
#### Dynamic Options
174+
175+
Instead of writing out each option by hand, dropdowns and selectors can generate their options from a list, automatically creating one option per item. This is ideal for entities with long, dynamic attribute lists like a light's `effect_list`, a media player's `source_list`, or a `select`/`input_select` entity's `options`. In the configuration UI, set **Options source** to `Entity attribute` or `Template` in the dropdown or selector general options. There are three sources:
176+
177+
##### From an entity attribute
178+
179+
Point the options straight at an entity attribute that contains a list. This requires no template:
180+
181+
```yaml
182+
- type: dropdown
183+
entity_id: light.my_light
184+
value_attribute: effect
185+
options_attribute: effect_list
186+
# options_entity: light.other_light # optional, defaults to entity_id
187+
```
188+
189+
The same can also be written as an object on `options`:
190+
191+
```yaml
192+
options:
193+
attribute: effect_list
194+
# entity_id: light.other_light # optional override
195+
```
196+
197+
For `select` and `input_select` entities you can leave the attribute value blank — it defaults to their `options` attribute. The `options_attribute` key still needs to be present to opt in to attribute sourcing, but its value can be empty, so this is enough:
198+
199+
```yaml
200+
- type: dropdown
201+
entity_id: input_select.scene
202+
options_attribute: # blank value → uses the select's "options" attribute
203+
```
204+
205+
##### From a template
206+
207+
For computed lists (joins, cross-entity lookups, filtering, etc.) point `options` at a template that renders to a list:
208+
209+
```yaml
210+
options: "{{ state_attr('light.my_light', 'effect_list') }}"
211+
```
212+
213+
A template result can be expressed in any of these forms:
214+
215+
- A comma-separated string, which is what the default `{{ state_attr(...) }}` output produces (e.g. `Solid,Rainbow,Fireworks`).
216+
- A newline-separated string, useful when values themselves contain commas (e.g. `{{ my_list | join('\n') }}`).
217+
- A JSON or YAML array, which preserves values containing commas and supports objects. Use the `dump` filter to emit JSON, e.g. `{{ my_list | dump }}`. List items may be objects of the form `{ value, label, icon }` (the keys `option`, `id`, `name`, `friendly_name`, and `title` are also accepted as aliases), letting you show a friendly label while selecting a different underlying value:
218+
219+
```yaml
220+
options: >-
221+
{{ [{'value': 'src1', 'label': 'Spotify', 'icon': 'mdi:spotify'},
222+
{'value': 'src2', 'label': 'Radio'}] | dump }}
223+
```
224+
225+
A template expression must be wrapped in `{{ }}` like all other templates, e.g. `options: "{{ state_attr('light.x', 'effect_list') }}"`. A plain string with no template expression is instead used verbatim as a literal comma-separated list, so `options: "Cool, Warm, Hot"` also works. Attribute sources can use the same object item form as templates.
226+
227+
##### Option template
228+
229+
For both dynamic sources, an **Option template** is applied to every generated option (with its own label, icon, action, and styles). Each item's value is exposed to the option template's fields as the `option` template variable (also available as `config.option`), so you wire up the label and action once:
230+
231+
```yaml
232+
option_template:
233+
label: '{{ option }}'
234+
tap_action:
235+
action: perform-action
236+
perform_action: light.turn_on
237+
target:
238+
entity_id: light.my_light
239+
data:
240+
effect: '{{ option }}'
241+
```
242+
243+
Generated options re-render automatically when their source changes, and the resolved list is cached so that frequent state updates remain cheap even for very long lists.
244+
245+
Selected state detection works exactly as it does for manual options: the value of each generated option is compared against the feature's value (its entity's state or attribute).
246+
247+
When you source options from a recognized list attribute, generated options that have no action of their own are given a sensible default action, so they work with no option template at all:
248+
249+
| Source attribute | Default action | Set in data |
250+
| --- | --- | --- |
251+
| `effect_list` | `light.turn_on` | `effect` |
252+
| `source_list` | `media_player.select_source` | `source` |
253+
| `sound_mode_list` | `media_player.select_sound_mode` | `sound_mode` |
254+
| `hvac_modes` | `climate.set_hvac_mode` | `hvac_mode` |
255+
| `fan_modes` | `climate.set_fan_mode` | `fan_mode` |
256+
| `swing_modes` | `climate.set_swing_mode` | `swing_mode` |
257+
| `preset_modes` | `<domain>.set_preset_mode` | `preset_mode` |
258+
| `available_modes` | `humidifier.set_mode` | `mode` |
259+
| `operation_list` | `water_heater.set_operation_mode` | `operation_mode` |
260+
| `fan_speed_list` | `vacuum.set_fan_speed` | `fan_speed` |
261+
| `options` (select/input_select) | `select.select_option` | `option` |
262+
263+
Set the feature's `Attribute` to the value the action sets (e.g. `effect` for `effect_list`, `source` for `source_list`) so the currently selected option is highlighted. To use a different action, set it in the option template — it takes precedence over the default. In the configuration UI the option template is pre-filled with this default action so you can see and tweak it.
264+
169265
### Toggle General Options
170266

171267
<img src="https://raw.githubusercontent.com/Nerwyn/custom-card-features/main/assets/toggle_general_options.png" width="600"/>
@@ -188,7 +284,7 @@ Buttons, dropdowns, selectors, sliders, and toggles have design variants that ca
188284

189285
Almost all fields support nunjucks templating. Nunjucks is a templating engine for JavaScript, which is heavily based on the jinja2 templating engine for Python which Home Assistant uses. While the syntax of nunjucks and jinja2 is almost identical, you may find the [nunjucks documentation](https://mozilla.github.io/nunjucks/templating.html) useful. Most extensions supported by Home Assistant templates are supported by this templating system, but not all and the syntax may vary. Please see the [ha-nunjucks](https://github.com/Nerwyn/ha-nunjucks) repository for a list of available extensions. If you want additional extensions to be added or have templating questions or bugs, please make an issue or discussion on that repository, not this one.
190286

191-
You can include the current value of a feature and its units by using the variables `value` and `unit` in a label template. You can also include `hold_secs` in a template if performing a momentary repeat or end action. For toggles you can use the boolean variable `checked` to check whether the toggle is on or off. Each custom feature can also reference its entry using `config` within templates. `config.entity` and `config.attribute` will return the features entity ID and attribute with their templates rendered (if they have them), and other templated config fields can be rendered within templates by wrapping them in the function `render` within a template. Information about the parent card such as its entity ID, state, and attributes can be accessed using `stateObj`. The structure of `stateObj` can be found [here](https://github.com/home-assistant/home-assistant-js-websocket/blob/1d51737f6092b95e2bc98e85aca752771b97b760/lib/types.ts#L72-L96) as a `HassEntity` type and is listed below.
287+
You can include the current value of a feature and its units by using the variables `value` and `unit` in a label template. You can also include `hold_secs` in a template if performing a momentary repeat or end action. For toggles you can use the boolean variable `checked` to check whether the toggle is on or off. For dropdown and selector options you can use the variable `option` (also available as `config.option`) to reference that option's own value, which is especially useful with [dynamic options](#dynamic-options). Each custom feature can also reference its entry using `config` within templates. `config.entity` and `config.attribute` will return the features entity ID and attribute with their templates rendered (if they have them), and other templated config fields can be rendered within templates by wrapping them in the function `render` within a template. Information about the parent card such as its entity ID, state, and attributes can be accessed using `stateObj`. The structure of `stateObj` matches the [Home Assistant websocket `HassEntity` type definition](https://github.com/home-assistant/home-assistant-js-websocket/blob/1d51737f6092b95e2bc98e85aca752771b97b760/lib/types.ts#L72-L96) and is listed below.
192288

193289
<details>
194290

@@ -2457,3 +2553,113 @@ transparent: true
24572553
```
24582554
24592555
</details>
2556+
2557+
## Example 9
2558+
2559+
Generating dropdown options dynamically from a light's `effect_list` attribute. The dropdown lists every effect the light supports, marks the active one as selected, and tapping an option sets that effect — all without hand writing a single option. If the light's firmware adds or removes effects, the dropdown updates automatically.
2560+
2561+
<details>
2562+
2563+
<summary>Config</summary>
2564+
2565+
```yaml
2566+
type: tile
2567+
entity: light.wled
2568+
features:
2569+
- type: custom:service-call
2570+
entries:
2571+
- type: dropdown
2572+
entity_id: light.wled
2573+
value_attribute: effect
2574+
icon: mdi:string-lights
2575+
label: '{{ state_attr(config.entity, "effect") }}'
2576+
options_attribute: effect_list
2577+
option_template:
2578+
label: '{{ option }}'
2579+
tap_action:
2580+
action: perform-action
2581+
perform_action: light.turn_on
2582+
target:
2583+
entity_id: light.wled
2584+
data:
2585+
effect: '{{ option }}'
2586+
```
2587+
2588+
</details>
2589+
2590+
The same pattern works for any entity with a list attribute, for example a media player's `source_list` (paired with `media_player.select_source`) or a climate entity's `preset_modes`. For `select` and `input_select` entities you can omit both `options_attribute` and `option_template`, since the `options` attribute is used and a `select_option` action is generated for you:
2591+
2592+
```yaml
2593+
- type: dropdown
2594+
entity_id: input_select.scene
2595+
options_attribute:
2596+
```
2597+
2598+
If you need a computed list instead of a single attribute, point `options` at a template that renders to a list, for example to merge two lists or filter them:
2599+
2600+
```yaml
2601+
options: "{{ (state_attr(config.entity, 'effect_list') or []) | reject('eq', 'Solid') | list }}"
2602+
```
2603+
2604+
## Example 10
2605+
2606+
Generating a dropdown from a media player's `source_list`, with the active source marked as selected and tapping a source switching to it. A dropdown is a good fit here because source lists can be long.
2607+
2608+
<details>
2609+
2610+
<summary>Config</summary>
2611+
2612+
```yaml
2613+
type: tile
2614+
entity: media_player.living_room
2615+
features:
2616+
- type: custom:service-call
2617+
entries:
2618+
- type: dropdown
2619+
entity_id: media_player.living_room
2620+
value_attribute: source
2621+
icon: mdi:import
2622+
label: '{{ state_attr(config.entity, "source") }}'
2623+
options_attribute: source_list
2624+
option_template:
2625+
label: '{{ option }}'
2626+
tap_action:
2627+
action: perform-action
2628+
perform_action: media_player.select_source
2629+
target:
2630+
entity_id: media_player.living_room
2631+
data:
2632+
source: '{{ option }}'
2633+
```
2634+
2635+
</details>
2636+
2637+
## Example 11
2638+
2639+
Generating a **selector** from a climate entity's `hvac_modes`. Because a selector lays its options out in a single row, attribute lists with only a handful of items (like HVAC modes, fan presets, or vacuum fan speeds) work best; use a dropdown for longer lists. The climate state is the active HVAC mode, so the matching option is highlighted automatically.
2640+
2641+
<details>
2642+
2643+
<summary>Config</summary>
2644+
2645+
```yaml
2646+
type: tile
2647+
entity: climate.living_room
2648+
features:
2649+
- type: custom:service-call
2650+
entries:
2651+
- type: selector
2652+
entity_id: climate.living_room
2653+
options_attribute: hvac_modes
2654+
option_template:
2655+
label: '{{ option | replace("_", " ") | capitalize }}'
2656+
tap_action:
2657+
action: perform-action
2658+
perform_action: climate.set_hvac_mode
2659+
target:
2660+
entity_id: climate.living_room
2661+
data:
2662+
hvac_mode: '{{ option }}'
2663+
```
2664+
2665+
</details>

dist/custom-card-features.min.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)