[Carte] Carte soleil avec button-card

Salut à tous,
J'ai fait une carte pour afficher l'état du soleil avec le prochain lever/coucher du soleil et dans combien de temps. Une barre d'avance est incluse avec une icône de soleil/lune suivant l'état du soleil. Le badge est pour afficher les UV du soleil en journée et sera caché la nuit.
L'entité utilisée pour l'UV est celle fournie par l'intégration Météo France, penser à remplacer le nom de l'entité.

Installer button-card depuis HACS.

les icônes animées, à placer dans /config/www/weather/animated ou sous file editor, c'est /homeassistant/www/weather/animated.

Code
type: custom:button-card
entity: sun.sun
update_timer: 10s
show_icon: false
show_name: true
show_label: true
show_state: false
show_entity_picture: true
tap_action:
  action: more-info
hold_action:
  action: more-info
  entity: sensor.xxxx_uv
entity_picture: |
  [[[
    return entity.state == "below_horizon"
      ? "/local/weather/animated/night.svg"
      : "/local/weather/animated/day.svg";
  ]]]
name: |
  [[[
    const sunrise = new Date(states["sensor.sun_next_rising"].state);
    const sunset = new Date(states["sensor.sun_next_setting"].state);

    function heure(d){
      return d.toLocaleTimeString("fr-FR",
      {hour:"2-digit",minute:"2-digit"}).replace(":","h");
    }

    if (sunrise < sunset)
      return `Soleil sous l'horizon | Prochain lever: ${heure(sunrise)}`;

    return `Soleil au-dessus de l'horizon | Prochain coucher: ${heure(sunset)}`;
  ]]]
label: |
  [[[
      const sunrise = new Date(states["sensor.sun_next_rising"].state);
      const sunset = new Date(states["sensor.sun_next_setting"].state);

      const next = sunrise < sunset ? sunrise : sunset;

      let sec = Math.floor((next - new Date()) / 1000);

      if (sec <= 0)
        return "Maintenant";

      const d = Math.floor(sec / 86400);
      sec %= 86400;
      const h = Math.floor(sec / 3600);
      sec %= 3600;
      const m = Math.floor(sec / 60);
      const s = sec % 60;

      let txt = "Dans ";
      
      if (d > 0) txt += `${d}j `;
      
      if (h > 0) {
        txt += `${String(h).padStart(2,"0")}h`;
        txt += `${String(m).padStart(2,"0")}m`;
      } else if (m > 0) {
        txt += `${m}m`;
      }
      
      txt += `${String(s).padStart(2,"0")}s`;
      
      return txt;
  ]]]
custom_fields:
  uv: |
    [[[
      if (entity.state == "below_horizon") return "";
      return states["sensor.xxxx_uv"].state;
    ]]]
  progress: |
    [[[
      const sunrise = new Date(states['sensor.sun_next_rising'].state);
      const sunset = new Date(states['sensor.sun_next_setting'].state);
      const now = new Date();

      let start, end, icon, gradient, offset;

      if (sunrise < sunset) {
        // Nuit
        end = sunrise;
        start = new Date(sunrise);
        start.setDate(start.getDate()-1);

        icon = "🌙";
        offset = 17;
        gradient = "linear-gradient(90deg,#435CFF,#5C6BC0,#FFF59D)";
      } else {
        // Jour
        start = new Date(sunset);
        start.setDate(start.getDate()-1);
        end = sunset;

        icon = "☀️";
        offset = 12;
        gradient = "linear-gradient(90deg,#FFE082,#FFB300,#F57C00)";
      }

      let pct = ((now - start) / (end - start)) * 100;
      pct = Math.max(0, Math.min(100, pct));
      
      const iconPct = Math.max(2, Math.min(98, pct));

      return `
      <div style="
          position:relative;
          width:100%;
          height:8px;
          border-radius:999px;
          background:rgba(255,255,255,.18);
          overflow:visible;">

          <div style="
              width:${pct}%;
              height:100%;
              border-radius:999px;
              background:${gradient};">
          </div>

          <div style="
              position:absolute;
              left: calc(${iconPct}% - ${offset}px);
              top:20%;
              transform:translateY(-55%);
              font-size:19px;
              transition:left 1s linear;">
              ${icon}
          </div>

      </div>`;
    ]]]   
