Clean Tile-Based Lovelace UI - only 2 cards needed!

…you forgot your config files :wink:

None of those custom cards are addons.

You’re right, I’ve changed the wording.

Fixed! :smiley: Let me know if there’s any other bits you’d like explained!

1 Like

This looks really nice and clean! Maybe you can also share how you created the “Good afternoon” part (along with the two lines below) on the Home page?

Thanks! Here’s the config for the Good Morning/Afternoon/Evening card:

Welcome:
Can also be repurposed as a general title card. 60px tall so needs a row height of at least this. Not a template - copy directly into your view config

      - type: 'custom:button-card'
        gridcol: 1/6
        gridrow: 1/6
        name: >-
          [[[ var d = new Date(); var n = d.getHours(); if (0 <= n && n
          < 12) return "Good morning."; else if (12 <= n && n < 18)
          return "Good afternoon."; else if (18 <= n && n < 24) return
          "Good evening."; else return "ERROR";]]]
        styles:
          card:
            - width: 300px
            - height: 60px
            - background: none
            - box-shadow: none
          name:
            - justify-self: start
            - margin-left: 10px
            - font-weight: normal
            - font-size: xx-large
2 Likes

Active Entities:
Requirements:

  • Header template (see top of page)
  • Popup Card (or Browser Mod, if you prefer)
  • Auto-Entities Card (+ Card Mod, if not installed)

First thing you need is a sensor to count the number of active devices (I’m using lights as an example):
Config.yaml:

  - platform: template
    sensors:
      active_lights:
        value_template: "{{ states.light|selectattr('state','equalto','on')|list|count }}"

This sensor won’t update automatically so you’ll need an automation to do this:
automation.yaml:

  - alias: Update Light Count
    initial_state: True
    trigger:
      platform: event
      event_type: state_changed
    condition:
      condition: template
      value_template: "{{ trigger.event.data.entity_id.startswith('light') }}"
    action:
      service: homeassistant.update_entity
      entity_id: sensor.active_lights

Then add your card to your view:

  - type: 'custom:button-card'
    entity: sensor.active_lights
    gridcol: 1/6
    gridrow: 2/6
    state:
      - name: All lights off.
        value: 0
      - name: 1 light on.
        value: 1
      - name: '[[[ return `${entity.state} lights on.` ]]]'
        operator: '>='
        value: 2
    template: header
    tap_action:
      action: more-info

Finally, define the popup card:

    popup_cards:
      sensor.active_lights:
        title: ' '
        card:
          type: 'custom:auto-entities'
          card:
            type: entities
            title: Active Lights
            show_header_toggle: true
          filter:
            include:
              - domain: light
                state: 'on'
          show_empty: true

Add this to your config as below:

title: My House
views:
  - path: home               #
    title: Home              #
    theme: Backend-selected  # Your view setup
    badges: []               #
    panel: true              #
    popup_cards:                              #
      sensor.active_lights:                   #
        title: ' '                            #
        card:                                 #
          type: 'custom:auto-entities'        #
          card:                               #
            type: entities                    # defining the popup
            title: Active Lights              # card
            show_header_toggle: true          #
          filter:                             #
            include:                          #
              - domain: light                 #
                state: 'on'                   #
          show_empty: true                    #
    cards:
      - type: 'custom:layout-card'    #
        column_width: 100%            # For setting up panel view
        layout: vertical              #
        cards:
          - type: 'custom:layout-card'                         #
            layout: grid                                       # Grid
            gridcols: 158px 158px 158px 158px 158px 158px      # setup
            gridrows: 158px 158px 158px 158px 158px 158px      #
            cards:
              - entity: sensor.active_lights       # the active lights card
                gridcol: 1/6
                gridrow: 2/6
                state:
                  - name: All lights off.
                    value: 0
                  - name: 1 light on.
                    value: 1
                  - name: '[[[ return `${entity.state} lights on.` ]]]'
                    operator: '>='
                    value: 2
                template: header
                tap_action:
                  action: more-info
                type: 'custom:button-card'
