A different take on designing a Lovelace UI

LafferAndré Regardless of my request, can you share the camera code?

Not quite. Here is a snapshot of my panel. I am using one big custom:button-card, because I cannot figure out how to style a grid-card. BUT I want to be able to click on a link or particular area and offer a popup. If I could style the grid-card and make the custom:button cards transparent, all would be right in my world.

Well, I don’t know how you’d make 2 separate grids group like that, but you could have both grid gaps match. But again, it would apply to everything… if this is not what you’re looking for then someone else has to pitch in, I’m afraid :slight_smile:

lovelace.yaml
grid-gap: 1vw

themes.yaml
grid-card-gap: 1vw

@Laffer Thanks again for the answer. But it gives something general unfortunately.

Can you share the code of your camera?

Here you go:

action: fire-dom-event
browser_mod:
  command: popup
  title: Weather
  style:
    hui-vertical-stack-card:
      $hui-history-graph-card$ : |
        .content {
          padding: 0.2em 1.7em 1.2em 1.7em !important;
        }
      $: |
        button-card {
          align-self: center;
          padding: 0.2em 0 2.3em 0;
        }
      $hui-map-card:
        $: |
          mwc-icon-button {
            color: var(--primary-color);
          }
          ha-card {
            border-radius: 0;
            animation: border 1s forwards;
          }
          @keyframes border {
            0%, 100% {
              border-top: 2px solid #1a1a1a;
            }
          }
        $ha-map$: |
          #map {
            background-color: #191919 !important;
          }
          .leaflet-control-attribution {
            display: none;
          }
          .leaflet-bar a {
            background-color: rgba(47, 51, 51, 0.9) !important;
            color: #9da0a2 !important;
          }
          a.leaflet-control-zoom-in {
            border-bottom: 1px solid #181818 !important;
          }
          .leaflet-pane.leaflet-tile-pane {
            filter: contrast(85%);
          }
  card:
    type: vertical-stack
    cards:
      - type: entities
        card_mod:
          class: content
        entities:
          - entity: weather.openweathermap
            secondary_info: last-changed
            name: Your place

      - type: custom:apexcharts-card
        layout: minimal
        locale: 'no' #no? #se
        graph_span: 12h
        show:
          loading: false
        apex_config:
          plotOptions:
            area:
              fillTo: end
          grid:
            padding:
              top: -15
          fill:
            type: gradient
            gradient:
              type: vertical
              opacityFrom: 0.8
              opacityTo: 0
              stops:
                - 0
                - 99
                - 100
          stroke:
            width: 4
          tooltip:
            style:
              fontSize: 14px
            x:
              format: dddd HH:mm
          chart:
            height: 140px
            offsetY: -20px
          xaxis:
            crosshairs:
              show: false
        series:
          - entity: sensor.openweathermap_temperature
            name: Temp
            color: '#385581'
            type: area
            fill_raw: last
            group_by:
              func: avg
              duration: 1h

      - color_type: blank-card
        type: custom:button-card

      - type: iframe
        url: https://embed.windy.com/embed2.html?lat=58.905&lon=7.789&detailLat=59.886&detailLon=9.360&width=650&height=450&zoom=7&level=surface&overlay=wind&product=ecmwf&menu=&message=&marker=&calendar=now&pressure=&type=map&location=coordinates&detail=&metricWind=default&metricTemp=default&radarRange=-1
        aspect_ratio: 75%

