button-card icon indicating copy to clipboard operation
button-card copied to clipboard

Create reusable javascript code

Open adabelleleiram opened this issue 3 years ago • 9 comments

Is your feature request related to a problem? Please describe. I would like to be able to reuse some code in multiple places. Right now I'm using a lot of different conditions to decide what color to use for my lights (e.g. is it dark mode, does the bulb have color/temperature support, is the color too close to white). I'm using this logic in three different places to color three different elements so it ends up being a lot of copy-paste code.

Describe the solution you'd like Some way to reuse the code/conditions, either by being able to save it in a variable or by being able to call a custom Javascript function.

adabelleleiram avatar May 09 '22 15:05 adabelleleiram

did you check the button card templates? thats exactly what you can do with templates, reuse code, any code really. for some real life examples, check my templates file

Mariusthvdb avatar May 09 '22 21:05 Mariusthvdb

did you check the button card templates? thats exactly what you can do with templates, reuse code, any code really.

Yes! But as far as I understood it was if you were reusing elements rather than the JavaScript code itself. In my case, I'm using the same code to choose the colour of the card's icon and colour as well as the background color of another card. As far as I can tell, I can't use the templates to write that code in one place as it is being used for different elements of the cards...?

adabelleleiram avatar May 09 '22 21:05 adabelleleiram

you can ofc use more than 1 template for a button, and use a template per function you need. but maybe I am not grasping what you want, so some code would be useful. not sure f that is allowed here, or we need to move to the community.... as long as no one complains... ;-)

Mariusthvdb avatar May 09 '22 21:05 Mariusthvdb

Ok, I'll see if I can show what I mean a bit more clearly! So I have these room cards with a card inside that controls the lighting (the round icon). I'm working now on how to show them when the lights in the room are RGB lights.

Screenshot 2022-05-10 at 08 59 31

I want the colour of the icon, the light card's background colour and the room card's background colour to reflect the colour of the lights. The problem is, in light mode, if the lights are too close to white they disappear in the UI. So in those cases I want to use the yellow colour you see in this pic.

Screenshot 2022-05-10 at 08 59 42

So I have this block of Javascript code that deduces whether to use the light's colour or yellow:

// Use yellow if the light doesn't have temp/colour
if (entity.attributes.color_mode != "brightness") {

  // Sum of RGB colour to see how close it is to white
  const colorSum = entity.attributes.rgb_color.reduce((partialSum, a) => partialSum + a, 0);
  
  var tooWhite = colorSum/(255*3) > 0.8;
  
  // Use white colour if in dark mode
  if (hass.themes.darkMode == true || tooWhite == false) {
    return "rgba(" + entity.attributes.rgb_color + ", 1)";
  }
}

return "rgba(var(--color-yellow), 1)";

It's relatively a lot of code and right now I have to copy-paste it in the styles for all the elements that I want coloured. Simplified:

custom_room_light:
  state:
  - value: "on"
    styles:
      icon:
        - color: >
            [[[
              // Insert lighting code from above here
            ]]]
      img_cell:
        - background-color: >
            [[[
              // Insert lighting code from above here
            ]]]
custom_room:
  state:
  - value: "on"
    styles:
      card:
        - background-color: >
            [[[
              // Insert lighting code from above here
            ]]]

So basically, whenever I want to tweak the code I wrote above, I have to remember to copy-paste it in all three places. Would be nice to be able to e.g. call a function or somehow reference the Javascript code from all the different areas instead.

Hope this makes more sense.

adabelleleiram avatar May 10 '22 07:05 adabelleleiram

ok, that most certainly makes sense ;-)

you can use yaml anchors for that, if its all in one file. thats exactly what they do...

its like you already did: write the code in the first spot, and precede it with <<: &lighting_code (and dont forget to indent the code below the anchor) and then copy it in the next spot and next, and next, with <<: *lighting_code. Especially powerful I your case, when using the entity. This is probably the easiest way.

if the code is required in more than 1 file and scattered throughout your config, you'd use the button-card-templates, which are in fact system global anchors..... they can be a single line, a more complex js template, or a whole set of button config options, and can nest. It does require some tinkering though, because of the key we need in the template. Have to give that some thought.

btw, a very interesting template you use...! Its one of the issues I didnt get around to yet, having the icon use the light color, except when too white (which practically makes then icon invisible indeed...) so thank you for that solution ;-)

Mariusthvdb avatar May 10 '22 07:05 Mariusthvdb

Oh awesome, thanks so much for the thorough description! I hadn't heard of yaml anchors.

