A different take on designing a Lovelace UI

and we have improvements, @Mattias_Persson, this might be something to add to the base repo, it could use some css magic

now it should auto detect any scenes that use any of the lights and add them, if you have a light but you don’t want the scenes to be added then you can add the following on the button card

  variables:
    show_scenes: false

templates

light:
  template:
    - base
    - circle
    - loader
  double_tap_action:
    action: fire-dom-event
    browser_mod:
      service: browser_mod.popup
      data:
        title: >
          [[[
            return !entity || entity.attributes.friendly_name;
          ]]]
        content:
          type: vertical-stack
          cards:
            - type: entities
              card_mod:
                style: |
                  #states {
                    padding-top: 0.5em;
                  }
              entities: >
                [[[
                  if (entity) {
                      let lights = [],
                          id = Boolean(entity.attributes.entity_id)
                              ? [entity.entity_id].concat(entity.attributes.entity_id)
                              : [entity.entity_id];
                      for (let i = 0; i < id.length; i++) {
                          lights.push({
                              "type": "custom:mushroom-light-card",
                              "entity": id[i],
                              "fill_container": false,
                              "primary_info": "name",
                              "secondary_info": "state",
                              "icon_type": "icon",
                              "show_brightness_control": true,
                              "use_light_color": true,
                              "show_color_temp_control": true,
                              "show_color_control": true,
                              "collapsible_controls": true
                          });
                      }

                      return lights;
                  }
                ]]]
            - type: custom:mod-card
              card_mod:
                style:
                  hui-horizontal-stack-card$: |
                    #root {
                      justify-content: space-evenly !important;
                      padding: var(--tablet-popup-button-padding);
                    }
              card:
                type: horizontal-stack
                cards: >
                  [[[
                    if (entity) {
                      let id = Boolean(entity.attributes.entity_id)
                            ? [entity.entity_id].concat(entity.attributes.entity_id)
                            : [entity.entity_id];
                      let foundSenes = Object.keys(states).filter(s => s.indexOf("scene.") > -1).map(s=>states[s]).filter(s=>s.attributes.entity_id.some(e=>id.includes(e))),
                      scenes = [];
                      for (let i = 0; i < foundSenes.length; i++) {
                        scenes.push({"type": "custom:mushroom-chips-card",
                          "alignment": "center",
                          "chips":[
                          {
                            "type": "entity",
                            "entity": foundSenes[i].entity_id,
                            "content_info": "name",
                            "tap_action": {
                              "action": "call-service",
                              "service": "scene.turn_on",
                              "target": {
                                "entity_id": foundSenes[i].entity_id
                              }
                              }
                            }
                          ]
                      });
                      }
                      return variables.show_scenes? scenes: [];
                    }
                  ]]]
  variables:
    show_scenes: true
    circle_input: >
      [[[
        if (entity) {
            // if light group get brightness from child to remove bounce
            let child = entity.attributes.entity_id,
                brightness = child && states[child[0]].attributes.brightness
                    ? Math.round(states[child[0]].attributes.brightness / 2.54)
                    : Math.round(entity.attributes.brightness / 2.54);
            return brightness === 0 && entity.state !== 'off'
                ? 1
                : brightness
        }
      ]]]
    circle_input_unit: '%'

3 Likes

Slick as.

Other than some strange scrollbar behaviour, this is perfect.

On PC, depending on screen layout, entity count (I have 5 including the parent group) and which lights are toggled on/off, both vertical and horizontal scrollbars can appear and disappear in very unpredictable ways.

It’s a very small scroll, and isn’t very noticeable on mobile without deliberately dragging.

try replacing --tablet-popup-button-padding with --tablet-card-content-padding

No luck unfortunately.

I’m not sure if the hui-horizontal-stack-card$ or perhaps mod-card itself is functioning in the card at all, I can’t seem to change or add anything there to any effect.

I can confirm that hui-horizontal-stack-card$ is working, I set it to 1000px and could see the change,