It’s basically a copy of Mattias’s Conditional Media card - just flipped.
…I think you can remove ‘- type: horizontal-stack’ and everything beneath if you don’t want that option.

      - type: grid
        title: Cameras
        view_layout:
          grid-area: cameras
        columns: 1
        cards:

          - type: custom:swipe-card
            start_card: 1
            parameters:
              roundLengths: true
              effect: coverflow
              speed: 650
              spaceBetween: 20
              threshold: 7
              coverflowEffect:
                rotate: 80
                depth: 300
            cards:

              - type: grid
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: camera.inngangen
                    name: " "
                    template:
                      - camera
                      - icon_unifi_cam
                    tap_action:
                      !include popup/camera_single.yaml

                  - type: custom:button-card
                    entity: camera.ringeklokke
                    name: " "
                    template:
                      - camera
                      - icon_unifi_cam
                    tap_action:
                      !include popup/camera_single.yaml

                  - type: custom:button-card
                    entity: camera.stua
                    name: " "
                    template:
                      - camera
                      - icon_unifi_cam
                    tap_action:
                      !include popup/camera_single.yaml

                  - type: custom:button-card
                    entity: camera.verandaen
                    name: " "
                    template:
                      - camera
                      - icon_unifi_cam
                    tap_action:
                      !include popup/camera_single.yaml


              - type: horizontal-stack
                cards:

                  - type: conditional
                    conditions:
                      - entity: input_select.conditional_camera
                        state: Driveway
                    card:
                      type: custom:button-card
                      entity: camera.inngangen
                      tap_action:
                        action: none
                      template:
                        - conditional_camera

                  - type: conditional
                    conditions:
                      - entity: input_select.conditional_camera
                        state: Doorbell
                    card:
                      type: custom:button-card
                      entity: camera.ringeklokke
                      template:
                        - conditional_camera

                  - type: conditional
                    conditions:
                      - entity: input_select.conditional_camera
                        state: Living-room
                    card:
                      type: custom:button-card
                      entity: camera.stua
                      template:
                        - conditional_camera

                  - type: conditional
                    conditions:
                      - entity: input_select.conditional_camera
                        state: Terrace
                    card:
                      type: custom:button-card
                      entity: camera.verandaen
                      template:
                        - conditional_camera

2 Likes

Thanks, Template Camera can you share? And Icon_unifi_cam - I will be happy to share what is connected to your code …

Thank you

The UniFi cam icon is not really used there, yet. And it’s too messy atm.
It’s getting there, tho… I can share it once I’m done.