styles:
  card:
    - height: 58px
    - padding: 3px 0px
    - border-radius: 12px
    - overflow: visible
    - overflow: hidden
    - background: |
        [[[
          return entity.state=="below_horizon"
          ? "linear-gradient(180deg,#40445a 0%, rgba(64,68,90,.15) 250%)"
          : "linear-gradient(180deg,#44739e 0%, rgba(68,115,158,.15) 250%)";
        ]]]
    - "--button-card-ripple-icon-hover-opacity": 0.2
    - "--button-card-ripple-icon-pressed-opacity": 0.3
    - "--button-card-ripple-color": |
        [[[
          return entity.state=="below_horizon"
          ? "#f9e491"
          : "#ffe54d";
        ]]]      
  grid:
    - grid-template-areas: |
        "i n"
        "i l "
        "i progress"
    - grid-template-columns: 54px 1fr
    - grid-template-rows: min-content min-content 7px
  img_cell:
    - width: 54px
    - height: 54px
    - justify-content: center
    - align-items: center
  icon:
    - width: 80px
    - height: 80px
  name:
    - align-self: end
    - justify-self: start
    - font-size: 16px
    - font-weight: 500
    - color: white
  label:
    - justify-self: start
    - align-self: start
    - font-size: 15px
    - font-weight: bold
    - padding-bottom: 8px
    - color: |
        [[[
          return entity.state=="below_horizon"
          ? "#f9e491"
          : "#ffe54d";
        ]]]
  custom_fields:
    progress:
      - justify-self: stretch
      - align-self: center
      - overflow: visible
      - width: calc(100% - 10px)
      - padding-bottom: 12px
      - filter: drop-shadow(0 1px 2px rgba(0,0,0,.35))
    uv:
      - position: absolute
      - top: 3px
      - left: 34px
      - width: 16px
      - height: 16px
      - border-radius: 50%
      - display: |
          [[[
            return entity.state == "below_horizon"
              ? "none"
              : "flex";
          ]]]
      - justify-content: center
      - align-items: center
      - font-size: 11px
      - font-weight: bold
      - color: white
      - z-index: 10
      - box-shadow: 0 2px 5px rgba(0,0,0,.35)
      - background-color: |
          [[[
            const uv = Number(states['sensor.xxxx_uv'].state);

            if (uv >= 11) return "purple";
            if (uv >= 8) return "firebrick";
            if (uv >= 6) return "darkorange";
            if (uv >= 3) return "gold";
            return "forestgreen";
          ]]]

Vous avez la base, changez si besoin les couleurs à votre guise.
Bonne journée.

Très belle carte, Merci.

Une petite remarque pour les images affichés, il est indiqué un indice UV de 8 pendant la nuit. :grinning_face:

Oui, c'est le sensor météo France qui affiche des données d'UV la nuit. :rofl:
Bonne remarque, je vais corriger ça pour pas qu'il soit affiché la nuit.

edit:
corriger en ajoutant :

  uv: |
    [[[
      if (entity.state == "below_horizon") return "";
      return states["sensor.xxxx_uv"].state;
    ]]]
      - display: |
          [[[
            return entity.state == "below_horizon"
              ? "none"
              : "flex";
          ]]]

le badge ne s'affichera pas quand le soleil est sous l'horizon. je corrige le code sur le sujet.

Hello,

Il fait déjà assez chaud la journée sans en plus bronzer la nuit :smiley:

cdt

Une petite requête,

J’utilise les données UV de ma station météo, l’état est un nombre float avec deux chiffres après la virgule que je veux garder. Il y aurait-il un moyen de convertir le nombre qui s’affiche dans le badge en int directement dans votre code? Pour l’instant, j’ai créé un sensor qui fait la conversion.

Voici des exemples suivant comment tu veut arrondir :

Par exemple, si ton capteur renvoie 5.67 et que tu veux afficher 6 :

uv: |
  [[[
    if (entity.state == "below_horizon") return "";
    return Math.round(Number(states["sensor.xxxx_uv"].state));
  ]]]

Ou si tu préfères tronquer (5.67 → 5) :