FYI if you change any template file or file added via include to a dashboard, you need to also make a change to ui-lovelace.yaml for the change to take effect, this is a home assistant “issue”.

Just remove a letter, save add the same letter back and then save again. if you dont see this message then your change might not take effect

for me on a Mac, using chrome. I can get vertical scrolling but not horizontal even with 6 scenes and long names on my lights.


Oh yeah, adding 1000px defs has an effect (I’m the worst at using or understanding cardmod haha)

I use

alias: Reload lovelace
sequence:
  - service: browser_mod.javascript
    data:
      code: lovelace_reload()
mode: single

mapped to a keyboard shortcut using hass-agent to deal with the updating!

2 Likes

I don’t have any long names, and also noticed scrolls appearing if the card is present bunt entirely empty.

For both chrome and firefox, I get this sort of behaviour:
scroll

I also tested it without the scene with japanese characters just in case of something funky.

The HA mobile app also seems to have this scroll space behaviour, but is less evident as it doesn’t show scrollbars.

nice!
well in that case, I would recommend you play with the CSS now that you have the auto adding,
Im not a fan of the chips with no background, they feel lots, but I also dont have any scenes

I dont think vertical scrolling can be avoided but I would use css to try and prevent the horizontal scrolling that you are getting.

ones you have tweaked thing id love to see how you are using it.

1 Like