image

  base_camera:
    tap_action:
      action: >
        [[[
          return !(variables.state === 'off' || variables.state === 'standby') ? 'call-service' : 'none';
        ]]]
      service: media_player.media_play_pause
      service_data:
        entity_id: >
          [[[ return entity === undefined || entity.entity_id; ]]]
    double_tap_action:
      action: call-service
      service: >
        [[[ return variables.state === 'off' || variables.state === 'standby' ? 'media_player.turn_on' : 'media_player.turn_off'; ]]]
      service_data:
        entity_id: >
          [[[ return entity === undefined || entity.entity_id; ]]]
    styles:
      card:
        - color: >
            [[[
              let entity_picture = entity === undefined || entity.attributes.entity_picture;
              if (variables.state === 'off' || variables.state === 'standby' ||
                variables.state === 'unknown' || variables.state === 'unavailable' || entity === undefined) {
                return 'rgba(255, 255, 255, 0.3)';
              }
              return (variables.state != 'off' && variables.state != 'standby') && (entity_picture == null) ? 'rgba(0, 0, 0, 0.6)' : '#efefef';
            ]]]
        - text-shadow: >
            [[[
              let entity_picture = entity === undefined ? null : entity.attributes.entity_picture;
              return entity_picture == null ? 'none' : '1px 1px 5px rgba(18, 22, 23, 0.9)';
            ]]]


  camera:
    template:
      - base
      - base_camera
    state_display: >
        [[[ if (variables.state == 'idle' || variables.state == 'recording') return ' '; ]]]
    styles:
      custom_fields:
        icon:
          - width: 70%
          - fill: '#9da0a2'
          - opacity: >
              [[[
                let entity_picture = entity === undefined ? null : entity.attributes.entity_picture;
                if (entity.state !== 'unavailable' && entity.state !== 'standby') {
                  return entity_picture == null ? 1 : 0;
                }
              ]]]
      card:
        #- align-self: middle
        - background-color: none
        - background-size: cover #137% 101%
        - background-position: center
        - background-repeat: no-repeat
        - background-image: >
            [[[
              let entity_picture = entity === undefined || entity.attributes.entity_picture;
              if (variables.state === 'off' || variables.state === 'standby' ||
                variables.state === 'unknown' || variables.state === 'unavailable' || entity === undefined) {
                return 'linear-gradient(0deg, rgba(115, 115, 115, 0.2) 0%, rgba(115, 115, 115, 0.2) 100%)';
              }
              return (variables.state != 'off' && variables.state != 'standby') && (entity_picture == null) ? 
                'linear-gradient(0deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%)' : 
                'linear-gradient(0deg, rgba(0,0,0,.1) 0%, rgba(0,0,0,0) 100%), url(' + entity_picture + ')';
            ]]]


  conditional_camera:
    template:
      - camera
      - base_camera
    state_display: >
      [[[
        if (entity.attributes.online === 'false') {
          return 'No cameras online';
        }
        return entity.attributes.online === 'true' && variables.state === 'recording' ? 'No title' : entity.attributes.last_tripped_time;
      ]]]
    custom_fields:
      blur: >
        [[[
          if (entity.attributes.entity_picture !== undefined) return '<div></div>';
        ]]]
      overlay: >
        [[[ 
          if (entity && entity.attributes.entity_picture === undefined && entity.state !== 'unavailable' && entity.state !== 'standby') return '<div></div>';
        ]]]
      media_image: >
        <div></div>
      play_pause: >
        [[[
          let style = `
            <style>
              .scale-up {
                animation: scale-up 1s forwards;
                cubic-bezier(.05, .5, .3, 1);
                transform-origin: center center;
              }

              @keyframes scale-up {
                0% {
                  opacity: 0;
                  transform: scale(0);
                }
                20% {
                  transform: scale(1);
                }
                30% {
                  opacity: 1;
                }
                80% {
                  opacity: 1;
                }
                100% {
                  opacity: 0;
                }
              }
            </style>
          `;
          if (variables.state === 'paused' && variables.timeout < 2000) {
            return `
              <svg viewBox="0 0 166 166">${style}
                <path class="scale-up" d="M0 0h59.9v166H0zm106.1 0H166v166h-59.9z"/>
              </svg>
            `;
          }
          if (variables.state === 'playing' && variables.timeout < 2000) {
            return `
              <svg viewBox="0 0 166 166">${style}
                <path class="scale-up" d="M0 0l166 83L0 166z"/>
              </svg>
            `;
          }
        ]]]
    styles:
      name:
        - z-index: 3
        - margin-bottom: -1%
      state:
        - z-index: 3
      card:
        - background-color: rgba(115, 115, 115, 0.2)
        - padding: 5%
        - border-radius: calc(var(--custom-button-card-border-radius) / 2)
        - backdrop-filter: blur(0) #fix chrome bug
        - -webkit-clip-path: inset(0) #fix safari bug
      custom_fields:
        blur:
          - z-index: 2
          - top: 75%
          - left: 0%
          - width: 100%
          - height: 25.5%
          - position: absolute
          - background-color: rgba(0, 0, 0, 0.2)
          - backdrop-filter: blur(0.4em)
          - -webkit-backdrop-filter: blur(0.4em)
        overlay:
          - z-index: 2
          - opacity: 1
          - top: 75.5%
          - left: 0%
          - width: 100%
          - height: 26%
          - position: absolute
          - background-color: rgba(255, 255, 255, 0.8)
        media_image:
          - z-index: 1
          - top: 0
          - left: 0
          - width: 100%
          - height: 100%
          - position: absolute
          - background-size: cover
          - background-position: center
          - background-repeat: no-repeat
          - background-image: >
              [[[
                return entity.attributes.entity_picture === undefined ? 'none' : `url(${entity.attributes.entity_picture})`;
              ]]]
        play_pause:
          - z-index: 3
          - top: 0
          - right: 0
          - bottom: 0
          - left: 0
          - margin: auto
          - width: 21%
          - height: 21%
          - position: absolute
          - fill: '#dedede'
          - overflow: visible
          - filter: >
              [[[
                let entity_picture = entity === undefined || entity.attributes.entity_picture;
                return entity_picture == null ? 'none' : 'drop-shadow(0 0 1.3vw rgba(0,0,0,0.7))';
              ]]]
        icon:
          - z-index: 3
          - width: 29%
          - fill: >
              [[[ 
                return variables.state === 'off' || variables.state === 'standby' ||
                variables.state === 'unknown' || variables.state === 'unavailable' || entity === undefined ? 
                  '#9da0a2' : 
                  'rgba(255, 255, 255, 0.8)';
              ]]]