1 Like

… not since 11 November…

…really?! I completely missed that, sorry! Original post updated accordingly.

Wow. this is exactly what I was looking for. How does it look like on a mobile?

I use a modified layout for mobile and it looks like this. I have been slowly tweaking the design to make it more universal across browsers, and make it more customisable. There’s now better indicators for bulb brightness and percentages, and I have made the media and camera widgets much more consistent in design. Can upload code updates if anyone is interested!

I will be very grateful if you can share your code. I have noticed that the current one has not been updated with the latest layout card settings. For some reason, I am getting everything in the middle of the screen

Question around your light count template. I use light groups and then group lights into rooms.

e.g. Office light group contains 2 lights, lounge light group contains 3 lights.

If both of these rooms are on the light count is 7 as the light group is counted in the template. Anyway to avoid this and only count the individual lights?

Edit. Don’t worry I managed to do it expanding the group and counting the entities that show on.

1 Like

Updated: 30/03/2021

So with layout-card being rewritten to include better CSS support for grid elements I took the time to simplify and rewrite parts of my setup. The new layout features require layout-card 2.0 or later to work properly.

Most of my button-card templates now draw basic elements from a base template - this means you can edit multiple cards simultaneously just by changing properties in the base template.

I’ve also tweaked some UI elements to make it prettier and more consistent.

Also improved:

  • Auto-generating/iterating grids, less work to add more elements and move them around
  • Simplified yaml with fewer stacked elements
  • Ability to adjust aspects of all cards
  • Circular elements to display additional data, and can show a partial circle for percentage properties, using the circle and circle_dynamic templates

How to set up

Set YAML mode for Lovelace, install button-card and layout-card.

ui-lovelace.yaml:

button_card_templates: !include ui-resources.yaml #required for button card templates
popup_cards: !include ui-popups.yaml #if using browser

title: My Home
views:
  - title: Home
    path: home
    type: custom:grid-layout #new simpler grid setting
    layout:
      grid-auto-columns: 150px #default card width is 150px (small) or 308px (medium)
      grid-auto-rows: 150px #default card height is 150px
      grid-template-rows: 50px 30px 50px #use if you want some specific row heights
      grid-column-gap: 8px #default size
      grid-row-gap: 8px #default size
    cards:
      - type: 'custom:button-card' #your first card
        template: greeting #template specified in ui-resources.yaml
        view_layout:
          grid-column-start: 1 #grid x coordinate
          grid-row-start: 1 #grid y coordinate

See next post for ui-resources.yaml and templates.

1 Like

How to set up (part 2)