Hi
I have an error in logs : TemplateError('TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'') while processing template 'Template("{{ this.attributes.values() | sum }}")' for attribute '_attr_native_value' in entity 'sensor.template_updates'
This error come from version_updates.yaml, line 51 {{ this.attributes.values() | sum }}
How to fix it ? Did I miss something in my fles ?

Thanks

Hello all,
I have a question about the persons and the last_change sensor.
I would have the quite gladly exchanged against my proximity sensor, so that there displays the current distance instead of the last change.
However, dipping the sensor through the proximity has not helped :confused:

Update: Figured it out. You have to modify the fire-dom-event action in each of your popup yaml files as described here.

Just updated to browser mod 2 today and I noticed I can’t open any popups. I haven’t changed anything in the ui-lovelace.yaml file, but here’s an example of one of my buttons with a tap action to open a popup:

cards:
  - type: button
    icon: mdi:security
    tap_action:
      !include popup/sidebar_security.yaml
    hold_action:
      action: none

Searched through this thread to see if anyone else has already had this issue but couldn’t find anything. Any pointers?

sidebar_security.yaml is a custom popup, browser mod 2 is completely rewritten FAQ

Are all popups broken or just popups that you have written?

Have you migrated your custom popups to the new browser mod 2 syntax? like so

Have you completed the full browser mod install? Quickstart

This doenst work for me.

Have to change config to browser_mod w

action: fire-dom-event
browser_mod:
  service: browser_mod.popup
  data:
    title: Cameras
    style:
      .: |
        :host .main-title {
          pointer-events: none;
        }
      $: |
        .mdc-dialog__surface {
          background: transparent !important;
          border-style: none !important;
          border: 0px !important;
          box-shadow: none;
        }

    card:
      type: vertical-stack
      cards:
        - type: custom:layout-card
          layout_type: grid
          layout:
            grid-gap: 0.4vw
            #grid-template-columns: 860px 860px
            grid-template-rows: 540px 540px
            grid-template-areas: |
              "cam1 cam2"
              "cam3 cam4"

            mediaquery:
              #hide_header: false
              #phone
              "(max-width: 800px)":
                grid-gap: 1.5vw
                grid-template-columns: auto
                grid-template-rows: auto auto auto auto
                grid-template-areas: |
                  "cam1"
                  "cam2"
                  "cam3"
                  "cam4"

              #tablet
              "(max-width: 2000px)":
                grid-gap: 0.5vw
                grid-template-rows: 23vw 23vw
                grid-template-areas: |
                  "cam1 cam2"
                  "cam3 cam4"

          cards:
            - type: picture-entity
              entity: camera.garagemfrente
              aspect_ratio: 50%
              show_info: false
              show_state: false
              show_name: false
              camera_view: auto
              view_layout:
                grid-area: cam1

            - type: picture-entity
              entity: camera.varandafrente
              show_info: false
              show_state: false
              show_name: false
              camera_view: auto
              view_layout:
                grid-area: cam2

            - type: picture-entity
              entity: camera.varandafundo
              show_info: false
              show_state: false
              show_name: false
              camera_view: auto
              view_layout:
                grid-area: cam3

            - type: picture-entity
              entity: camera.terraco
              show_info: false
              show_state: false
              show_name: false
              camera_view: auto
              view_layout:
                grid-area: cam4

WIP kitchen timer card

this weekend I worked on a kitchen timer card for my kitchen dashboard, this is still a work in progress. I wanted to put it up to see if anyone had any thoughts

there are 3 sections,
the 1st is the time input that I got from this post (thanks @Mattias_Persson and @ParalaX)
this is where how you can enter a time for the timer to run in hours and minutes, the last enter time is saved, and tapping the button will start the timer

the 2ed sections, is the countdown, if a timer is running this is displayed that shows how long the timer has remaining, and can be tapped to stop a running timer, a lot of work went into drawing the outline around the circle as the timer ticks down the circle outline will fill in.


The last section is just 4 buttons that can be used to start common timers with one click, this section can be shown by swiping

setup

you will need an input date time and a timer Entity, along with the following script that is used to start the timer with the input from the card

script

kitchen_timer:
  alias: calculates the time given for the timer and then starts the kitchen timer,
  fields:
    time_input:
      description: Entity id of the date time input
      example: input_datetime.kitchen_timer
    timer:
      description: Entity id of the timer to set
      example: timer.kitchen_timer
  sequence:
  - service: timer.start
    data:
      duration:  '{{ states(time_input) }}'
    target:
      entity_id: '{{ timer }}'
  mode: single

code

use

      #################################################
      #                                               #
      #                     Timer                     #
      #                                               #
      #################################################

      - type: grid
        title: Timer ↔
        view_layout:
          grid-area: timer
        columns: 1
        cards: !include decks/kitchen_timer_deck.yaml

kitchen_timer_deck.yaml

  #################################################
  #                                               #
  #             kitchen timer deck                #
  #                                               #
  #################################################
- type: custom:swipe-card
  start_card: 1
  parameters:
    roundLengths: true
    effect: coverflow
    speed: 650
    spaceBetween: 20
    threshold: 7
    coverflowEffect:
      rotate: 80
      depth: 300
  cards:
# page 1
    - type: horizontal-stack
      cards:
      # show input if timer is idle, else show countdown
        - type: conditional
          conditions:
            - entity: timer.kitchen_timer
              state: idle
          card:
            #################################################
            #                                               #
            #                     Input                     #
            #                                               #
            #################################################
            type: custom:button-card
            name: >
              [[[ return '&nbsp;'; ]]]
            state_display: Kitchen Timer
            template:
              - base
            custom_fields:
              time:
                card:
                  type: custom:time-picker-card
                  entity: input_datetime.kitchen_timer
                  hide:
                    name: true
                  card_mod:
                    style:
                      .: |
                        .time-picker-content {
                          justify-content: space-evenly !important;
                          padding-right: 1%;
                        }
                        .time-separator {
                          color: rgba(255, 255, 255, 0.3);
                        }
                        .time-picker-row {
                          display: block !important;
                          padding: 0 !important;
                          overflow: hidden !important;
                        }
                        .time-input {
                          border-radius: 10px;
                        }
                        :host {
                          --ha-card-border-width: 0px;
                          --time-picker-elements-background-color: rgba(0, 0, 0, 0.15);
                          --time-picker-icon-color: rgba(255, 255, 255, 0.4);
                          --time-picker-text-color: rgba(255, 255, 255, 0.6);
                          --time-picker-control-padding: 6px;
                        }
                      time-unit:
                        $: |
                          .time-input {
                            border-radius: 0.4vw;
                          }
                          .time-unit {
                            padding: 0 !important;
                          }
            styles:
              custom_fields:
                time:
                  - position: absolute
                  - width: 100%
                  - height: 80%
                  - clip-path: inset(0 round var(--custom-button-card-border-radius))
                  - left: 0
                  - top: 15%
            tap_action:
              action: call-service
              service: script.kitchen_timer
              service_data:
                time_input: input_datetime.kitchen_timer
                timer: timer.kitchen_timer
              
        - type: conditional
          conditions:
            - entity: timer.kitchen_timer
              state_not: idle
          card:
            #################################################
            #                                               #
            #                   Countdown                   #
            #                                               #
            #################################################
            type: custom:button-card
            entity: timer.kitchen_timer
            template:
              - base
            tap_action:
              action: call-service
              service: timer.cancel
              service_data:
                entity_id: timer.kitchen_timer
            custom_fields:
              countdown: >
                [[[
                  setTimeout(() => {

                    let elt = this.shadowRoot,
                    circle_stroke = elt.getElementById('circle_stroke'),
                    r = 22.1,
                    c = r * 2 * Math.PI,
                    now = new Date().getTime(),
                    endDate = new Date(entity.attributes.finishes_at),
                    remaining = entity.attributes.remaining.split(':'),
                    duration = entity.attributes.duration.split(':'),
                    startDate = new Date(endDate.getTime() - (remaining[0]*3600+remaining[1]*60)*1000),
                    percent = ((now - startDate) / (endDate - startDate)) * 100;
                    circle_stroke.style.strokeDashoffset = c - percent / 100  * c;
                    circle_stroke.style.strokeWidth = 'var(--c-stroke-width-dragging)';
                  }, 0);

                  let r = 22.1,
                  c = r * 2 * Math.PI,
                  state = true,
                  input = variables.circle_input || ' ';

                  return `
                  <svg viewBox="0 0 50 50">
                    <style>
                      circle {
                        transform: rotate(-90deg);
                        transform-origin: 50% 50%;
                        stroke-dasharray: ${c};
                        stroke-dashoffset: ${typeof input === 'number' && c - input / 100 * c};
                        stroke-width: var(--c-stroke-width);
                        
                      }
                      #circle_stroke{
                        stroke: ${'var(--c-stroke-color-on)' };
                        fill: ${'var(--c-fill-color-on)'};
                      }
                      #circle_bg {
                        stroke: ${'var(--c-stroke-color-off)'};
                        fill: ${'var(--c-fill-color-off)'};

                      }
                      text {
                        font-size: var(--c-font-size);
                        font-weight: var(--c-font-weight);
                        letter-spacing: var(--c-letter-spacing);
                        fill: var(--c-font-color);
                      }
                      tspan {
                        font-size: var(--c-unit-font-size);
                      }
                      #circle_value, tspan {
                        text-anchor: middle;
                        dominant-baseline: central;
                      }
                    </style>
                    <circle id="circle_stroke" cx="25" cy="25" r="${r}"/>
                    <circle id="circle_bg" cx="25" cy="25" r="${r}"/>
                  </svg>       `;           
                ]]]
            styles:
              state:
                - position: relative
                - line-height: 0px
                - overflow: visible
                - top: -2.5em
                - margin: auto
                - display: initial
                - --button-card-font-size: 4vw
              card:
                - --c-stroke-color-on: '#b0b0b0'
                - --c-stroke-color-off: none
                - --c-fill-color-on: none
                - --c-fill-color-off: rgba(255,255,255,0.04)
                - --c-stroke-width: 2.3
                - --c-stroke-width-dragging: 1
                - --c-font-color: '#97989c'
                - --c-font-size: 14px
                - --c-unit-font-size: 10.5px
                - --c-font-weight: 700
                - --c-letter-spacing: -0.02rem
              custom_fields:
                countdown:
                  - position: absolute
                  - width: 100%
                  - height: 100%
                  - left: 0
                  - top: -8%
                  - display: initial
                  - opacity: 1
                  - justify-self: end
# page 2
    - type: grid
      columns: 2
      cards:
        - type: custom:button-card
          entity: binary_sensor.template_living_room_tv_source_plex
          template: 
            - base
          name: 5 min
          show_state: false
          custom_fields:
            icon: <ha-icon icon="mdi:clock-time-two-outline"></ha-icon>
          tap_action:
            action: call-service
            service: timer.start
            service_data:
              duration: "00:05:00"
              entity_id: timer.kitchen_timer

        - type: custom:button-card
          entity: binary_sensor.template_living_room_tv_source_youtube
          template: 
            - base
          name: 25 min
          show_state: false
          custom_fields:
            icon: <ha-icon icon="mdi:clock-time-five-outline"></ha-icon>
          tap_action:
            action: call-service
            service: timer.start
            service_data:
              duration: "00:25:00"
              entity_id: timer.kitchen_timer

        - type: custom:button-card
          entity: binary_sensor.template_living_room_tv_source_netflix
          template: 
            - base
          name: 45 min
          show_state: false
          custom_fields:
            icon: <ha-icon icon="mdi:clock-time-eight-outline"></ha-icon>
          tap_action:
            action: call-service
            service: timer.start
            service_data:
              duration: "00:45:00"
              entity_id: timer.kitchen_timer

        - type: custom:button-card
          entity: binary_sensor.template_living_room_tv_source_funimation
          template: 
            - base
          name: 1 hour
          show_state: false
          custom_fields:
            icon: <ha-icon icon="mdi:clock-time-twelve-outline"></ha-icon>
          tap_action:
            action: call-service
            service: timer.start
            service_data:
              duration: "01:00:00"
              entity_id: timer.kitchen_timer
14 Likes

hi,

i have the same Problem.

Can you post a screenshot of your home assistance folder with all sub folders expanded?

@Laffer has made his config available, there is no “problem” you are just using outdated code.

2 Likes

So I ended up rebuilding the card and managed to fix the glitchy scrolls by eliminating the horizontal stack card, but still couldn’t avoid a normally behaving vertical scroll appearing (despite the popup having enough space that it shouldn’t vertically scroll)

So I tried adding the scene chips to the top of the card instead, which eliminated all scroll entirely!
I also did a quick check that a horizontal stack behaves normally when it’s the first card of the bunch, and it does.

My code is missing some of the card mod styling yours had, but seems to display correctly on all my devices:

Code
light:
  template:
    - base
    - circle
    - loader
  double_tap_action:
    action: fire-dom-event
    browser_mod:
      service: browser_mod.popup
      data:
        title: >
          [[[
            return !entity || entity.attributes.friendly_name;
          ]]]
        content:
          type: vertical-stack
          cards:
            - type: custom:mushroom-chips-card
              card_mod:
                style: |
                  ha-card {
                    --chip-background: rgba(var(--rgb-primary-text-color), 0.05);
                  }
              alignment: center
              chips: >
                [[[
                  if (entity) {
                    let id = Boolean(entity.attributes.entity_id)
                          ? [entity.entity_id].concat(entity.attributes.entity_id)
                          : [entity.entity_id];
                    let foundSenes = Object.keys(states).filter(s => s.indexOf("scene.") > -1).map(s=>states[s]).filter(s=>s.attributes.entity_id.some(e=>id.includes(e))),
                    scenes = [];
                    for (let i = 0; i < foundSenes.length; i++) {
                      scenes.push({"type": "entity",
                        "entity": foundSenes[i].entity_id,
                        "content_info": "",
                        "tap_action": {
                          "action": "call-service",
                          "service": "scene.turn_on",
                          "target": {
                            "entity_id": foundSenes[i].entity_id
                          },
                          "data": {
                            "transition": "1"
                          }
                        }
                      });
                    }
                    return variables.show_scenes? scenes: [];
                  }
                ]]]
            - type: entities
              card_mod:
                style: |
                  #states {
                    margin-top: -1em;
                  }
              entities: >
                [[[
                  if (entity) {
                      let lights = [],
                          id = Boolean(entity.attributes.entity_id)
                              ? [entity.entity_id].concat(entity.attributes.entity_id)
                              : [entity.entity_id];
                      for (let i = 0; i < id.length; i++) {
                          lights.push({
                              "type": "custom:mushroom-light-card",
                              "entity": id[i],
                              "fill_container": false,
                              "primary_info": "name",
                              "secondary_info": "state",
                              "icon_type": "icon",
                              "show_brightness_control": true,
                              "use_light_color": true,
                              "show_color_temp_control": true,
                              "show_color_control": true,
                              "collapsible_controls": false
                          });
                      }

                      return lights;
                  }
                ]]]
  variables:
    circle_input: >
      [[[
        if (entity) {
            // if light group get brightness from child to remove bounce
            let child = entity.attributes.entity_id,
                brightness = child && states[child[0]].attributes.brightness
                    ? Math.round(states[child[0]].attributes.brightness / 2.54)
                    : Math.round(entity.attributes.brightness / 2.54);
            return brightness === 0 && entity.state !== 'off'
                ? 1
                : brightness
        }
      ]]]
    circle_input_unit: "%"
    show_scenes: true

