[ConcoursDash] carte agenda + tâches'

Salut, voici une carte qui n’est pas réellement liée à ma maison, mais plutôt à mes activités pro et perso. J’avais un certain nombres d’outils pense-bête (agenda - tasks - keep chez Google, et Asana qui est un gestionnaire de projets, en gros des tâches mais qu’on peut affecter à quelqu’un et dont on peu gérer les échéances et les interactions). Une app pour chaque outil et les difficultés qui vont avec pour partager le contenu. J’ai tout centralisé dans HA avec une carte maison, construite avec button-card:


D’un seul coup d’œil je vois mes événements en cours ou à venir pour chaque calendrier, le nombre de tâches actives, si une tâche demande une attention particulière (le signe exclamation rouge)… Etc…
Toutes les icônes sur la gauche amènent à une sous vue qui détaille seulement les calendriers, c’est une carte HACS week-planner-card:

Les icônes de droite ouvrent un popup avec la carte HACS bubble-Card, qui contient une autre carte HACS, cette fois-ci flex-table-card.
Les données proviennent soit de Google Tasks:

Soit de Asana:

Je ne développerai pas ici car c’est une petite usine à gaz mais j’ai du créé des composants pour autoriser de vraies interactions avec ces deux services ( n’hésitez pas à MP si quelqu’un est intéressé par ça, je créerai un sujet dédié).

Pour finir, il y a une icône de météo pour le département d’à côté où je bosse régulièrement, qui évidemment permet de naviguer vers une subview plus développée.
Pour revenir à la carte de base, voici le code suivi des templates auquel il fait appel.

#//////////////////////////////////////////////////////////////////////////////////////#
#///////                                                                 //////////////#
#/////// CARD: Calendars
#///////                                                                 //////////////#
#//////////////////////////////////////////////////////////////////////////////////////#