ui-resources.yaml:

  base: #this is used by most cards for base characteristics
    color: var(--state-icon-active-color)
    show_state: true
    styles:
      card:
        - width: 150px
        - height: 150px
      icon: 
        - width: 60px
        - height: 60px
        - top: 5px
        - left: 10px
        - position: absolute
      name:
        - bottom: 25px
        - left: 10px
        - position: absolute
        - justify-self: start
      state:
        - bottom: 5px
        - left: 10px
        - position: absolute
        - justify-self: start
        - font-weight: lighter

  circle: #this puts a data field in a circle on the card
    custom_fields:
      circle: >
        [[[
            const input = variables.circle_input;
            const radius = 23;
            const circumference = radius * 2 * Math.PI;
            return `
              <svg viewBox="0 0 50 50">
                <circle cx="25" cy="25" r="${radius}" stroke="var(--disabled-text-color)" stroke-width="3" fill="none" />
                <text x="50%" y="52%" fill="var(--primary-text-color)" font-size="12" text-anchor="middle" alignment-baseline="middle">${input}</text>
              </svg>
            `;
        ]]]
    styles:
      custom_fields:
        circle:
          - top: 15px
          - right: 15px
          - width: 50px
          - position: absolute

  circle_dynamic: #as per circle, but the circle will be partially complete depending on percentage
    custom_fields:
      circle_dynamic: >
        [[[
          if (entity.state === 'on' || entity.state === 'Printing') {
            const input = variables.circle_input;
            const radius = 23;
            const circumference = radius * 2 * Math.PI;
            return `
              <svg viewBox="0 0 50 50">
                <style>
                  circle {
                    transform: rotate(-90deg);
                    transform-origin: 50% 50%;
                    stroke-dasharray: ${circumference};
                    stroke-dashoffset: ${circumference - input / 100 * circumference};
                  }
                </style>
                <circle cx="25" cy="25" r="${radius}" stroke="var(--disabled-text-color)" stroke-width="3" fill="none" />
                <text x="50%" y="52%" fill="var(--primary-text-color)" font-size="12" text-anchor="middle" alignment-baseline="middle">${input}%</text>
              </svg>
            `;
          }
        ]]]
    styles:
      custom_fields:
        circle_dynamic:
          - top: 15px
          - right: 15px
          - width: 50px
          - position: absolute

  alarm: #for alarm_control_panel entities
    lock:
      enabled: true
    template:
      - base
    state:
      - icon: 'mdi:shield-check'
        styles:
          icon:
            - color: var(--label-badge-green)
        value: disarmed
      - icon: 'mdi:shield-lock'
        styles:
          icon:
            - color: var(--label-badge-red)
        value: armed_away
    styles:
      lock:
        - position: absolute
        - top: 19px
        - right: 12px
    tap_action:
      action: call-service
      service: script.alarm_toggle
  
  camera: #to display a live camera feed
    template:
      - base
    show_live_stream: true
    show_state: false
    custom_fields:
      background1: " "
      background2: " "
    styles:
      card:
        - width: 308px
        - z-index: 2
      img_cell:
        - top: -10px
        - left: -10px
        - z-index: 1
        - width: 318px
        - height: 156px
      entity_picture:
        - width: 318px
        - position: absolute
      name:
        - z-index: 4
      custom_fields:
        background1:
          - z-index: 3
          - bottom: 0px
          - left: 0px
          - position: absolute
          - width: 308px
          - height: 75px
          - background-image: >
              [[[ return `linear-gradient(to top, var(--card-background-color), var(--card-background-color-transparent)` ]]]
        background2:
          - z-index: 3
          - bottom: 0px
          - left: 0px
          - position: absolute
          - width: 154px
          - height: 150px
          - background-image: >
              [[[ return `linear-gradient(to right, var(--card-background-color), var(--card-background-color-transparent)` ]]]

  climate: #for climate entities
    template:
      - base
      - circle
    state_display: >-
      [[[ return `Set: ${(entity.attributes.temperature)}°C` ]]]
    variables:
      circle_input: >
        [[[ return `${(entity.attributes.current_temperature)}°C` ]]]
    state:
      - styles:
          card:
            - filter: opacity(25%)
          icon:
            - filter: grayscale(100%)
          state:
            - filter: opacity(0%)
        value: unavailable


  default: #a starting card if you're not sure what to use
    template:
      - base
    color: var(--state-icon-active-color)
    hold_action:
      action: more-info
    state:
      - styles:
          card:
            - filter: opacity(50%)
          icon:
            - filter: grayscale(100%)
        value: 'off'
      - styles:
          card:
            - filter: opacity(25%)
          icon:
            - filter: grayscale(100%)
        value: unavailable
    tap_action:
      action: toggle
  
  garbage: #for use with the Garbage Collection (HACS) sensors
    template:
      - base
    tap_action:
      action: none
    state_display: >-
      [[[ if (entity.state == "2") return `in ${entity.attributes.days} days`;
      else if (entity.state == "1") return "Tomorrow";
      else if (entity.state == "0") return "Today"
      ]]]
  
  greeting: #Good Morning/Afternoon/Evening
    name: >-
      [[[ var d = new Date(); var n = d.getHours(); if (0 <= n && n
      < 12) return "Good morning."; else if (12 <= n && n < 18)
      return "Good afternoon."; else if (18 <= n && n < 24) return
      "Good evening."; else return "ERROR";]]]
    styles:
      card:
        - width: 300px
        - height: 60px
        - background: none
        - box-shadow: none
      name:
        - justify-self: start
        - margin-left: 10px
        - font-weight: normal
        - font-size: xx-large
    tap_action:
      action: none
  
  header: #a basic header for pages
    tap_action:
      action: none
    show_icon: false
    show_name: true
    show_state: false
    styles:
      card:
        - width: 300px
        - height: 40px
        - background: none
        - box-shadow: none
      name:
        - justify-self: start
        - margin-left: 10px
        - font-weight: lighter
        - font-size: x-large
  
  light: #for light entities. brightness displayed in dynamic circle
    template:
      - base
      - circle_dynamic
    variables:
      circle_input: >
        [[[ return Math.round(entity.attributes.brightness / 2.54); ]]]
    color: auto-no-temperature
    hold_action:
      action: more-info
    show_state: true
    state:
      - styles:
          card:
            - filter: opacity(50%)
          icon:
            - filter: grayscale(100%)
          label:
            - background-color: var(--card-background-color)
        value: 'off'
      - styles:
          card:
            - filter: opacity(25%)
          icon:
            - filter: grayscale(100%)
          label:
            - background-color: var(--card-background-color)
        value: unavailable
      - operator: template
        value: >-
          [[[ if (entity.attributes.rgb_color == "255,255,255") return true; else return false ]]]
        styles:
          icon:
            - color: var(--state-icon-active-color)
    tap_action:
      action: toggle
  
  media: #for media players; set up for my Sonos system - cards will appear faded if media is paused
    template:
      - base
    state_display: >-
      [[[ if (entity.attributes.media_title == undefined) return `${entity.state}`[0].toUpperCase() + `${entity.state}`.substring(1);
      else if (entity.attributes.media_artist == undefined) return `${entity.state}`[0].toUpperCase() + `${entity.state}`.substring(1) + `: ${entity.attributes.media_title}`;
      else return `${entity.state}`[0].toUpperCase() + `${entity.state}`.substring(1) + `: ${entity.attributes.media_title} - ${entity.attributes.media_artist}` ]]]
    state:
      - styles:
          card:
            - filter: opacity(50%)
          icon:
            - filter: grayscale(100%)
        value: 'paused'
      - styles:
          card:
            - filter: opacity(25%)
          icon:
            - filter: grayscale(100%)
        value: unavailable
    custom_fields:
      background1: " "
      background2: " "
    styles:
      card:
        - background-size: cover
        - background-image: '[[[ return `url("${entity.attributes.entity_picture}")` ]]]'
        - background-position: center center
        - width: 308px
        - height: 150px
      icon: 
        - z-index: 2
      name:
        - z-index: 2
      state:
        - z-index: 2
      custom_fields:
        background1:
          - top: 75px
          - right: 0px
          - position: absolute
          - width: 308px
          - height: 75px
          - background-image: >
              [[[ return `linear-gradient(to top, var(--card-background-color), var(--card-background-color-transparent)` ]]]
        background2:
          - top: 0px
          - right: 154px
          - position: absolute
          - width: 154px
          - height: 150px
          - background-image: >
              [[[ return `linear-gradient(to right, var(--card-background-color), var(--card-background-color-transparent)` ]]]
  
  person: #for person entities
    template:
      - base
    show_entity_picture: true
    state:
      - operator: '!='
        styles:
          card:
            - filter: opacity(50%)
          icon:
            - filter: grayscale(100%)
        value: home
    styles:
      icon:
        - border-radius: 50%
  
  sensor: #for sensor entities. needs work
    template:
      - base

  task: #keep track of chores, displays when chore last done using an input_datetime, tap the card to reset the timer
        #cards go red when the chore is overdue, defaults to 7 days
        #to change the overdue limit on a specific card in ui-lovelace.yaml, add:
        #variables:
        #  days: x (where x is number of days)
    template:
      - base
    variables:
      days: 7
    state_display: >-
      [[[
      return html`
        <ha-relative-time
          .hass="${hass}"
          .datetime="${(entity.attributes.timestamp)*1000}"
        ></ha-relative-time>`
      ]]]
    state:
      - operator: template
        value: >-
          [[[ if (Math.round(Date.now()/1000) - entity.attributes.timestamp > (variables.days*86400)) return true; else return false ]]]
        styles:
          icon:
            - color: var(--label-badge-red)
    tap_action:
      action: call-service
      service: input_datetime.set_datetime
      service_data:
        entity_id: entity
        timestamp: '[[[return Math.round(Date.now()/1000);]]]'
      confirmation:
        text: '[[[ return `Complete this task?` ]]]'
    hold_action:
      action: more-info

  weather: #for weather entities. I have only tested with Dark Sky
    template:
      - base
      - circle
    variables:
      circle_input: '[[[ return `${entity.attributes.temperature}°C` ]]]'
    show_state: true
    state:
      - icon: 'mdi:weather-night'
        value: clear-night
      - icon: 'mdi:weather-cloudy'
        value: cloudy
      - icon: 'mdi:weather-fog'
        value: fog
      - icon: 'mdi:weather-hail'
        value: hail
      - icon: 'mdi:weather-lightning'
        value: lightning
      - icon: 'mdi:weather-lightning-rainy'
        value: lightning-rainy
      - icon: 'mdi:weather-partly-cloudy'
        value: partlycloudy
      - icon: 'mdi:weather-pouring'
        value: pouring
      - icon: 'mdi:weather-rainy'
        value: rainy
      - icon: 'mdi:weather-snowy'
        value: snowy
      - icon: 'mdi:weather-snowy-rainy'
        value: snowy-rainy
      - icon: 'mdi:weather-sunny'
        value: sunny
      - icon: 'mdi:weather-windy'
        value: windy
      - icon: 'mdi:weather-windy-variant'
        value: windy-variant
      - icon: 'mdi:weather-cloudy-alert'
        value: exceptional