I bought styled chips back, and dropped the name display. I also eliminated some dead space below the scene card that annoyed me

Part of me still wants to use the manual listing version you first posted tho. I really like that can order my scenes with it, and I also have a couple of scenes that control lights indirectly (I have switches for a circadian temp mode and one for extracted spotify album colours).

Perhaps it’d be wiser to figure out a similar template to add switches alongside the scenes, but I do want to avoid a second row of chips, which may further complicate things (with my lack of coding ability).

As another solution, I’m also curious about pulling scenes based on area in common. Not sure if this is possible, as templating for area seems a little different from just checking attributes.

Would there be a chance that you’d know a resource or two for me to better understand how to code the dynamics parts that you’ve made?

Totally understand if knowing such a specific resource is a bit too specific of a request.

Thanks again for the base template!

evening, that’s looking nice, lots to go over apologies if I don’t cover everything.

I also moved the chips to the top of the card as I felt it was more useful.

yes most of it was copy and pasted for other sections of my dashboard so I would not be surprised if most of what I had was not needed.

nice

its looking a lot better than my attempt, im better at the make it work not so good at the make it look nice.

it would be possible to order the scenes alphabetically, that could be friendly_name or entity_id. think we could even order by last used or last added date.

you could do that but it might be a little over kill and if it is only a 1 off then just create a custom popup for that light and dont worry about the dynamics parts