type: custom:vertical-stack-in-card
# card_mod:
#   class: stock # not stack
cards:
  # ============================================================================== mushu
  - type: custom:button-card
    template:
      - calendar_todo
    variables:
      name: Mushu
      color: var(--fire)
      bubble_path: "#mushu"
      google_calendar: calendar.google_mushu
      tplt_calendar: binary_sensor.tplt_calendar_mushu
      todo_list: todo.google_mushu

  # ========================================================================== gabrechal
  - type: custom:button-card
    template:
      - calendar_todo
    variables:
      name: Gabrechal
      color: var(--nova-color)
      bubble_path: "#gabrechal"
      google_calendar: calendar.google_gabrechal
      tplt_calendar: binary_sensor.tplt_calendar_gabrechal
      todo_list: todo.google_gabrechal

  # ============================================================================== olive
  - type: custom:button-card
    template:
      - calendar_todo
    variables:
      name: Olive
      color: var(--yellow)
      bubble_path: "#olive"
      google_calendar: calendar.google_olive
      tplt_calendar: binary_sensor.tplt_calendar_olive
      todo_list: todo.google_olive

  # ======================================================================== asana olive
  - type: conditional
    conditions:
      - condition: user
        users:
          # - b51e228b56384578a8ac0b5b731b9c4b # nova user
          # - daa087b7316e4133bdc52448c0d2dc75 # hometab user
          - 5bb507e8c5f845028511086983cbabf5 # olive user
    card:
      type: custom:button-card
      template:
        - asana
      variables:
        title: Tâches
        entity: binary_sensor.asana_tasks_perso_olive
        color: var(--accent-color)
        tasks_path: "#asana_perso"
        create_path: "#asana_perso_create"

  # =============================================================================== nova
  # - type: conditional
  #   conditions:
  #     - condition: user
  #       users:
  #         - b51e228b56384578a8ac0b5b731b9c4b # nova user
  #         # - daa087b7316e4133bdc52448c0d2dc75 # hometab user
  #         # - 5bb507e8c5f845028511086983cbabf5 # olive user
  #   card:
  #     type: custom:button-card
  #     template:
  #       - asana
  #     variables:
  #       title: Nova
  #       entity: binary_sensor.asana_tasks_nova
  - type: custom:button-card
    template:
      - calendar_todo
    variables:
      name: Nova
      color: var(--water)
      # bubble_path: "#nova"
      bubble_path: "#it"
      google_calendar: calendar.google_nova
      tplt_calendar: binary_sensor.tplt_calendar_nova
      # todo_list: todo.google_nova
      todo_list: todo.google_it

  # ========================================================================= asana nova
  - type: conditional
    conditions:
      - condition: user
        users:
          - b51e228b56384578a8ac0b5b731b9c4b # nova user
          # - daa087b7316e4133bdc52448c0d2dc75 # hometab user
          # - 5bb507e8c5f845028511086983cbabf5 # olive user
    card:
      type: custom:button-card
      template:
        - asana
      variables:
        title: Tâches
        entity: binary_sensor.asana_tasks_perso_nova
        color: var(--accent-color)
        tasks_path: "#asana_perso"
        create_path: "#asana_perso_create"

  # ===================================================================== asana mcorp
  - type: custom:button-card
    template:
      - asana
    variables:
      title: Tâches mCorp
      entity: binary_sensor.asana_tasks_marecorp
      color: var(--sky)
      tasks_path: "#asana_marecorp"
      create_path: "#asana_marecorp_create"
    styles: &third_button
      custom_fields:
        button3:
          - left: calc( 100% - 124px + 16px) # calc( 100% - 152px) +style16px
    custom_fields:
      button3:
        card:
          <<: &meteo_marecorp
            type: custom:button-card
            tap_action:
              action: navigate
              navigation_path: "#meteo_marecorp"
            entity: weather.30_sauveterre
          template:
            - icon
          extra_styles: |
            @keyframes wind {
              0% { transform: translate(0px, 0px) rotate(0deg); }
              15% { transform: translate(4px, -3px) rotate(5deg); }
              30% { transform: translate(1px, 1px) rotate(-4deg); }
              45% { transform: translate(6px, 3px) rotate(3deg); }
              60% { transform: translate(-5px, -2px) rotate(-1deg); }
              75% { transform: translate(4px, -4px) rotate(-5deg); }
              100% { transform: translate(0px, 0px) rotate(0deg); }
            }
          styles:
            card:
              - <<: &meteo_card
                  - width: 68px # card48px + img_cell38px + gap4px
                  - height: 48px # card48px + img_cell38px + gap4px
                  - background: var(--secondary-background-color)
                  - border-radius: 50% 0 0 50%
                  - box-shadow: -8px 0px 16px var(--secondary-background-color) # mask text below
            img_cell: &meteo_img_cell
              - margin-left: -28px
            icon: &meteo_icon
              - color: |
                  [[[
                    if (states['binary_sensor.openmeteo_wind_30'].state === 'on') {
                      return 'var(--warning-color)' }
                    else {
                      return 'var(--accent-color)' }
                  ]]]
              - animation: |
                  [[[
                    if (states['binary_sensor.openmeteo_wind_30'].state === 'on') {
                      return 'wind 3s infinite' }
                  ]]]
            custom_fields: &meteo_custom_fields
              temp:
                - position: absolute
                - left: 40px # img_cell38px + gap4px
                - font-size: 14px
                - color: var(--primary-text-color)
          custom_fields:
            temp:
              card:
                <<: *meteo_marecorp
                template:
                  - value
                state_display: "[[[ return Math.round(states['weather.30_sauveterre'].attributes.temperature) + '°' ]]]"

  # =============================================================================== work
  - type: custom:button-card
    template:
      - calendar_todo
    variables:
      name: Calendrier mCorp
      color: var(--sky)
      bubble_path: "#work"
      google_calendar: calendar.google_mcorp
      tplt_calendar: binary_sensor.tplt_calendar_work
      todo_list: todo.google_work

  # - spé Nova
  - type: conditional
    conditions:
      - condition: user
        users:
          - b51e228b56384578a8ac0b5b731b9c4b # nova user
          # - daa087b7316e4133bdc52448c0d2dc75 # hometab user
          # - 5bb507e8c5f845028511086983cbabf5 # olive user
    card:
      type: custom:vertical-stack-in-card
      # card_mod:
      #   class: stock
      cards:
        # ==================================================================== birthdays
        - type: conditional
          conditions:
            - condition: state
              entity: binary_sensor.tplt_calendar_birthdays
              state: "on"
          card:
            type: custom:button-card
            template:
              - calendar
            entity: calendar.google_birthdays
            name: Evènements
            icon: mdi:cake-variant
            variables:
              tplt_calendar: binary_sensor.tplt_calendar_birthdays

        # ============================================================== epic free games
        - type: conditional
          conditions:
            - condition: state
              entity: calendar.epic_games_store_jeux_gratuits
              state: "on"
          card:
            type: custom:button-card
            template:
              - full
            entity: calendar.epic_games_store_jeux_gratuits
            name: Jeu Gratuit Epic
            styles:
              icon:
                - color: var(--primary-text-color)
            state_display: "[[[ return entity.attributes.message; ]]]"
            label: >
              [[[ 
                let end_time = entity.attributes.end_time;
                let stamp = new Date(end_time);
                return "Termine le " + stamp.getDate() + "/" + '0'+(stamp.getMonth()+1) + " à " + stamp.toLocaleTimeString().slice(0,-3)
              ]]]
            icon_tap_action:
              action: fire-dom-event
              browser_mod:
                service: browser_mod.popup
                data:
                  initial_style: classic
                  adaptive: true
                  content:
                    type: markdown
                    card_mod:
                      # class: stock
                      style:
                        .: |
                          ha-markdown {
                            padding: 0px 0px 0px 0px !important;
                          }
                        ha-markdown $:
                          ha-markdown-element p ha-alert $: |
                            .issue-type::after {
                              border-radius: 8px !important;
                            }
                    content: |-
                      <ha-alert alert-type="info">Descriptif du jeu gratuit du moment:</ha-alert>
                      {{ state_attr('calendar.epic_games_store_jeux_gratuits','description') }}
                  right_button: "retour" 

