A different take on designing a Lovelace UI

i’m trying to make the volume level of a media_player work the same as the brightness of a light, so circle touch to adjust volume.

i tried this but the circle does not react to touch.

            if (domain === 'media_player' && state) {

                // wait 0ms for shadow dom
                setTimeout(() => {

                    // then get elements
                    let elt = this.shadowRoot,
                        circle_slider = elt.getElementById('circle_slider'),
                        circle_value = elt.getElementById('circle_value'),
                        circle_stroke = elt.getElementById('circle_stroke');

                    // approximate position of thumb relative to circle
                    circle_slider.style.top = `${(circle_slider.value - 50) / 1.66 - 1}%`;

                    // debug position
                    let debug = false;
                    if (debug) circle_slider.style.opacity = 0.3;

                    // pass each event to handler
                    ['click', 'input', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach((event) => {
                        circle_slider.addEventListener(event, handler, { passive: true })
                    });

                    function handler(event) {

                        // "this" refers to slider
                        if (event.target === this) {

                            // bypass button-card tap_action
                            event.stopPropagation();

                            // update circle_value
                            circle_value.innerHTML = `${this.value}${tspan}${unit}</tspan>`;

                            // update stroke
                            circle_stroke.style.strokeDashoffset = c - this.value / 100 * c;
                            circle_stroke.style.strokeWidth = 'var(--c-stroke-width-dragging)';
                            
                            // set cursor while dragging
                            if (event.type === 'mousedown' || event.type === 'input') {
                                this.style.cursor = 'grabbing';
                            } else {
                                this.style.cursor = 'grab';
                            }

                            // reset stroke width if value doesn't change
                            if (input == this.value && (event.type === 'click' || event.type === 'touchend'))
                                circle_stroke.style.strokeWidth = 'var(--c-stroke-width)';

                            // on release
                            if (event.type === 'mouseup' || event.type === 'touchend') {

                                // display loader if brightness is 0
                                if (circle_slider.value == 0 && elt.getElementById('loader')) {
                                    elt.getElementById('loader').style.display = 'initial';
                                    elt.getElementById('circle').style.display = 'none';
                                }

                                // set volume
                                hass.callService('media_player', 'volume_set', {
                                    entity_id: entity.entity_id,
                                    volume_level: this.value
                                });
                            }
                        }
                    }
                }, 0);

                return circle(state, input, unit);
            }  
                  ${domain === 'media_player' && `
                      <input id="circle_slider" type="range" min="0" max="1" value="${input}">
                  `}                
                `;

you are spot on with the changes, but you have not updated the css in button_card_templates/extra_styles.yaml

I have set debug to true so you can see the slider in the screenshot

 // debug position
let debug = true;
if (debug) circle_slider.style.opacity = 0.3;

I did this with a little hack as you will see.
as you have not provided the yaml of the button that you are using this on you will have to apply my code to your situation yourself

in extra_styles.yaml look for

        ${this._config.template.includes('light')  ? `

         /* * * * * * * * * * * * * * * * * *
          *                                 *
          *          CIRCLE SLIDER          *
          *                                 *
          * * * * * * * * * * * * * * * * * */

that if condition is only applying the css to buttons that have the light template

I tested this on my sound-bar button and i just used the icon template to apply the styles but it would be better if you had a template that was for your TV not an icon template this was just a quick test

        ${(this._config.template.includes('light') || this._config.template.includes('icon_soundbar'))  ? `

         /* * * * * * * * * * * * * * * * * *
          *                                 *
          *          CIRCLE SLIDER          *
          *                                 *
          * * * * * * * * * * * * * * * * * */

edit
I also did this but I didn’t try what you had so I dont know if that is going to be an issue

                  ${(domain === 'light' || domain === 'media_player') && `
                      <input id="circle_slider" type="range" min="0" max="100" value="${input}">
                  `}

Does anyone know how to have a single button cover 2 columns on 1 row?
I can’t seem to figure it out :sweat_smile:

edit-
Actually, not just 2 columns on 1 row, but rather all columns in 1 row.

cool thanks for the info! i got it working, exept now the custom:swipe-card is prohibiting me from using the slider.

So it works on normal buttons, but when i use a custom: swipe-card to allow for multiple tabs is does not work. Also the lights dont work then.

Do you have any suggestions for that?

EDIT: Strange it does work on the tablet with touch just not on the PC with the mouse

      #################################################
      #                                               #
      #                    leefkeuken                     #
      #                                               #
      #################################################
      - type: grid
        title: Leefkeuken ↔
        view_layout:
          grid-area: leefkeuken
        columns: 1
        cards:

          - type: custom:swipe-card
            parameters:
              speed: 550
              spaceBetween: 40
              threshold: 5
            start_card: 1
            cards:
            
              - type: grid
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: light.keuken
                    name: Keuken Lampen
                    double_tap_action:
                      !include popup/leefkeuken_lampen.yaml
                    template:
                      - light
                      - icon_spot

                  - type: custom:button-card
                    entity: light.eettafel
                    name: Eettafel
                    template:
                      - light
                      - icon_lamp

                  - type: custom:button-card
                    entity: switch.oven_power
                    name: Oven
                    double_tap_action:
                      !include popup/leefkeuken_oven.yaml
                    template:
                      - base
                      - icon_stove
#                      - circle



                  - type: custom:button-card
                    entity: media_player.leefkeuken
                    name: Leefkeuken
                    double_tap_action:
                      !include popup/leefkeuken_monitorer.yaml
                    template:
                      - mediaplayer
                      - icon_monitors


              - type: grid
    #            view_layout:
    #              grid-area: woonkamer
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: climate.leefkeuken
                    name: Verwarming
                    tap_action:
                      !include popup/woonkamer_verwarming.yaml
                    template:
                      - base
                      - icon_climate
                      - circle
                    variables:
                      circle_input: >
                        [[[
                          if (entity) {
                              return entity.state === 'cool'
                                  ? Math.round(entity.attributes.temperature).toString()
                                  : Math.round(entity.attributes.current_temperature).toString();
                          }
                        ]]]
                      circle_input_unit: '°C'

mediaplayer.yaml (template)

mediaplayer:
  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;
          ]]]
        card_mod:
          style:
            #popup header
            .:
        content:
          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;
              }
            ]]]
  variables:
    circle_input: >
      [[[
        if (entity) {
            // if light group get brightness from child to remove bounce
            let child = entity.attributes.entity_id,
                volume = child && states[child[0]].attributes.volume_level
                    ? Math.round(states[child[0]].attributes.volume_level * 100)
                    : Math.round(entity.attributes.volume_level * 100);
            return volume === 0 && entity.state !== 'off'
                ? 1
                : volume
        }
      ]]]  
    circle_input_unit: '%'

mediaplayer section in circle.yaml

          /* * * * * * * * * * * * * * * * * *
            *                                 *
            *              MEDIAPLAYER        *
            *                                 *
            * * * * * * * * * * * * * * * * * */

            if (domain === 'media_player' && state) {

                // wait 0ms for shadow dom
                setTimeout(() => {

                    // then get elements
                    let elt = this.shadowRoot,
                        circle_slider = elt.getElementById('circle_slider'),
                        circle_value = elt.getElementById('circle_value'),
                        circle_stroke = elt.getElementById('circle_stroke');

                    // approximate position of thumb relative to circle
                    circle_slider.style.top = `${(circle_slider.value - 50) / 1.66 - 1}%`;

                    // debug position
                    let debug = true;
                    if (debug) circle_slider.style.opacity = 0.3;

                    // pass each event to handler
                    ['click', 'input', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach((event) => {
                        circle_slider.addEventListener(event, handler, { passive: true })
                    });

                    function handler(event) {

                        // "this" refers to slider
                        if (event.target === this) {

                            // bypass button-card tap_action
                            event.stopPropagation();

                            // update circle_value
                            circle_value.innerHTML = `${this.value}${tspan}${unit}</tspan>`;

                            // update stroke
                            circle_stroke.style.strokeDashoffset = c - this.value / 1 * c;
                            circle_stroke.style.strokeWidth = 'var(--c-stroke-width-dragging)';
                            
                            // set cursor while dragging
                            if (event.type === 'mousedown' || event.type === 'input') {
                                this.style.cursor = 'grabbing';
                            } else {
                                this.style.cursor = 'grab';
                            }

                            // reset stroke width if value doesn't change
                            if (input == this.value && (event.type === 'click' || event.type === 'touchend'))
                                circle_stroke.style.strokeWidth = 'var(--c-stroke-width)';

                            // on release
                            if (event.type === 'mouseup' || event.type === 'touchend') {

                                // display loader if brightness is 0
                                if (circle_slider.value == 0 && elt.getElementById('loader')) {
                                    elt.getElementById('loader').style.display = 'initial';
                                    elt.getElementById('circle').style.display = 'none';
                                }

                                // set volume
                                hass.callService('media_player', 'volume_set', {
                                    entity_id: entity.entity_id,
                                    volume_level: this.value / 100
                                });
                            }
                        }
                    }
                }, 0);

                return circle(state, input, unit);
            }  

So the red arrow one is working, the blue arrow ones are showing a hand in the cursor but do not respond to a vertical swipe.

Thanks for sharing this great design. I’m a bit strugling with combining the entities in a row. Can someone direct me in the right direction to get one border around all entities or have no gray border at all.
Schermafbeelding 2023-01-22 205042

- type: grid
        title: Neerslag
        view_layout:
        grid-area: neerslag
        columns: 1
        cards:
          - type: vertical-stack
            cards:
            - type: custom:neerslag-card
              entity: sensor.neerslag_buienalarm_regen_data

            - type: entities
              entities:
              - entity: sensor.tuin_oregon_temperature
                name: Tuin
                icon: mdi:tree

              - entity: sensor.huiskamer_oregon_temperature
                name: Huiskamer
                icon: mdi:sofa

Hello all,
looks like a question asked thousand of times but I’m still not able to make it work.
is there a “clean” way to add a card under the sidebar template aligned to the bottom of the screen?
As of now the only way I’ve found was to add an empty card and fixing its size to add some empty space.

Here’s my code:

  - type: vertical-stack
    view_layout:
      grid-area: sidebar
    cards:
      - type: custom:button-card
        entity: sensor.template_sidebar
        template: sidebar
      - type: 'custom:button-card'
        color_type: blank-card
        card_mod:
          style: |
            ha-card {
              min-height: 25vh !important;
              border-style: none !important
            }

      - type: custom:atomic-calendar-revive
....

This “works” but in case sidebar template adds some more info it is not automatically updated and (even worse) it adds an empty space in my mobile view.

Has anyone found a better solution for that?

Thanks a lot!
Dave

This might not be the “right” way to do it, but you can make it happen with card mod and @media queries to change the look at different screen sizes.

The important part of the first CSS section is position: absolute and bottom: 2vh. 2vh just gives it a slight amount of padding at the bottom rather than the edge of the screen/browser. Also be warned you might have to set or override some other values like width when using position: absolute depending on what card you’re adding and what styles that card has set.

          - type: "custom:atomic-calendar-revive"
            entities:
            - entity: calendar.home_assistant_devs
            card_mod: 
              style: |
                ha-card {
                  border: none;
                  --card-height: inherit !important;
                  position: absolute;
                  bottom: 2vh;
                }
                @media (max-width: 800px) {
                  ha-card {
                    position: inherit;
                  }
                }

1 Like

Yes, it’s possible.
Open template sidebar.yaml and go to section ‘extra_styles’. Modify id ‘#card’ then add min-height value: ‘min-height: 80vh;’ (adapt its value to your screen).


Now go to the section ‘@media screen and (max-width: 800px)’ and add id ‘#card’ with min-height value auto (see image).

That’s all.

This is my result:

1 Like

Hello,

How can I make this popup a bit wider, so that there is no slide bar?
Screenshot 2023-01-23 130441

Thanks!

this might help, A different take on designing a Lovelace UI - #3692 by Mattias_Persson

try this A different take on designing a Lovelace UI - #4072 by Mattias_Persson

thanks @D34DC3N73R and @pajeronda I will have to try your solutions, I am not that happy with my current method for this

1 Like

Thanks @D34DC3N73R I tried it and it partially works.
Card is correctly placed at the bottom of the page but it is no more aligned to the sidebar column (it slightly overlaps other columns).

Any idea?

Thanks!

Ciao @pajeronda I tried also your solution but it works only if I have 2 cards.
On my solution I have 3 cards and changing value on sidebar template moves down the second card (that I want close to the sidebar).

Davide

Ciao @Dave81 if you want to add more cards reduce the min-height value.

Yes, you’ll have to add other css attributes like padding, width or max-width. When you use absolute position the card is sort of ‘floating’ and no longer bound to the parent styles. In your case it looks like you could reduce padding-left, or set the width.

I made a way to easily add motion sensors to light cards. Pretty much all of my lights are tied to motion sensors and I wanted a way to include motion info. It’s styled similarly to the climate cards when lights are off, and when the lights are on, it animates to the upper right corner of the card and doesn’t interfere with the brightness circle. Side note, I couldn’t get triggers_update to work with a template so I’ve left that as part of the button card rather than in the template itself. If anyone has pointers on that I’d be glad to hear it.

kitchen_button

Add motion.yaml to the button_card_templates directory.

In your ui-lovelace.yaml, add

variables: 
  motion: 

and define the motion sensor entity_id.

If you want the card to update with the motion sensor and light entity state (rather than only the light entity), you’ll also need to add the motion entity to triggers_update:.

Lastly include - motion in the template section of your button card.

Code available here:

18 Likes

I’m trying to get the state (current power usage) in the name of the footer-button.

          - type: custom:button-card
            entity: sensor.power_consumed
            name: >
              <ha-icon icon="mdi:chart-line-variant"></ha-icon> [[[ return "states['sensor.power_consumed']" ]]]
            show_name: true
            show_state: false
            tap_action:
              !include popup/footer_updates.yaml
            template: footer

But this doesn’t seem to work, anybody knows how to template the name-field so it does work?

Try this

            name: >
              [[[
               let power = states['sensor.power_consumed'].state;
               return `<ha-icon icon="mdi:chart-line-variant"></ha-icon> ${power}`;
              ]]]
1 Like

this works! many thanks for the advice!

I don’t understand how the icons are created. Is it possible to get more?

see this post, @Mattias_Persson iv needed to share this what feels like 5 times, could we Look at adding it to the FAQ?

2 Likes