3 Likes

Awesome work. How can i load a scene.xyz on the buttons? only works for entities? thx

Why I am getting this? :frowning: Please help @Mattias_Persson

In order to apply card-mod to a layout-card, you need to wrap it in a mod-card and apply the style you want for the grid to the mod-card. It won’t work without the mod-card, I’m afraid.

Hello , i also try to add kodi instead of plex.
the sensor you meantion above where is it located ?

i get this error when i try to copy your card :

ButtonCardJSTemplateError: TypeError: data[1] is undefined in ‘const data = JSON.parse(states[entity.entity_id].attributes.data); return `${data[1].title} (${dat…’

Well I think I know what my problem is… I just don’t know the solution. It seems my button-cards inside the grid are being forced to remain square. Unless I have a grid of 2x2 or 3x3 I’m going to have “off screen” issues. Is there a way to build this with a top row of 2 cells (each with a 2x2 grid of buttons inside) and a bottom row of 3 cells (each with a 2x2 grid of buttons)? I read something about setting square to false but I can’t seem to find the right spot to do it.

HI André,

can you share your Code with the Ccamera Button?

THank You

Hi,

Can someone offer some advice please.

I’m wanting to use a icon other than the ones in the custom button card file.

Is it a case of adding an new icon to the custom button card? The icon is a mdi icon and I would like the base template behind it also?

type: grid
title: Backyard
columns: 2
square: false
cards:

Can somebody tell me where the distance of the sidebar icons is defined ?

(Icons should be at the top and close to the time)

Does anybody know where in the code the HA side menu and top bar are switch off?

Thanks.

Yeah the problem isn’t the grid. The problem is that the custom:button-card wants to be square. I can make my grids “non square” but if the button cards inside the grids want to be square, that doesn’t solve my problem.

The only solution I’ve found so far (which is less than perfect) is to add the aspect_ratio attribute to the custom:button-card. Then I have to tweak the aspect ratio for each display device.

I was wondering if someone could explain this piece of code in the button_card_templates.yaml under the base template:

    extra_styles: |
      [[[
        if (entity) {
          let hs = entity.attributes.hs_color === undefined,
            h = hs || entity.attributes.hs_color[0],
            s = hs || entity.attributes.hs_color[1],
            l_min = 28,
            l_max = 48,
            l_calc = entity.attributes.brightness / 2.54 * (l_max - l_min) / 100 + l_min;
          var light_color = entity.attributes.color_mode === 'color_temp'
            ? `hsl(204, 58%, ${l_calc}%);`
            : `hsl(${h}, ${s}%, ${l_calc}%);`;
        }
        return `
          svg {
            --light-color:
            ${ variables.state_on && entity.attributes.brightness !== undefined
                ? light_color
                : variables.state_on && entity.attributes.brightness === undefined
                  ? 'var(--state-icon-active-color);'
                  : 'var(--state-icon-color);'
            }
          }

The way I read this is that the top part of the code sets the light_color if the entity uses a colored light. However, in my situation, I am using this on a dimmable switch (not a color changing light) and, as a result, hs_color is “null” according to developer tools / templates – I am assuming that translates to “undefined” in javascript.

Anyway, what is actually being returned is

    --light-color: hsl(true, true%, 32.01574803149606%);

Which means entity.attributes.color_mode === ‘color_temp’ is false.

Now this is where I get confused. Since this is not a color_temp light and brightness is NOT undefined, why are we taking the light_color from above and not the --state-icon-active-color instead?

I guess I just need clarification on how to get my dimmable switch (which only has brightness levels) to use the --state-icon-active-color and not a HSL calculation.