Les templates:


# ---------------------------------------------------------------- calendar solo
calendar:
  template:
    - full
  name: "[[[ return variables.name ]]]"
  # entity: "[[[ return variables.google_calendar ]]]"
  entity: |
    [[[
      const cal = states[variables.google_calendar]
      if (cal !== undefined)
        return variables.google_calendar
      else
        return "sun.sun"
    ]]]
  icon_tap_action:
    action: fire-dom-event
    browser_mod:
      service: browser_mod.navigate
      data:
        path: /main-dashboard/agenda
  # triggers_update:
  #   - "[[[ return variables.google_calendar ]]]"
  #   - "[[[ return variables.tplt_calendar ]]]"
  state_display: "[[[ return states[variables.tplt_calendar].attributes.event_name ]]]"
  label: "[[[ if (entity.state === 'on') return states[variables.tplt_calendar].attributes.current_event_end ; return states[variables.tplt_calendar].attributes.next_event_start ]]]"
  styles:
    img_cell:
      - background-color: "[[[ if (entity.state === 'on') return 'var(--accent-color)' ]]]"
    icon:
      - color: "[[[ if (entity.state === 'on') return 'var(--accent-text-color)'; return 'var(--primary-text-color)' ]]]"
    label:
      - color: "[[[ if (entity.state === 'on') return 'var(--accent-color)'; return 'var(--secondary-text-color)' ]]]"
    custom_fields:
      bubble:
        - color: "[[[ if (entity.state === 'on') return 'transparent' ]]]"