I dont think it will be possible to pull out scenes based on area, we are not working with the Jinja2 templating engine, but we are accessing the JavaScript object directly. so we are limited by the data that is on the JavaScript object

I have 10+ years of coding with javascript, a diploma in programming and a bachelors in computer science, and I work as a software developer where i work with javascript for 40 - 50 hours a week.
even with all that I still had lots of google tabs and referenced @Mattias_Persson’s code.

as a start you could look at JavaScript Tutorial for some basic javascript, but programming is all about foundations, a loop is a loop no mater the language or syntax.

Thanks for the detailed reply, I know it’s shooting a little off topic at this point

You’re probably right that I should save time and just make a couple of custom pop-ups instead of getting carried away.

we are accessing the JavaScript object directly. so we are limited by the data that is on the JavaScript object

ah makes sense, I had a feeling it was something like this

I have 10+ years of coding with javascript, a diploma in programming and a bachelors in computer science, and I work as a software developer where i work with javascript for 40 - 50 hours a week.
even with all that I still had lots of google tabs and referenced @Mattias_Persson’s code.

This makes me feel much better about not being able to figure some of it out myself lol

programming is all about foundations, a loop is a loop no mater the language or syntax

A good reminder that I really should start to learn some programming to complement the endless poking around with code I find, cheers.