uv: |
  [[[
    if (entity.state == "below_horizon") return "";
    return Math.floor(Number(states["sensor.xxxx_uv"].state));
  ]]]

Ou encore afficher toujours sans décimales (arrondi classique) :

uv: |
  [[[
    if (entity.state == "below_horizon") return "";
    return Number(states["sensor.xxxx_uv"].state).toFixed(0);
  ]]]

Quelle méthode choisir ?

  • Math.round()5.5 devient 6 (arrondi classique) :white_check_mark:
  • Math.floor()5.9 devient 5 (arrondi inférieur)
  • Math.ceil()5.1 devient 6 (arrondi supérieur)
  • toFixed(0) → affiche une chaîne sans décimales (équivalent visuel à Math.round()).

Pour un indice UV, je te recommande Math.round(), c'est généralement le comportement le plus naturel.

Merci , fonctionne très bien Math.round() :grinning_face:

Mise à jour du code dans le topic, décalage à gauche de l'icône lune pour masquer l'espace vide sur la barre.
Avant :
image
Après :
image

Modification de l'affichage du temps restant.
image

Exemples

Valeur Affichage
02h 15m 08s Dans 02h15m08s
15m 08s Dans 15m08s
08s Dans 08s
01h 00m 08s Dans 01h08s
00h 05m 08s Dans 05m08s

Super carte!
Tu penses pouvoir l’intégrer dans HACS?
Ça éviterait de passer par ici pour les mises à jour

Non, vu que c'est une carte faite avec button-card. J'ai pas codé la carte :wink:
J'ai codé le code pour button-card.
Il faut juste installer button-card depuis HACS et utiliser le code.

@MichelJ , j'aime beaucoup ta carte de prévisions.
Pourrais-tu partager le code STP?
Merci d'avance

C'est cette carte :

Attention :wink:

Merci.
A tester. Il va me falloir prévoir un peu de temps pour lire toutes options possibles et y rattacher les prévisions de Météo France !

Voici le code de ma configuration si ça peut vous aidez. Comme vous le dite, il y a beaucoup d’options.

```
 - type: custom:weather-forecast-card
    entity: weather.pirateweather
    show_current: true
    show_forecast: true
    name: Shawinigan-Sud
    default_forecast: daily
    show_condition_effects: true
    current:
      show_attributes:
        - name: humidity
          entity: sensor.st_00032986_humidite
        - name: pressure
          entity: sensor.st_00032986_pression_atmospherique
        - name: uv_index
          entity: sensor.st_00032986_index_uv
        - name: visibility
          entity: sensor.shawinigan_sud_sensors_visibility
      secondary_info_attribute: apparent_temperature
      temperature_entity: sensor.st_00032986_temperature
      temperature_precision: 1
    forecast:
      mode: chart
      extra_attribute: wind_direction
      scroll_to_selected: true
      use_color_thresholds: true
      daily_slots: 7
      show_attribute_selector: false
      temperature_precision: 1

```

Avec un RPI 5, je n’ai pas eu de problème performances.

Bonjour,

Je rencontre un soucis, j'ai mis la carte mais les icone animé soleil et lune à gauche ne saffiche pas.

Je les ai mis dans www:weather.animated c'est ç dire /homeassistant/www/weather/animated.
Chez moi le dossier racine n'est pas config mais homeassistant. et le chemin /local/www/weather/animated ne fonctionne pas

Si vous avez une idée, j'ai tout vérifier les casses, orthographe, vider le cache, droit du dossier..

Edit : j'ai oublié de dire aussi que mon_ip:8123/local/weather/animated/day.svg me met une erreur 404

Salut,

Avec Studio Code Server, tu peux copier le chemin du path, ce qui donne chez moi /config/www/weather/day.svg

Essaie /local/weather/animated sans le www

Bonjour,
c'est normal, sous File Editor, le dossier /config est renommé en /homeassistant. C'est le bon dossier, j'ai modifié mon post pour le mentionner et éviter les problèmes de compréhension.

J'ai testé, ça fonctionne bien avec ce lien.
La chose à faire est d'essayer de vider le cache de votre navigateur.

le chemin est déjà dans le code /local/weather/animated/