Nerwyn / service-call-tile-feature

Home Assistant custom features for tile cards and more. Call any action using buttons, sliders, selectors, and spinboxes
Apache License 2.0
96 stars 3 forks source link

Add templates for properties #6

Closed ncrmnt closed 10 months ago

ncrmnt commented 11 months ago

It'd be nice to set some properties like icon, color, etc depends on a target object state.

For example: I have a tile card for my bedside lights. It has some additional features (buttons) for setting light brightness or color. And one button to swich the light alarm function:

  - service: switch.toggle
    icon: mdi:alarm
    target: switch.bedroom_light_alarm
    confirmation: true

It works but I can't determine the current state of this switch. Plugins like button-card and some other allows to set templates for properties. And my config with the same ability would looks like:

  - service: switch.toggle
    icon: >-
      [[[ return `${(TARGETENTITY.state == "on" ? "mdi:alarm" : "mdi:alarm-off")}` ]]]
    target: switch.bedroom_light_alarm
    confirmation: true

Thank you for this awesome project!

Nerwyn commented 10 months ago

Going to use this card to keep notes since I've been to asked to add templating in 5-10 requests across two projects and it's a lot more complicated than I initially thought.

button-card uses Thomas Loven's lovelace-card-tools with a lot of additional logic. card-tools hasn't been updated since 2020 and doesn't work well with TypeScript, so I gave up on trying to implement it.

card-mod has a newer implementation of template processing but it's wrapped up in other card-mod logic and also confusing (I am no where near the front end dev that Thomas Loven is, I'm a backend engineer by trade) so I don't think I should go that route either.

I'm going to work on a "reinvent the wheel" approach where instead of having Home Assistant process the template as is I'll reimplement a limited set of Home Assistant jinja2 templating, as defined on the Home Assistant templating page.

Edit 1: So far I have the basic functions working (states, is_state, state_attr, is_state_attr, has_value) but if else statements will be harder. And this is all being processed by JS in the frontend rather than Python in the backend so some of the surrounding syntax is different which makes things harder for end users.

Edit 2: Actually if else statements work with my code as is if they're JS syntax if else statements or ternary operators. Happy little accident.

Nerwyn commented 10 months ago

Note to self: try using nunjucks instead of reinventing the wheel.

Nerwyn commented 10 months ago

Implementing nunjucks was a huge success! I've ripped out the DIY template evaluating system and the string interpolation system (it's gonna be a big breaking change come v3.0.0). The latter can be completely replaced by nunjucks, which is functionally 99% like jinja2.

Nerwyn commented 10 months ago

I've got what I think is a pretty functional HA/jinja2-like template system in the dev builds. Here is an example config:

features:
  - type: custom:service-call
    entries:
      - service: light.toggle
        icon_color: red
        flex_basis: 200%
        icon: >
          {% if is_state("light.chandelier", "on") %} mdi:ceiling-light {% else
          %} mdi:ceiling-light-outline {% endif %}
        color: |
          {% if is_state("light.chandelier", "on") %}
            rgb({{ state_attr("light.chandelier", "rgb_color") }})
          {% endif %}
        label: >-
          {{ (100*state_attr("light.chandelier", "brightness")/255) | round or
          undefined}}
        unit_of_measurement: '%'
      - service: light.toggle
        icon: mdi:lightbulb
        icon_color: orange
        label: Bulb 1
        target:
          entity_id: light.chandelier_bulb_1
      - service: light.toggle
        icon: mdi:lightbulb
        icon_color: yellow
        label: Bulb 2
        target:
          entity_id: light.chandelier_bulb_2
      - service: light.toggle
        icon: mdi:lightbulb
        icon_color: green
        label: Bulb 3
        target:
          entity_id: light.chandelier_bulb_3
      - service: light.toggle
        icon: mdi:lightbulb
        icon_color: blue
        label: Bulb 4
        target:
          entity_id: light.chandelier_bulb_4
      - service: light.toggle
        icon: mdi:lightbulb
        icon_color: purple
        label: Bulb 5
        target:
          entity_id: light.chandelier_bulb_5
  - type: custom:service-call
    entries:
      - type: selector
        entity_id: light.chandelier
        value_attribute: rgb_color
        background_color: rgb(VALUE)
        invert_label: true
        options:
          - service: light.turn_on
            option: 255,0,0
            color: red
            label: Red
            label_color: red
            icon: mdi:alpha-r
            data:
              color_name: red
          - service: light.turn_on
            option: 0,128,0
            color: green
            label: Green
            label_color: green
            icon: mdi:alpha-g
            data:
              color_name: green
          - service: light.turn_on
            option: 0,0,255
            color: blue
            label: Blue
            label_color: blue
            icon: mdi:alpha-b
            data:
              color_name: blue
          - service: light.turn_on
            option: 255,166,86
            color: white
            label: White
            label_color: white
            icon: mdi:alpha-w
            flex_basis: 300%
            invert_icon: true
            data:
              color_temp: 500
  - type: custom:service-call
    entries:
      - type: slider
        color: rgb({{ state_attr("light.chandelier", "rgb_color") or (0, 0, 0) }})
        label: >-
          {{ (100*state_attr("light.chandelier", "brightness")/255) | round or
          undefined}}
        unit_of_measurement: '%'
        value_attribute: brightness
        icon: mdi:brightness-4
        service: light.turn_on
        flex_basis: 200%
        data:
          brightness_pct: VALUE
      - type: slider
        thumb: line
        background_color: linear-gradient(-90deg, rgb(255, 167, 87), rgb(255, 255, 251))
        background_opacity: 1
        value_attribute: color_temp
        service: light.turn_on
        label: '{{ state_attr("light.chandelier", "color_temp") }}'
        unit_of_measurement: ' Mireds'
        label_color: var(--disabled-color)
        icon: mdi:thermometer
        range:
          - 153
          - 371
        step: 1
        data:
          color_temp: VALUE
type: tile
entity: light.chandelier
Nerwyn commented 10 months ago

Implementing all of the custom HA template functions is going to be a pain or not possible, especially since a lot of them likely wouldn't be used for a card like this. I think I'm going to opt to not implement them but was able to expose the js eval function to nunjucks.

While nunjucks warns against allowing for user defined templates, the whole purpose of this is to allow for user defined templates in the HA frontend. So similarly I'm going to use the same logic to allow end users access to the eval function to run whatever JS they want in templates. Generally both user defined nunjucks templates and the JS eval function are extremely bad practice but since Home Assistant servers should be private and not publicly exposed it's up to end users to not abuse or allow for this to be abused on their Home Assistant servers.

Note to self - put warnings about user defined templates and eval function in README.

Edit: eval is probably too dangerous to expose to end users, so I'm removing it.

Nerwyn commented 10 months ago

Released version 3.0.0, which adds templating thanks to Nunjucks.

ncrmnt commented 10 months ago

Thnks a lot! It's a very useful feature.