calendar_todo:
  template:
    - calendar
  state_display: |
    [[[ 
      const tplt = states[variables.tplt_calendar];
      if (tplt && tplt.state === 'on') {
        return (tplt.attributes && tplt.attributes.event_name) ? tplt.attributes.event_name : 'Évènement (nom inconnu)';
      }
      return 'Aucun évènement';
    ]]]
  label: |
    [[[ 
      const cal = states[variables.google_calendar];
      const tplt = states[variables.tplt_calendar];
      const todo = states[variables.todo_list];
      if (cal && tplt && cal.state === 'on')
        return (tplt.attributes.current_event_end) ? tplt.attributes.current_event_end : 'Fin inconnue';
      if (tplt && tplt.state === 'on') 
        return (tplt.attributes.next_event_start) ? tplt.attributes.next_event_start : 'Début inconnu';
      if (!todo || ['unknown', 'unavailable'].includes(todo.state))
        return 'État inconnu';
      const count = Number(todo.state);
      if (isNaN(count)) return 'État inattendu';
      switch (count) {
        case 0: return 'Aucune tâche';
        case 1: return '1 tâche';
        default: return `${count} tâches`;
      }
    ]]]

  styles:
    img_cell:
      - background: |
          [[[
            const cal = states[variables.tplt_calendar];
            if (!cal || !cal.attributes) return '';
            const running = (cal.attributes.running ?? 0) + '%';
            if (entity.state === 'on') 
              return 'radial-gradient(' 
                + variables.color + ',' 
                + variables.color + '55%, transparent 60%), '
                + 'conic-gradient(' 
                + variables.color + running 
                + ' 0%, var(--primary-background-color) 0% 100%)';
            return '';
          ]]]
    icon:
      - color: |
          [[[
            if (entity.state === 'on') 
              return 'var(--accent-text-color)';
            const cal = states[variables.tplt_calendar];
            if (cal && cal.attributes && cal.attributes.delay === 0) 
              return variables.color;
            return '';
          ]]]
    label:
      - color: |
          [[[
            const cal = states[variables.tplt_calendar];
            const todo = states[variables.todo_list];
            const calOn = cal && cal.state === 'on';
            const todoCount = todo ? Number(todo.state) : 0;
            if (calOn || (!isNaN(todoCount) && todoCount > 0))
              return variables.color;
            return 'var(--secondary-text-color)';
          ]]]
    custom_fields:
      bubble:
        - opacity: |
            [[[
              const cal = states[variables.tplt_calendar];
              if (cal && cal.attributes && cal.attributes.delay === 1) 
                return '1';
              return '0';
            ]]]
        - color: |
            [[[
              return variables.color;
            ]]]
      button1:
        - display: |
            [[[
              const todo = states[variables.todo_list];
              if (todo) 
                return '';
              return 'none';
            ]]]

  custom_fields:
    button1:
      card:
        type: custom:button-card
        template:
          - icon
        entity: "[[[ return variables.todo_list ]]]"
        icon: mdi:clipboard-list
        styles:
          card:
            - box-shadow: 0px 0px 12px 8px var(--secondary-background-color)
            - background: var(--secondary-background-color)

          img_cell:
            - background: |
                [[[
                  const todo = states[variables.todo_list];
                  const count = todo ? Number(todo.state) : 0;
                  if (!isNaN(count) && count === 0)
                    return 'var(--secondary-background-color)';
                  return variables.color;
                ]]]

          icon:
            - width: 24px
            - height: 24px
            - color: |
                [[[
                  const todo = states[variables.todo_list];
                  const count = todo ? Number(todo.state) : 0;
                  if (!isNaN(count) && count !== 0)
                    return 'var(--accent-text-color)';
                  return 'var(--secondary-text-color)';
                ]]]

          custom_fields:
            notification:
              - background-color: "[[[ return variables.color ]]]"
              - opacity: |
                  [[[
                    const todo = states[variables.todo_list];
                    const count = todo ? Number(todo.state) : 0;
                    if (!isNaN(count) && count === 0)
                      return '0';
                    return '1';
                  ]]]

        custom_fields:
          notification: |
            [[[
              const todo = states[variables.todo_list];
              if (!todo) return "X";
              const count = Number(todo.state);
              if (!isNaN(count)) return count;
              return "X";
            ]]]
        tap_action:
          action: |
            [[[
              const todo = states[variables.todo_list];
              return todo ? "navigate" : "none";
            ]]]
          navigation_path: |
            [[[
              const todo = states[variables.todo_list];
              return todo ? variables.bubble_path : "";
            ]]]