The code is required in more than 1 file, I'm not sure if I follow how you mean I could handle that. Are you suggesting I create a file with a yaml anchor in and then multiple templates that use the anchor? And then reuse those templates in the cards? E.g. icon_light_color, img_cell_light_color, card_background_light_color? And then use those in the different cards?

Thanks btw, feel free to use the code :) I also played around with some brightness and saturation filters to get strong colours even if it was a bit close to white. That worked pretty well :)

adabelleleiram avatar May 10 '22 11:05 adabelleleiram

Are you suggesting I create a file with a yaml anchor in and then multiple templates that use the anchor? And then reuse those templates in the cards? E.g. icon_light_color, img_cell_light_color, card_background_light_color? And then use those in the different cards

yes, thats what I was trying to bring across ;-) and the way to do that (create a file) is in the button-card-templates file, where you can create those separates.

I have been fiddling about with this, and must admit it sounds easier than it is done, mostly because I have many other templates in place, and combining those state: templates can be challenging (because they dont replace, and must merge correctly)

also, the multiline indicator wont allow the anchor to be 1 line lower (I think), so this is an anchor for 'color', and you'd have to create one more for 'background-color'. But that would be it, and you'd call those I any button-card you like, I all of your config. If these would be the only 2, you could even ditch the anchor altogether and use those templates only

its a bit tricky, and I am not 100% certain this works, but try:

with anchor:

light_color:
  state:
    - value: "on"
      styles:
        icon:
          - <<: &light_color
              color: >
                [[[
                  // Use yellow if the light doesn't have temp/colour
                  if (entity.attributes.color_mode != "brightness") {

                    // Sum of RGB colour to see how close it is to white
                    const colorSum = entity.attributes.rgb_color.reduce((partialSum, a) => partialSum + a, 0);

                    var tooWhite = colorSum/(255*3) > 0.8;

                    // Use white colour if in dark mode
                    if (hass.themes.darkMode == true || tooWhite == false) {
                      return "rgba(" + entity.attributes.rgb_color + ", 1)";
                    }
                  }

                  return 'red';
                ]]]

without anchor:

light_color:
  state:
    - value: "on"
      styles:
        icon:
          - color: >
              [[[
                // Use yellow if the light doesn't have temp/colour
                if (entity.attributes.color_mode != "brightness") {

                  // Sum of RGB colour to see how close it is to white
                  const colorSum = entity.attributes.rgb_color.reduce((partialSum, a) => partialSum + a, 0);

                  var tooWhite = colorSum/(255*3) > 0.8;

                  // Use white colour if in dark mode
                  if (hass.themes.darkMode == true || tooWhite == false) {
                    return "rgba(" + entity.attributes.rgb_color + ", 1)";
                  }
                }

                return 'red';
              ]]]

and call that on a light like this:

          - type: custom:button-card
            template:
#              - button_light
              - light_color
            entity: light.kayon
            triggers_update: sensor.kayon_device_power

note I used red because I dont have that variable --color-yellow in place this does change to red when the visibility is not very good... ;

Mariusthvdb avatar May 10 '22 12:05 Mariusthvdb

nvm the above, Ill leave for reference, but here's what does actually work, and allows the multiline to be used, so you can actually only have to write the template once, and repeat it via the anchor:

light_color:
  state:
    - value: "on"
      styles:
        icon:
          - color: &light_color
              >
                [[[
                  // Use yellow if the light doesn't have temp/colour
                  if (entity.attributes.color_mode != "brightness") {

                    // Sum of RGB colour to see how close it is to white
                    const colorSum = entity.attributes.rgb_color.reduce((partialSum, a) => partialSum + a, 0);

                    var tooWhite = colorSum/(255*3) > 0.8;

                    // Use white colour if in dark mode
                    if (hass.themes.darkMode == true || tooWhite == false) {
                      return "rgba(" + entity.attributes.rgb_color + ", 1)";
                    }
                  }

                  return 'red';
                ]]]

and then use that in

custom_room:
  state:
    - value: "on"
      styles:
        card:
          - background-color: *light_color
        img_cell:
          - background-color: *light_color

I think...

once you get the hang of it, you can see other opportunities, like above, where you have - background-color: *light_color twice.

that could be another (nested) anchor ....

custom_room:
  state:
    - value: "on"
      styles:
        card: &background
          - background-color: *light_color
        img_cell: *background

Mariusthvdb avatar May 10 '22 13:05 Mariusthvdb

That's very similar to what I ended up doing. I realised I couldn't use anchors after all as I needed to break out the alpha value. Mine looks like this.