Examples of cards

Alarm:
Screenshot 2021-03-30 at 23.19.14

Camera:
Screenshot 2021-03-30 at 23.21.00

Climate:
Screenshot 2021-03-30 at 23.19.28

Default:
Screenshot 2021-03-30 at 23.20.29

Garbage:
Screenshot 2021-03-30 at 23.19.23

Greeting:
Screenshot 2021-03-30 at 23.19.32

Header:
Screenshot 2021-03-30 at 23.23.01

Light:
Screenshot 2021-03-30 at 23.19.57

Media:
Screenshot 2021-03-30 at 23.20.09

Person:
Screenshot 2021-03-30 at 23.19.08

Sensor:
Screenshot 2021-03-30 at 23.20.22

Task:
Screenshot 2021-03-30 at 23.21.12

Weather:
Screenshot 2021-03-30 at 23.19.18

Hey

So im using the above but have warnings in my logs about the automation already running. Do you ge this?

Yes. Doesn’t affect functionality but something I’ve been meaning to fix. Usually happens if several lights get turned on simultaneously. Will fix at some point

EDIT: I don’t get this any more because I use nodered for the automation - however try this and let me know if it works:

- alias: Update Light Count
    initial_state: True
    mode: restart    #this line is new
    trigger:
      platform: event
      event_type: state_changed
    condition:
      condition: template
      value_template: "{{ trigger.event.data.entity_id.startswith('light') }}"
    action:
      service: homeassistant.update_entity
      entity_id: sensor.active_lights

Did the floors get mopped yet? I laughed when I saw that todo as mine need mopping too.

1 Like

4 months later and the floors still need mopping :rofl:

2 Likes