# ------------------------------------------------------------------------ asana
asana:
  template:
    - calendar
  entity: "[[[ return variables.entity ]]]"
  name: >
    [[[
      const total = entity.attributes.all_count || 0;
      const prefix = `<span style="font-weight: 600;">${variables.title}</span>`;
      let suffix = ""
      if (total > 0) {
        suffix = `<span style="font-weight: 300;">${' • ' + total + ' au total'}</span>`;
      }
      return `${prefix + suffix}`;
    ]]]
  state_display: |
    [[[ 
      const a = entity.attributes;
      const conditions = [
        { test: a.today_count > 0, count: a.today_count, period: 'aujourd\'hui' },
        { test: a.tomorrow_count > 0, count: a.tomorrow_count, period: 'demain' },
        { test: a.week_count > 0, count: a.week_count, period: 'cette semaine' },
        { test: a.month_count > 0, count: a.month_count, period: 'ce mois-ci' },
        { test: a.other_count > 0, count: a.other_count, period: 'sans échéance' },
        { test: a.all_count === 0, message: 'Aucune tâche' }
      ];
      const match = conditions.find(c => c.test);
      if (!match) return '';
      if (match.message) return match.message;
      const plural = match.count === 1 ? ' tâche ' : ' tâches ';
      return match.count + plural + match.period;
    ]]]
  # label: |
  #   [[[ 
  #     const a = entity.attributes;
  #     const today = a.today_count || 0;
  #     const tomorrow = a.tomorrow_count || 0;
  #     const week = a.week_count || 0;
  #     const month = a.month_count || 0;
  #     const total = today > 0 ? tomorrow + week : month;
  #     const plural = total === 1 ? ' tâche' : ' tâches';
  #     return total === 0 ? 'Voir détail' : 'Dont ' + total + plural + ' cette semaine';
  #   ]]]
  label: |
    [[[
      const today = entity.attributes.today_count || 0
      const tomorrow = entity.attributes.tomorrow_count || 0
      const week = entity.attributes.week_count || 0
      const month = entity.attributes.month_count || 0
      const other = entity.attributes.other_count || 0
      if (today > 0) {
        if (tomorrow > 0)
          return 'Et ' + tomorrow + ' tâche' + (tomorrow > 1 ? 's' : '') + ' demain'
        if (week > 0)
          return 'Et ' + week + ' tâche' + (week > 1 ? 's' : '') + ' cette semaine'
        if (month > 0)
          return 'Et ' + month + ' tâche' + (month > 1 ? 's' : '') + ' à venir'
        return 'Voir détail'
      }
      if (tomorrow > 0) {
        if (week > 0)
          return 'Et ' + week + ' tâche' + (week > 1 ? 's' : '') + ' cette semaine'
        if (month > 0)
          return 'Et ' + month + ' tâche' + (month > 1 ? 's' : '') + ' à venir'
        return 'Voir détail'
      }
      if (week > 0) {
        if (month > 0)
          return 'Et ' + month + ' tâche' + (month > 1 ? 's' : '') + ' à venir'
        return 'Voir détail'
      }
      if (other > 0)
        return 'Voir détail'
      return 'Well Done!'
    ]]]

  styles:
    img_cell:
      - background: |
          [[[
            if (entity.state === 'on' )
              return `${variables.color}`
            else return 'transparent'
          ]]]
    icon:
      - color: |
          [[[
            if (entity.state === 'on' )
              return 'var(--accent-text-color)'
          ]]]
    state:
      - color: |
          [[[
            const a = entity.attributes;
            if (a.overdue == 'active' ) return 'var(--error-color)';
            if (a.today_count > 0 ) return 'var(--nova-color)';
            if (a.tomorrow_count > 0 ) return 'var(--info-color)';
            return 'var(--primary-text-color)';
          ]]]
    label:
      - color: "[[[ if (entity.attributes.all_count > 0) return `${variables.color}`; return 'var(--secondary-text-color)' ]]]"
  custom_fields:
    button1:
      card:
        type: custom:button-card
        template:
          - icon
        entity: binary_sensor.asana_tasks_nova
        icon: mdi:dots-triangle
        styles:
          img_cell:
            - background: |
                [[[
                  if ((entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count) != 0) 
                    return `${variables.color}`; 
                  return 'var(--secondary-background-color)';
                ]]]
          icon:
            - width: 24px
            - height: 24px
            - color: |
                [[[
                  if ((entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count) != 0) 
                    return 'var(--accent-text-color)';
                  if ((entity.attributes.all_count ) != 0) 
                    return `${variables.color}`;
                  return 'var(--secondary-text-color)';
                ]]]
          custom_fields:
            notification:
              - color: |
                  [[[
                    if ((entity.attributes.today_count + entity.attributes.tomorrow_count ) === 0) 
                      return 'var(--accent-text-color)';
                    return 'var(--primary-background-color)';
                  ]]]
              - background-color: |
                  [[[
                    if (entity.attributes.overdue == 'active' ) return 'var(--error-color)';
                    if (entity.attributes.today_count > 0) return 'var(--nova-color)';
                    if (entity.attributes.tomorrow_count > 0) return 'var(--info-color)';
                    return `${variables.color}`;
                  ]]]
              - opacity: &notif_opa |
                  [[[
                    if ((entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count) === 0) return '0'; 
                    return '1'; 
                  ]]]
            severity:
              - position: absolute
              - top: -3px
              - left: 28px
              - opacity: *notif_opa
              # - height: 16px
              - width: 16px
        custom_fields:
          notification: |
            [[[
              const today = entity.attributes.today_count || 0
              const tomorrow = entity.attributes.tomorrow_count || 0
              const week = entity.attributes.week_count || 0
              if (today > 0)
                return today;
              if (tomorrow > 0)
                return tomorrow;
              if (week > 0)
                return week;
              return '';
            ]]]
          # notification: |
          #   [[[ return entity.attributes.today_count + entity.attributes.tomorrow_count + entity.attributes.week_count || "" ]]]
          severity: |
            [[[ return (entity.attributes.today_severity) == 'Urgent' ? '<ha-icon icon="mdi:alert" style="color: var(--error-color)"></ha-icon>':
                (entity.attributes.today_severity) == 'Important' ? '<ha-icon icon="mdi:star" style="color: var(--warning-color)"></ha-icon>':
                (entity.attributes.today_severity) == 'Achats' ? '<ha-icon icon="mdi:cart" style="color: var(--info-color)"></ha-icon>':
                (entity.attributes.today_severity) == 'Corvées' ? '<ha-icon icon="mdi:autorenew" style="color: var(--purple)"></ha-icon>':
                '';
            ]]]
        tap_action:
          action: navigate
          navigation_path: |
            [[[
              return (entity.attributes.all_count) == 0
                ? `${variables.create_path}`
                : `${variables.tasks_path}`;
            ]]]
        hold_action:
          action: perform-action
          perform_action: |
            [[[ 
              if (browser_mod?.browserID == "olivecell") 
                return "notify.mobile_app_olivecell";
              else if (browser_mod?.browserID == "novacell") 
                return "notify.mobile_app_novacell";
              else return null;
            ]]]
          data:
            message: "command_launch_app"
            data:
              package_name: "com.asana.app"

# ======================================================================================
# ======================================================================================
# ============================================================================== climate
# ---------------------------------------------------------------------- climate
climate:
  template:
    - full
  label: "[[[ return 'Consigne: ' + (typeof entity.attributes.temperature === 'number' ? entity.attributes.temperature + '°C' : 'N/C') ]]]"
  state_display: >
    [[[
      if (entity.state === 'heat')
        return 'En service'
      if (entity.state === 'auto')
        return 'Automatique'
      else
        return 'Hors service'
    ]]]
  styles:
    img_cell:
      - background-color: >
          [[[
            if (entity.state === 'heat')
            {
              if (entity.attributes.temperature > entity.attributes.current_temperature)
                return 'var(--warning-color)'
              else
                return 'var(--info-color)'
            }
            if (entity.state === 'auto')
              return 'var(--success-color)'
            else
              return 'var(--secondary-background-color)'
          ]]]
    icon:
      - color: "[[[ if (entity.state === 'heat' || entity.state === 'auto') return 'var(--accent-text-color)' ; return 'var(--secondary-text-color)' ]]]"

6 « J'aime »