light_color.yaml

light_color_base:
  variables:
    light_color: >
      [[[
        const max_whiteness_factor = 0.95;

        if (entity.attributes.color_mode != "brightness") {
          if (typeof entity.attributes.rgb_color === "undefined") {
            return null;
          }
          const colorSum = entity.attributes.rgb_color.reduce((partialSum, a) => partialSum + a, 0);
          var tooWhite = colorSum/(255*3) > max_whiteness_factor;

          if (hass.themes.darkMode == true || tooWhite == false) {
            return entity.attributes.rgb_color;
          }
        }

        return "var(--color-yellow)";
      ]]]

light_color_img_cell:
  template: light_color_base
  state:
  - value: "on"
    id: light_color
    styles:
      img_cell:
        - background-color: "[[[ return 'rgba(' + variables.light_color + ', 0.2)'; ]]]"

light_color_card_bg:
  template: light_color_base
  state:
  - value: "on"
    id: light_color
    styles:
      card:
        - background-color: >
            [[[
              const alpha = hass.themes.darkMode === true ? 0.2 : 0.1;
              return 'rgba(' + variables.light_color + ', ' + alpha + ')';
            ]]]

light_color_icon:
  template: light_color_base
  state:
  - value: "on"
    id: light_color
    styles:
      icon:
        - color: "[[[ return 'rgba(' + variables.light_color + ', 1)'; ]]]"

I had to return it as a rgb since the alpha values are different for each usage. I tried passing the alpha as a variable but it got overriden if I used more than one of these templates on the same card (e.g. light_color_icon and light_color_img_cell).

The next issue I've run into is that I would like to reuse the const max_whiteness_factor = 0.95; in another Javascript template. You wouldn't happen to have any suggestions?

light_color_base:
  variables:
    light_color: >
      [[[
        // MATCH max_whiteness_factor below
        const max_whiteness_factor = 0.95;
        // MATCH max_whiteness_factor below

       ...

      ]]]

light_color_icon:
  template: light_color_base
  state:
  - value: "on"
    id: light_color
    styles:
      icon:
        - color: "[[[ return 'rgba(' + variables.light_color + ', 1)'; ]]]"
        - filter: > 
            [[[
              // MATCH max_whiteness_factor above
              const max_whiteness_factor = 0.95;
              // MATCH max_whiteness_factor above

             ...

            ]]]

This template is going to adjust the saturation and brightness of the colours depending on how white they are, so I'd like it to match the same value that is used when choosing the yellow colour instead. I tried putting it as a variable in light_color_base but that doesn't work as you can't seem to have dependencies between variables (variables isn't defined). Any suggestions?

adabelleleiram avatar May 11 '22 06:05 adabelleleiram

A quick comment to this issue, which may help others in the same situation. The issue at hand is that you have a block of JavaScript code which is used repeatedly in the same template - or not template. variable definitions are also evaluated even though the yaml is not a template. It seems the variables are evaluated on page refresh or when the sensor updates, which is great.

Here is my example on how to reuse JS code in a custom_button-card:

          - type: custom:button-card
            entity: sensor.my_sensor
            variables:
              some_var: 100
              my_reused_js: |
                [[[
                  // "large" code block to be used below
                  // ...
                  return some_computed_result;
                ]]]
            styles:
              card:
                - background: |
                    [[[
                      // Here the first use of a variable with a constant is used.
                      if(entity.state <= variables.some_var) {
                        return "linear-gradient(90deg, rgba(162,255,162,1) 0%, rgba(90,209,52,1) 47%, rgba(0,255,59,1) 100%)"
                      } else {
                        return "linear-gradient(90deg, rgba(107,13,13,1) 0%, rgba(201,21,25,1) 47%, rgba(255,0,0,1) 100%)"
                      }
                    ]]]
            label: |
              [[[
               // Here the result of the reused code block is used
                const computed_result = variables.my_reused_js;
                return computed_result + " foo";
              ]]]
            name: |
              [[[
               // Here the result of the reused code block is used - again
                const computed_result = variables.my_reused_js;
                return computed_result + " bar";
              ]]]

In my case I use a construction like this to to compute the lowest and highest electricity prices for the next 12 hours including tariffs etc. and display these in a tabular form while coloring the whole card traffic light style with the 'now' status. It helps to know this when to delay stuff like the washing machine or dishwasher to a low-price period.

kpoppel avatar Mar 25 '23 12:03 kpoppel

This last comment is the way to go

RomRider avatar Jul 26 '23 21:07 RomRider