Affichage graphique des Heures pleines/creuses

J’ai fait ça


type: custom:mushroom-template-card
primary: Tarif électricité
secondary: |
  {% if is_state('binary_sensor.heures_creuses','on') %}
  Heures creuses
  {% else %}
  Heures pleines
  Prochaines heures creuses dans {{ states('sensor.prochaine_heure_creuse') }}
  {% endif %}
icon: |
  {% if is_state('binary_sensor.heures_creuses','on') %}
    mdi:leaf
  {% else %}
    mdi:flash
  {% endif %}
multiline_secondary: true
entity: binary_sensor.heures_creuses
color: |
  {% if is_state('binary_sensor.heures_creuses','on') %}
    #00b050
  {% else %}
    #4472c4
  {% endif %}
features_position: bottom
grid_options:
  columns: 12
  rows: auto

  - template:
  - binary_sensor:
      - name: Heures creuses
        unique_id: heures_creuses
        state: >
          {% set m = now().hour * 60 + now().minute %}
          {{ (60 <= m < 420) or (720 <= m < 840) }}
          {# 01:00-07:00 (60-420) ou 12:00-14:00 (720-840) #}

  - sensor:
      - name: Prochaine heure creuse
        unique_id: prochaine_heure_creuse
        state: >
          {% set now_ts = as_timestamp(now()) %}
          {% set t1 = as_timestamp(today_at('01:00')) %}
          {% set t2 = as_timestamp(today_at('12:00')) %}

          {# Prochain début de plage HC (aujourd'hui), sinon demain 01:00 #}
          {% set candidates = [t1, t2] %}
          {% set future = candidates | select('gt', now_ts) | list %}

          {% if is_state('binary_sensor.heures_creuses', 'on') %}
            Maintenant
          {% else %}
            {% if future | length > 0 %}
              {% set next_ts = future | min %}
            {% else %}
              {% set next_ts = t1 + 86400 %}
            {% endif %}

            {% set diff = (next_ts - now_ts) | int %}
            {{ '%02dh%02d' % ((diff // 3600), ((diff % 3600) // 60)) }}
          {% endif %}

Je te propose un truc dans la journée

1 « J'aime »

Voici ce que ça donne en custom:button-card :

Je vais juste ajouter de quoi configurer la carte (définition des plages d’heures creuses avec choix entre 1 ou deux plages et définition d’une plage d’heures super creuses avec choix de l’afficher ou pas).
Je posterai le code dans la foulée.

3 « J'aime »

Bonsoir,

Je suis intéressé par ton code pour la custom:button-card…

Tu te bases sur le sensor “tarif_actuel_ttc“ ?

Il me reste quelques lignes de code à finaliser et je posterai celui-ci (aujourd’ui ou demain normalement)

1 « J'aime »

essai

Bonjour,

Code finalisé (en espérant qu’il ne reste pas quelques bugs)… Pour ceux qui vont lire ce post, ne prenez pas peur, le code fait pas loin de 1600 lignes (sans parler du script, des input_text et du capteur qui vont avec).

Pour le principe de fonctionnement, je suis parti des divers éléments récupérés sur le site d’Enedis notamment, afin de fixer les « règles » de fonctionnement suivantes :

  • la durée des « heures creuses » est de 8 heures (sur 24 heures glissantes entre 07:00 et 07:00 du jour suivant) ;
  • les heures creuses sont constituées d’une plage principale située entre 23h00 et 07h00 du matin le jour suivant ;
  • la durée minimale de la plage principale est de 5 heures et en cas de plage secondaire, elle ne peut excéder 6 heures (il ne peut pas y avoir une plage principale d’une durée de 7 heures avec une plage secondaire d’une durée d’une heure) ;
  • une plage secondaire est possible (si la durée de la plage principale est inférieure à 8 heures). Dans ce cas, la plage secondaire est comprise entre 11h00 et 17h00 ;
  • la durée de la plage secondaire est fonction de la durée de la plage principale, elle peut donc être de 2 heures, 2 heures et 30 minutes ou 3 heures ;
  • il est possible d’avoir une plage « Heures Super Creuses » (offre Charge’Heures Électricité de Total Energies) située entre 02h00 et 06h00 du matin (uniquement possible quand il y a une plage principale d’une durée de 8 heures).

A partir de ces règles, j’ai donc concu la carte suivante permettant da’fficher en temps réel la ou les plages d’heures creuses (et d’heures super creuses le cas échéant) en fonction de ce qui vous aura été imposé par Enedis (la ou les plages sont indiquées sur vos factures et votre contrat d’électricité). La carte permet donc de saisir votre ou vous plages et de les mémoriser dans un input_text (nommé « input_text.widget_tarif »). Un deuxième input_text nommé « input_text.widget_tarif_backup » permet d’annuler une configuration en cours de saisie si nécessaire. La configuration initiale de la carte se fait en cliquant sur l’engrenage en haut à droite de celle-ci.



Vous pourrez donc définir le début et la durée de la plage principale, le début de la plage secondaire si la durée de la plage principale est de moins de 8 heures et mettre une plage Heures super Creuses si toutes les conditions sont réunies.

Le pilotage de la carte est assuré par un script (affichage de la configuration, annulation et validation).

L’input_text.widget_tarif a cette forme : « 0|01:00|6.0|1|13:00|2.0|0|02:00|4.0 ». Le code récupère chaque valeur par un split(« | ») afin d’assurer le fonctionnement dynamique de la carte : déplacement du curseur, couleur du disque devant le commentaire, texte du commentaire et répresentation graphique de la (ou des plages) sur la barre.

Voici le script (à coller dans script.yaml) :

widget_tarif_configurator:
  alias: "Widget Tarification – Configuration"
  mode: single
  fields:
    action:
      description: "open | cancel | validate"
      example: "open"
  sequence:
    - choose:
        - conditions: "{{ action == 'open' }}"
          sequence:
            - service: input_text.set_value
              data:
                entity_id: input_text.widget_tarif_backup
                value: "{{ states('input_text.widget_tarif') }}"
            - service: input_text.set_value
              data:
                entity_id: input_text.widget_tarif
                value: >
                  {% set p = states('input_text.widget_tarif').split('|') %}
                  {% set new0 = '0' if p[0] == '1' else '1' %}
                  {{ [new0, p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8]] | join('|') }}

        - conditions: "{{ action == 'cancel' }}"
          sequence:
            - service: input_text.set_value
              data:
                entity_id: input_text.widget_tarif
                value: >
                  {% set p = states('input_text.widget_tarif_backup').split('|') %}
                  {{ ['0', p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8]] | join('|') }}

        - conditions: "{{ action == 'validate' }}"
          sequence:
            - service: input_text.set_value
              data:
                entity_id: input_text.widget_tarif
                value: >
                  {% set p = states('input_text.widget_tarif').split('|') %}
                  {% set duree_principale = p[2] | float %}
                  {% set duree_secondaire = (8.0 - duree_principale) %}
                  {% set has_secondaire = 0 if duree_principale == 8.0 else 1 %}
                  {{ [
                    '0',
                    p[1],
                    '%.1f' % duree_principale,
                    has_secondaire,
                    p[4],
                    '%.1f' % duree_secondaire,
                    p[6],
                    p[7],
                    p[8]
                  ] | join('|') }}

Par ailleurs, afin de simplifier l’usage, je joins un capteur binaire qui passe sur « on » dès qu’une plage d’heures creuses (et/ou heures super creuses) débute et repasse sur « off » quand elle se finit. Il peut donc être utilisé directement dans toute automatisation.

Le capteur (à coller dans configuration.yaml) :

  # --- BINARY SENSOR : HEURES CREUSES (HC + HSC) ---
  - binary_sensor:
      - name: Heures creuses actives
        unique_id: heures_creuses_actives
        device_class: running
        state: >
          {% set p = states('input_text.widget_tarif').split('|') %}
          {% if p | length < 9 %}
            false
          {% else %}
            {% set now_min = now().hour * 60 + now().minute %}

            {% macro m(t) %}{% set h,m = t.split(':') %}{{ h|int*60 + m|int }}{% endmacro %}

            {% set zones = [] %}
            {% set zones = zones + [(m(p[1]), m(p[1]) + (p[2]|float*60)|int)] %}
            {% if p[3] == '1' %}
              {% set zones = zones + [(m(p[4]), m(p[4]) + (p[5]|float*60)|int)] %}
            {% endif %}
            {% if p[6] == '1' %}
              {% set zones = zones + [(m(p[7]), m(p[7]) + (p[8]|float*60)|int)] %}
            {% endif %}

            {{ zones | selectattr(0,'<=',now_min)
                     | selectattr(1,'>',now_min)
                     | list | count > 0
               or zones | selectattr(1,'>',1440)
                        | selectattr(0,'<=',now_min + 1440)
                        | list | count > 0 }}
          {% endif %}

Le code de la carte :

type: custom:button-card
name: widget tarification
show_icon: false
show_state: false
show_label: true
entity: sensor.time
tap_action:
  action: none
hold_action:
  action: none
double_tap_action:
  action: none
custom_fields:
  config:
    card:
      type: custom:button-card
      entity: input_boolean.widget_tarif_conf
      show_name: false
      icon: mdi:cog
      styles:
        card:
          - background: transparent
          - border: none
          - box-shadow: none
          - width: 28px
          - height: 28px
          - border-radius: 50%
          - transform: translateX(-50%) scaleY(-1)
          - pointer-events: auto
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "0" ? "block" : "none";
              ]]]
        icon:
          - width: 100%
          - color: var(--card-background-color)
      tap_action:
        action: call-service
        service: script.widget_tarif_configurator
        service_data:
          action: open
  bar:
    card:
      type: custom:button-card
      styles:
        card:
          - height: 18px
          - width: 432px
          - border-radius: 999px
          - border: none
          - padding: 0
          - background: |
              [[[
                const state = states["input_text.widget_tarif"].state;
                const p = state.split("|");
                if (p[0] !== "0") return "none";
                const PX_H = 18;
                const START = 7;
                const TOTAL = 24 * PX_H;
                const C_HC  = "rgba(240,160,40,1.0)";
                const C_HP  = "rgba(80,170,80,1.0)";
                const C_HSC = "rgba(110,180,260,1.0)";
                const t2px = (t) => {
                  const [h, m] = t.split(":").map(Number);
                  return ((h + m / 60 - START + 24) % 24) * PX_H;
                };
                const d2px = (d) => Number(d) * PX_H;
                let hc = [];
                let hsc = [];
                let s = t2px(p[1]);
                let e = s + d2px(p[2]);
                hc.push([s, e]);
                if (p[3] === "1") {
                  s = t2px(p[4]);
                  e = s + d2px(p[5]);
                  hc.push([s, e]);
                }
                if (p[6] === "1") {
                  s = t2px(p[7]);
                  e = s + d2px(p[8]);
                  hsc.push([s, e]);
                }
                const normalize = (arr) => {
                  let out = [];
                  arr.forEach(([s, e]) => {
                    if (e <= TOTAL) {
                      out.push([s, e]);
                    } else {
                      out.push([s, TOTAL]);
                      out.push([0, e - TOTAL]);
                    }
                  });
                  return out;
                };
                hc = normalize(hc);
                hsc = normalize(hsc);
                let hc_final = [];
                hc.forEach(([hs, he]) => {
                  let parts = [[hs, he]];
                  hsc.forEach(([ss, se]) => {
                    parts = parts.flatMap(([ps, pe]) => {
                      if (se <= ps || ss >= pe) return [[ps, pe]];
                      let res = [];
                      if (ss > ps) res.push([ps, ss]);
                      if (se < pe) res.push([se, pe]);
                      return res;
                    });
                  });
                  hc_final.push(...parts);
                });
                let stops = [];
                let cursor = 0;
                const addSeg = (s, e, c) => {
                  if (s > cursor) {
                    stops.push(`${C_HC} ${cursor}px`, `${C_HC} ${s}px`);
                  }
                  stops.push(`${c} ${s}px`, `${c} ${e}px`);
                  cursor = e;
                };
                [...hc_final.map(v => [...v, C_HP]), ...hsc.map(v => [...v, C_HSC])]
                  .sort((a, b) => a[0] - b[0])
                  .forEach(([s, e, c]) => addSeg(s, e, c));
                if (cursor < TOTAL) {
                  stops.push(`${C_HC} ${cursor}px`, `${C_HC} ${TOTAL}px`);
                }
                return `linear-gradient(to right, ${stops.join(",")})`;
              ]]]
          - display: |
              [[[
                return states["input_text.widget_tarif"].state.split("|")[0] === "0"
                  ? "block"
                  : "none";
              ]]]
  cursor:
    card:
      type: custom:button-card
      icon: mdi:water
      custom_fields:
        circle:
          card:
            type: custom:button-card
            styles:
              card:
                - width: 8px
                - height: 8px
                - border-radius: 50%
                - background: var(--card-background-color)
      styles:
        card:
          - background: transparent
          - border: none
          - box-shadow: none
          - width: 32px
          - height: 32px
          - border-radius: 50%
          - transform: translateX(-50%) scaleY(-1)
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "0" ? "block" : "none";
              ]]]
        icon:
          - width: 100%
          - color: rgba(90,90,90,1.0)
        custom_fields:
          circle:
            - position: absolute
            - bottom: 10px
            - left: 12px
  hour_1:
    card:
      type: custom:button-card
      name: 7h
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "0" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  hour_2:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const [h, m] = p[4].split(":").map(Number);
          return m === 0 ? `${h}h` : `${h}h${String(m).padStart(2,"0")}`;
        ]]]
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "0" && p[3] === "1") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  hour_3:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const [h, m] = p[4].split(":").map(Number);
          const start = h * 60 + m;
          const duree = parseFloat(p[5]);
          const end = (start + duree * 60) % (24 * 60);
          const hh = Math.floor(end / 60);
          const mm = end % 60;
          return mm === 0 ? `${hh}h` : `${hh}h${String(mm).padStart(2,"0")}`;
        ]]]
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "0" && p[3] === "1") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  hour_4:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const [h, m] = p[1].split(":").map(Number);
          return m === 0 ? `${h}h` : `${h}h${String(m).padStart(2,"0")}`;
        ]]]
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "0" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  hour_5:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const [h, m] = p[7].split(":");
          const hour = parseInt(h);
          return m === "00" ? `${hour}h` : `${hour}h${m}`;
        ]]]
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[6] === "1" && p[0] === "0") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  hour_6:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const [h, m] = p[7].split(":").map(Number);
          const start = h * 60 + m;
          const duree = parseFloat(p[8]);
          const end = (start + duree * 60) % (24 * 60);
          const hh = Math.floor(end / 60);
          const mm = end % 60;
          const mmStr = mm === 0 ? "" : String(mm).padStart(2, "0");
          return mm === 0 ? `${hh}h` : `${hh}h${mmStr}`;
        ]]]
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "0" && p[6] === "1") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  hour_7:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const [h,m] = p[1].split(":").map(Number);
          const start = h*60 + m;
          const duree = parseFloat(p[2]);
          const end = (start + duree*60) % 1440;
          const hh = Math.floor(end / 60);
          const mm = end % 60;
          return mm === 0 ? `${hh}h` : `${hh}h${String(mm).padStart(2,"0")}`;
        ]]]
      styles:
        card:
          - height: 24px
          - width: 24px
          - border-radius: 0px
          - border: none
          - background: transparent
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "0" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.7rem
  icon:
    card:
      type: custom:button-card
      icon: mdi:circle
      styles:
        card:
          - height: 24px
          - width: 24px
          - background: transparent
          - border: none
          - display: |
              [[[
                return states["input_text.widget_tarif"].state.split("|")[0] === "0"
                  ? "block"
                  : "none";
              ]]]
        icon:
          - width: 100%
          - color: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                const now = new Date();
                const cur = now.getHours() * 60 + now.getMinutes();
                const toMin = t => {
                  const [h,m] = t.split(":").map(Number);
                  return h*60 + m;
                };
                let hc = [];
                let hsc = [];
                hc.push([toMin(p[1]), toMin(p[1]) + p[2]*60]);
                if (p[3] === "1")
                  hc.push([toMin(p[4]), toMin(p[4]) + p[5]*60]);
                if (p[6] === "1")
                  hsc.push([toMin(p[7]), toMin(p[7]) + p[8]*60]);
                const inRange = (arr) =>
                  arr.some(([s,e]) => (cur>=s && cur<e) || (e>1440 && cur<(e-1440)));
                if (inRange(hsc)) return "rgba(110,180,260,1.0)";
                if (inRange(hc))  return "rgba(80,170,80,1.0)";
                return "rgba(240,160,40,1.0)";
              ]]]
  comment:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const d = new Date();
          const cur = d.getHours()*60 + d.getMinutes();
          const days = ["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"];
          const months = ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"];
          const toMin = t => {
            const [h,m] = t.split(":").map(Number);
            return h*60 + m;
          };
          let zones = [];
          zones.push({ s: toMin(p[1]), e: toMin(p[1]) + p[2]*60, type: "HC" });
          if (p[3] === "1")
            zones.push({ s: toMin(p[4]), e: toMin(p[4]) + p[5]*60, type: "HC" });
          if (p[6] === "1")
            zones.push({ s: toMin(p[7]), e: toMin(p[7]) + p[8]*60, type: "HSC" });
          const inZone = zones.find(z =>
            (cur>=z.s && cur<z.e) || (z.e>1440 && cur<(z.e-1440))
          );
          const pad = n => ("0"+n).slice(-2);
          const dateTxt = `${days[d.getDay()]} ${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()} à ${pad(d.getHours())}h${pad(d.getMinutes())}`;
          if (inZone) {
            let end = inZone.e > 1440 && cur < (inZone.e-1440)
              ? inZone.e - 1440
              : inZone.e;
            const delta = end - cur;
            const rem = delta < 60 ? `${delta} min` : `${Math.floor(delta/60)}h${pad(delta%60)}`;
            return `${dateTxt}. Durée restante : ${rem}`;
          }
          const next = zones
            .map(z => (z.s <= cur ? z.s + 1440 : z.s))
            .sort((a,b)=>a-b)[0];
          const delta = next - cur;
          const rem = delta < 60 ? `${delta} min` : `${Math.floor(delta/60)}h${pad(delta%60)}`;
          return `${dateTxt}. Dans ${rem}, je serai en`;
        ]]]
      styles:
        card:
          - background: transparent
          - border-radius: 0
          - border: none
          - width: auto
          - height: 24px
          - display: |
              [[[
                return states["input_text.widget_tarif"].state.split("|")[0] === "0"
                  ? "block"
                  : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.8rem
  label:
    card:
      type: custom:button-card
      name: |
        [[[
          const p = states["input_text.widget_tarif"].state.split("|");
          const now = new Date();
          const cur = now.getHours()*60 + now.getMinutes();
          const toMin = t => {
            const [h,m] = t.split(":").map(Number);
            return h*60 + m;
          };
          let hc = [];
          let hsc = [];
          hc.push([toMin(p[1]), toMin(p[1]) + p[2]*60]);
          if (p[3] === "1")
            hc.push([toMin(p[4]), toMin(p[4]) + p[5]*60]);
          if (p[6] === "1")
            hsc.push([toMin(p[7]), toMin(p[7]) + p[8]*60]);
          const inRange = (arr) =>
            arr.some(([s,e]) => (cur>=s && cur<e) || (e>1440 && cur<(e-1440)));
          if (inRange(hsc)) return "Heures super creuses";
          if (inRange(hc))  return "Heures creuses";
          return "Heures creuses";
        ]]]
      styles:
        card:
          - height: 22px
          - border-radius: 999px
          - border: none
          - background: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                const now = new Date();
                const cur = now.getHours()*60 + now.getMinutes();
                const toMin = t => {
                  const [h,m] = t.split(":").map(Number);
                  return h*60 + m;
                };
                let hc = [];
                let hsc = [];
                hc.push([toMin(p[1]), toMin(p[1]) + p[2]*60]);
                if (p[3] === "1")
                  hc.push([toMin(p[4]), toMin(p[4]) + p[5]*60]);
                if (p[6] === "1")
                  hsc.push([toMin(p[7]), toMin(p[7]) + p[8]*60]);
                const inRange = (arr) =>
                  arr.some(([s,e]) => (cur>=s && cur<e) || (e>1440 && cur<(e-1440)));
                if (inRange(hsc)) return "rgba(110,180,260,1.0)";
                if (inRange(hc))  return "rgba(80,170,80,1.0)";
                return "rgba(80,170,80,1.0)";
              ]]]
          - display: |
              [[[
                return states["input_text.widget_tarif"].state.split("|")[0] === "0"
                  ? "block"
                  : "none";
              ]]]
        name:
          - color: var(--card-background-color)
          - padding: 0 10px
          - font-size: 0.8rem
  plage_principale:
    card:
      type: custom:button-card
      name: "Plage principale :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
          - font-weight: 500
  debut_principale:
    card:
      type: custom:button-card
      name: "- Début :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
  config_debut_principale:
    card:
      type: custom:button-card
      custom_fields:
        btn_less:
          card:
            type: custom:button-card
            icon: mdi:menu-left
            styles:
              card:
                - width: 38px
                - height: 38px
                - border-radius: 0
                - border: none
                - background: transparent
                - padding: 0
                - pointer-events: |
                    [[[
                      const p = states["input_text.widget_tarif_backup"].state.split("|");
                      return p[1] === "23:00" ? "none" : "auto";
                    ]]]
                - "--button-card-ripple-color": transparent
                - "--button-card-ripple-pressed-color": transparent
              icon:
                - width: 100%
                - color: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      return p[1] === "23:00"
                        ? "rgba(90,90,90,0.3)"
                        : "rgba(90,90,90,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p = states["input_text.widget_tarif"].state.split("|");
                    const d = parseFloat(p[2]);
                    let [h,m] = p[1].split(":").map(Number);
                    let debut = (h*60 + m - 30 + 1440) % 1440;
                    if (!(debut >= 23*60 || debut < 7*60)) return p.join("|");
                    const fin = (debut + d*60) % 1440;
                    if (fin > 7*60) return p.join("|");
                    const hh = String(Math.floor(debut/60)).padStart(2,"0");
                    const mm = String(debut%60).padStart(2,"0");
                    p[1] = `${hh}:${mm}`;
                    return p.join("|");
                  ]]]
        value:
          card:
            type: custom:button-card
            name: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[1];
              ]]]
            styles:
              card:
                - width: 50px
                - height: 20px
                - border-radius: 0
                - border: 1px solid rgba(90,90,90,0.5)
                - background: transparent
                - padding: 0
              name:
                - color: rgba(90,90,90,1.0)
                - font-size: 1.0rem
                - font-weight: bold
        btn_more:
          card:
            type: custom:button-card
            icon: mdi:menu-right
            styles:
              card:
                - width: 38px
                - height: 38px
                - border-radius: 0
                - border: none
                - background: transparent
                - padding: 0
                - pointer-events: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const duree = parseFloat(p[2]);
                      if (duree >= 8.0) return "none";
                      const [h,m] = p[1].split(":").map(Number);
                      const debut = h*60 + m;
                      const fin = (debut + duree*60) % (24*60);
                      return fin > 7*60 ? "none" : "auto";
                    ]]]
                - "--button-card-ripple-color": transparent
                - "--button-card-ripple-pressed-color": transparent
              icon:
                - width: 100%
                - color: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const duree = parseFloat(p[2]);
                      if (duree >= 8.0)
                        return "rgba(90,90,90,0.3)";
                      const [h,m] = p[1].split(":").map(Number);
                      const debut = h*60 + m;
                      const fin = (debut + duree*60) % (24*60);
                      return fin >= 7*60
                        ? "rgba(90,90,90,0.3)"
                        : "rgba(90,90,90,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p = states["input_text.widget_tarif"].state.split("|");
                    const d = parseFloat(p[2]);
                    let [h,m] = p[1].split(":").map(Number);
                    let debut = (h*60 + m + 30) % 1440;
                    if (!(debut >= 23*60 || debut < 7*60)) return p.join("|");
                    const fin = (debut + d*60) % 1440;
                    if (fin > 7*60) return p.join("|");  // > et non >=
                    const hh = String(Math.floor(debut/60)).padStart(2,"0");
                    const mm = String(debut%60).padStart(2,"0");
                    p[1] = `${hh}:${mm}`;
                    return p.join("|");
                  ]]]
      styles:
        card:
          - width: 80px
          - height: 20px
          - background-color: transparent
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
        custom_fields:
          btn_less:
            - position: absolute
            - left: "-10px"
          value:
            - position: absolute
            - left: 15px
          btn_more:
            - position: absolute
            - right: "-10px"
  duree_principale:
    card:
      type: custom:button-card
      name: "- Durée :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
  config_duree_principale:
    card:
      type: custom:button-card
      custom_fields:
        btn_less:
          card:
            type: custom:button-card
            icon: mdi:menu-left
            styles:
              card:
                - width: 38px
                - height: 38px
                - border-radius: 0
                - border: none
                - background: transparent
                - padding: 0
                - pointer-events: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const duree = parseFloat(p[2]);
                      return duree <= 5.0 ? "none" : "auto";
                    ]]]
                - "--button-card-ripple-color": transparent
                - "--button-card-ripple-pressed-color": transparent
              icon:
                - width: 100%
                - color: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const duree = parseFloat(p[2]);
                      return duree <= 5.0
                        ? "rgba(90,90,90,0.3)"
                        : "rgba(90,90,90,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p=states["input_text.widget_tarif"].state.split("|");
                    let d1=parseFloat(p[2]);
                    if(d1===8.0)d1=6.0;
                    else if(d1===6.0)d1=5.5;
                    else if(d1===5.5)d1=5.0;
                    p[2]=d1.toFixed(1);
                    if(d1<8.0)p[6]="0";
                    if(d1===8.0){
                      p[3]="0";
                      p[5]="0.0";
                    }else{
                      p[3]="1";
                      const d2=8-d1;
                      p[5]=d2.toFixed(1);
                      let[h,m]=p[4].split(":").map(Number);
                      let debut=h*60+m;
                      if(debut<660)debut=660;
                      if(debut+d2*60>1020)debut=1020-d2*60;
                      const hh=String(Math.floor(debut/60)).padStart(2,"0");
                      const mm=String(debut%60).padStart(2,"0");
                      p[4]=`${hh}:${mm}`;
                    }
                    return p.join("|");
                  ]]]
        value:
          card:
            type: custom:button-card
            name: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[2];
              ]]]
            styles:
              card:
                - width: 50px
                - height: 20px
                - border-radius: 0
                - border: 1px solid rgba(90,90,90,0.5)
                - background: transparent
                - padding: 0
              name:
                - color: rgba(90,90,90,1.0)
                - font-size: 1.0rem
                - font-weight: bold
        btn_more:
          card:
            type: custom:button-card
            icon: mdi:menu-right
            styles:
              card:
                - width: 38px
                - height: 38px
                - border-radius: 0
                - border: none
                - background: transparent
                - padding: 0
                - pointer-events: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const duree = parseFloat(p[2]);
                      return duree >= 8.0 ? "none" : "auto";
                    ]]]
                - "--button-card-ripple-color": transparent
                - "--button-card-ripple-pressed-color": transparent
              icon:
                - width: 100%
                - color: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const duree = parseFloat(p[2]);
                      return duree >= 8.0
                        ? "rgba(90,90,90,0.3)"
                        : "rgba(90,90,90,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p=states["input_text.widget_tarif"].state.split("|");
                    let d1=parseFloat(p[2]);
                    if(d1===5.0)d1=5.5;else if(d1===5.5)d1=6.0;else if(d1===6.0)d1=8.0;
                    p[2]=d1.toFixed(1);
                    if(d1>6.0){
                      const dureeMin=d1*60;
                      let debut=420-dureeMin;
                      if(debut<0)debut+=1440;
                      if(debut<1380)debut+=1440;
                      const hh=String(Math.floor(debut/60)).padStart(2,"0");
                      const mm=String(debut%60).padStart(2,"0");
                      p[1]=`${hh}:${mm}`;
                    }
                    if(d1===8.0){
                      p[3]="0";
                      p[5]="0.0";
                      p[4]="11:00";
                    }else{
                      p[3]="1";
                      const d2=8-d1;
                      p[5]=d2.toFixed(1);
                      let[h,m]=p[4].split(":").map(Number);
                      let debut=h*60+m;
                      if(debut<660)debut=660;
                      if(debut+d2*60>1020)debut=1020-d2*60;
                      const hh=String(Math.floor(debut/60)).padStart(2,"0");
                      const mm=String(debut%60).padStart(2,"0");
                      p[4]=`${hh}:${mm}`;
                    }
                    return p.join("|");
                  ]]]
      styles:
        card:
          - width: 80px
          - height: 20px
          - background-color: transparent
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
        custom_fields:
          btn_less:
            - position: absolute
            - left: "-10px"
          value:
            - position: absolute
            - left: 15px
          btn_more:
            - position: absolute
            - right: "-10px"
  plage_secondaire:
    card:
      type: custom:button-card
      name: "Plage secondaire :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
          - font-weight: 500
  debut_secondaire:
    card:
      type: custom:button-card
      name: "- Début :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - padding-left: 10px
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "1" && p[3] === "1") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
  config_debut_secondaire:
    card:
      type: custom:button-card
      custom_fields:
        btn_less:
          card:
            type: custom:button-card
            icon: mdi:menu-left
            styles:
              card:
                - width: 38px
                - height: 38px
                - border-radius: 0
                - border: none
                - background: transparent
                - padding: 0
                - pointer-events: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const [h,m] = p[4].split(":").map(Number);
                      const debut = h*60 + m;
                      return debut <= 11*60 ? "none" : "auto";
                    ]]]
                - "--button-card-ripple-color": transparent
                - "--button-card-ripple-pressed-color": transparent
              icon:
                - width: 100%
                - color: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      return p[4] === "11:00"
                        ? "rgba(90,90,90,0.3)"
                        : "rgba(90,90,90,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p = states["input_text.widget_tarif"].state.split("|");
                    let [h,m] = p[4].split(":").map(Number);
                    let debut = h*60 + m;
                    if (debut <= 11*60) return p.join("|");
                    debut = (debut - 30 + 24*60) % (24*60);
                    if (debut < 11*60) debut = 11*60;
                    const hh = String(Math.floor(debut/60)).padStart(2,"0");
                    const mm = String(debut%60).padStart(2,"0");
                    p[4] = `${hh}:${mm}`;
                    return p.join("|");
                  ]]]
        value:
          card:
            type: custom:button-card
            name: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[4];
              ]]]
            styles:
              card:
                - width: 50px
                - height: 20px
                - border-radius: 0
                - border: 1px solid rgba(90,90,90,0.5)
                - background: transparent
                - padding: 0
              name:
                - color: rgba(90,90,90,1.0)
                - font-size: 1.0rem
                - font-weight: bold
        btn_more:
          card:
            type: custom:button-card
            icon: mdi:menu-right
            styles:
              card:
                - width: 38px
                - height: 38px
                - border-radius: 0
                - border: none
                - background: transparent
                - padding: 0
                - pointer-events: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const dureePrincipale = parseFloat(p[2]);
                      const dureeSecondaire = 8.0 - dureePrincipale;
                      const [h,m] = p[4].split(":").map(Number);
                      const debut = h*60 + m;
                      const debutMax = (17*60) - (dureeSecondaire*60);
                      return debut >= debutMax ? "none" : "auto";
                    ]]]
                - "--button-card-ripple-color": transparent
                - "--button-card-ripple-pressed-color": transparent
              icon:
                - width: 100%
                - color: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      const dureePrincipale = parseFloat(p[2]);
                      const dureeSecondaire = 8.0 - dureePrincipale;
                      const [h,m] = p[4].split(":").map(Number);
                      const debut = h*60 + m;
                      const debutMax = (17*60) - (dureeSecondaire*60);
                      return debut >= debutMax
                        ? "rgba(90,90,90,0.3)"
                        : "rgba(90,90,90,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p = states["input_text.widget_tarif"].state.split("|");
                    const dureePrincipale = parseFloat(p[2]);
                    const dureeSecondaire = 8.0 - dureePrincipale;
                    let [h,m] = p[4].split(":").map(Number);
                    let debut = h*60 + m;
                    const debutMax = (17*60) - (dureeSecondaire*60);
                    if (debut >= debutMax) return p.join("|");
                    debut = (debut + 30) % (24*60);
                    if (debut > debutMax) debut = debutMax;
                    const hh = String(Math.floor(debut/60)).padStart(2,"0");
                    const mm = String(debut%60).padStart(2,"0");
                    p[4] = `${hh}:${mm}`;
                    return p.join("|");
                  ]]]
      styles:
        card:
          - width: 80px
          - height: 20px
          - background-color: transparent
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "1" && p[3] === "1") ? "block" : "none";
              ]]]
        custom_fields:
          btn_less:
            - position: absolute
            - left: "-10px"
          value:
            - position: absolute
            - left: 15px
          btn_more:
            - position: absolute
            - right: "-10px"
  duree_secondaire:
    card:
      type: custom:button-card
      name: "- Durée :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - padding-left: 10px
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "1" && p[3] === "1") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
  config_duree_secondaire:
    card:
      type: custom:button-card
      custom_fields:
        value:
          card:
            type: custom:button-card
            name: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                const dureePrincipale = parseFloat(p[2]);
                const dureeSecondaire = (8.0 - dureePrincipale).toFixed(1);
                return dureeSecondaire;
              ]]]
            styles:
              card:
                - width: 50px
                - height: 20px
                - border-radius: 0
                - border: 1px solid rgba(90,90,90,0.5)
                - background: transparent
                - padding: 0
              name:
                - color: rgba(90,90,90,1.0)
                - font-size: 1.0rem
                - font-weight: bold
      styles:
        card:
          - width: 80px
          - height: 20px
          - background-color: transparent
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[0] === "1" && p[3] === "1") ? "block" : "none";
              ]]]
        custom_fields:
          value:
            - position: absolute
            - left: 15px
  plage_super_creuses:
    card:
      type: custom:button-card
      name: "Heures Super Creuses :"
      styles:
        card:
          - width: auto
          - height: auto
          - background-color: var(--card-background-color)
          - border: none
          - border-radius: 0
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[2] === "8.0" && p[1] === "23:00" && p[0] === "1") ? "block" : "none";
              ]]]
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
          - font-weight: 500
  btn_super_creuses:
    card:
      type: custom:button-card
      custom_fields:
        slider:
          card:
            type: custom:button-card
            styles:
              card:
                - width: 34px
                - height: 16px
                - border: none
                - background: rgba(90,90,90,0.3)
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p = states["input_text.widget_tarif"].state.split("|");
                    if (!(p[2] === "8.0" && p[1] === "23:00")) {
                      p[6] = "0";
                      return p.join("|");
                    }
                    if (p[6] === "1") {
                      p[6] = "0";
                    } else {
                      p[6] = "1";
                      p[7] = "02:00";
                      p[8] = "4.0";
                    }
                    return p.join("|");
                  ]]]
        button:
          card:
            type: custom:button-card
            styles:
              card:
                - width: 20px
                - height: 20px
                - border: none
                - box-shadow: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      return p[6] === "1"
                        ? "0 0 4px 4px rgba(110,180,260,0.9)"
                        : "none";
                    ]]]
                - background: |
                    [[[
                      const p = states["input_text.widget_tarif"].state.split("|");
                      return p[6] === "1" ? "rgba(0,162,232,1.0)" : "rgba(141,141,141,1.0)";
                    ]]]
            tap_action:
              action: call-service
              service: input_text.set_value
              service_data:
                entity_id: input_text.widget_tarif
                value: |
                  [[[
                    const p = states["input_text.widget_tarif"].state.split("|");
                    if (!(p[2] === "8.0" && p[1] === "23:00")) {
                      p[6] = "0";
                      return p.join("|");
                    }
                    if (p[6] === "1") {
                      p[6] = "0";
                    } else {
                      p[6] = "1";
                      p[7] = "02:00";
                      p[8] = "4.0";
                    }
                    return p.join("|");
                  ]]]
      styles:
        card:
          - width: 50px
          - height: 30px
          - background-color: rgba(90,90,90,0.05)
          - border: none
          - border-radius: 999px
          - pointer-events: auto
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return (p[2] === "8.0" && p[1] === "23:00" && p[0] === "1") ? "block" : "none";
              ]]]
        custom_fields:
          slider:
            - position: absolute
            - top: 7px
            - left: 7px
          button:
            - position: absolute
            - top: 5px
            - left: |
                [[[
                  const p = states["input_text.widget_tarif"].state.split("|");
                  return p[6] === "1" ? "25px" : "5px";
                ]]]
      tap_action:
        action: call-service
        service: input_text.set_value
        service_data:
          entity_id: input_text.widget_tarif
          value: |
            [[[
              const s = states["input_text.widget_tarif"].state;
              const p = s.split("|");
              p[6] = p[6] === "0" ? "1" : "0";
              return `${p[0]}|${p[1]}|${p[2]}|${p[3]}|${p[4]}|${p[5]}|${p[6]}|${p[7]}|${p[8]}`;
            ]]]
  btn_annuler:
    card:
      type: custom:button-card
      name: Annuler
      styles:
        card:
          - width: 100px
          - height: 26px
          - background-color: var(--card-background-color)
          - border: 2px rgba(32,32,32,0.5) outset
          - border-radius: 8px
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
          - pointer-events: auto
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
          - font-weight: 500
      tap_action:
        action: call-service
        service: script.widget_tarif_configurator
        service_data:
          action: cancel
  btn_valider:
    card:
      type: custom:button-card
      name: Valider
      styles:
        card:
          - width: 100px
          - height: 26px
          - background-color: var(--card-background-color)
          - border: 2px rgba(32,32,32,0.5) outset
          - border-radius: 8px
          - display: |
              [[[
                const p = states["input_text.widget_tarif"].state.split("|");
                return p[0] === "1" ? "block" : "none";
              ]]]
          - pointer-events: auto
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 1.0rem
          - font-weight: 500
      tap_action:
        action: call-service
        service: script.widget_tarif_configurator
        service_data:
          action: validate
styles:
  card:
    - aspect-ratio: 3/1
    - position: relative
    - border: none
    - background: |-
        linear-gradient(to bottom,
          rgba(114,157,133,1.0) 0px,
          rgba(114,157,133,1.0) 28px,
          var(--card-background-color) 28px)
    - pointer-events: none
  name:
    - align-self: start
    - margin-top: "-14px"
    - color: var(--card-background-color)
  custom_fields:
    config:
      - position: absolute
      - right: "-12px"
      - top: 0px
    bar:
      - position: absolute
      - left: 24px
      - top: 64px
    cursor:
      - position: absolute
      - top: 32px
      - left: |
          [[[
            const d = new Date();
            const now = d.getHours() * 60 + d.getMinutes();
            let minutes = now - 420;
            if (minutes < 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${24 + px}px`;
          ]]]
    hour_1:
      - position: absolute
      - top: 80px
      - left: 12px
    hour_2:
      - position: absolute
      - top: 80px
      - left: |
          [[[
            const p = states["input_text.widget_tarif"].state.split("|");
            const [h, m] = p[4].split(":").map(Number);
            const start = h * 60 + m;
            let minutes = start - 420;
            if (minutes < 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${12 + px}px`;
          ]]]
    hour_3:
      - position: absolute
      - top: 80px
      - left: |
          [[[
            const p = states["input_text.widget_tarif"].state.split("|");
            const [h, m] = p[4].split(":").map(Number);
            const start = h * 60 + m;
            const duree = parseFloat(p[5]);
            const end = (start + duree * 60) % (24 * 60);
            let minutes = end - 420;
            if (minutes < 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${12 + px}px`;
          ]]]
    hour_4:
      - position: absolute
      - top: 80px
      - left: |
          [[[
            const p = states["input_text.widget_tarif"].state.split("|");
            const [h, m] = p[1].split(":").map(Number);
            const start = h * 60 + m;
            let minutes = start - 420;
            if (minutes < 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${12 + px}px`;
          ]]]
    hour_5:
      - position: absolute
      - top: 80px
      - left: |
          [[[
            const p = states["input_text.widget_tarif"].state.split("|");
            const [h, m] = p[7].split(":").map(Number);
            const start = h * 60 + m;
            let minutes = start - 420;
            if (minutes < 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${12 + px}px`;
          ]]]
    hour_6:
      - position: absolute
      - top: 80px
      - left: |
          [[[
            const p = states["input_text.widget_tarif"].state.split("|");
            const [h, m] = p[7].split(":").map(Number);
            const start = h * 60 + m;
            const duree = parseFloat(p[8]);
            const end = (start + duree * 60) % (24 * 60);
            let minutes = end - 420;
            if (minutes < 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${12 + px}px`;
          ]]]
    hour_7:
      - position: absolute
      - top: 80px
      - left: |
          [[[
            const p = states["input_text.widget_tarif"].state.split("|");
            const [h, m] = p[1].split(":").map(Number);
            const start = h * 60 + m;
            const duree = parseFloat(p[2]);
            const end = (start + duree * 60) % 1440;
            let minutes = end - 420;
            if (minutes <= 0) minutes += 1440;
            const px = (minutes / 60) * 18;
            return `${12 + px}px`;
          ]]]
    icon:
      - position: absolute
      - top: 110px
      - left: 12px
    comment:
      - position: absolute
      - top: 110px
      - left: 40px
    label:
      - position: absolute
      - top: 110px
      - left: |
          [[[
            function getTextWidth(text, fontSize) {
              const canvas = document.createElement('canvas');
              const context = canvas.getContext('2d');
              context.font = fontSize + ' Roboto, sans-serif';
              return context.measureText(text).width;
            }
            const d = new Date();
            const days = ["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"];
            const months = ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"];
            const day = days[d.getDay()];
            const date = d.getDate();
            const month = months[d.getMonth()];
            const year = d.getFullYear();
            const hh = ("0" + d.getHours()).slice(-2);
            const mm = ("0" + d.getMinutes()).slice(-2);
            const now = d.getHours() * 60 + d.getMinutes();
            const hcRanges = [
              { start: 1*60, end: 7*60 },
              { start: 12*60, end: 14*60 }
            ];
            const shcRanges = [
              { start: 2*60, end: 6*60 }
            ];
            let text = "";
            for (const r of shcRanges) {
              if (now >= r.start && now < r.end) {
                const delta = r.end - now;
                const rem = delta < 60
                  ? `${delta} min`
                  : `${Math.floor(delta/60)}h${("0"+(delta%60)).slice(-2)}`;
                text = `${day} ${date} ${month} ${year} à ${hh}h${mm}. Durée restante : ${rem}`;
                break;
              }
            }
            if (!text) {
              for (const r of hcRanges) {
                if (now >= r.start && now < r.end) {
                  const delta = r.end - now;
                  const rem = delta < 60
                    ? `${delta} min`
                    : `${Math.floor(delta/60)}h${("0"+(delta%60)).slice(-2)}`;
                  text = `${day} ${date} ${month} ${year} à ${hh}h${mm}. Durée restante : ${rem}`;
                  break;
                }
              }
            }
            if (!text) {
              let nextStart = null;
              for (const r of hcRanges) {
                if (now < r.start) {
                  nextStart = r.start;
                  break;
                }
              }
              if (nextStart === null) nextStart = hcRanges[0].start + 1440;
              const delta = nextStart - now;
              const remaining = delta < 60
                ? `${delta} min`
                : `${Math.floor(delta/60)}h${("0"+(delta%60)).slice(-2)}`;
              text = `${day} ${date} ${month} ${year} à ${hh}h${mm}. Dans ${remaining}, je serai en`;
            }
            const width = getTextWidth(text, '11px');
            return `${40 + width + 10}px`;
          ]]]
    plage_principale:
      - position: absolute
      - left: 10px
      - top: 30px
    debut_principale:
      - position: absolute
      - left: 20px
      - top: 55px
    config_debut_principale:
      - position: absolute
      - left: 80px
      - top: 55px
    duree_principale:
      - position: absolute
      - left: 20px
      - top: 77px
    config_duree_principale:
      - position: absolute
      - left: 80px
      - top: 77px
    plage_secondaire:
      - position: absolute
      - left: 50%
      - top: 30px
    debut_secondaire:
      - position: absolute
      - left: 50%
      - top: 55px
    config_debut_secondaire:
      - position: absolute
      - right: 18%
      - top: 55px
    duree_secondaire:
      - position: absolute
      - left: 50%
      - top: 77px
    config_duree_secondaire:
      - position: absolute
      - right: 18%
      - top: 77px
    plage_super_creuses:
      - position: absolute
      - left: 10px
      - top: 100px
    btn_super_creuses:
      - position: absolute
      - left: 170px
      - top: 100px
    btn_annuler:
      - position: absolute
      - right: 120px
      - bottom: 4px
    btn_valider:
      - position: absolute
      - right: 10px
      - bottom: 4px

Cette utilisation d’un input_text avec la fonction split permet de répondre à ce sujet : [AIDE] Tableau 24×7 avec cellules cliquables sans créer 168 entités
Il suffit d’utiliser une entrée de type liste avec 7 lignes, chaque ligne formée d’une chaine de 0 ou de 1 séparés par des « | » ou tout autre caractère.

5 « J'aime »

T’es un grannd malade M. ButtonCard

On ne peut mettre qu’en smiley de réaction sur un post, mais tu mérites au moins tous ceux-ci :
:astonished_face:
:heart:
:exploding_head:
:tada:
:+1:t2:

En tout cas, super carte.
FÉLICITATIONS !

Oui, d’autant plus que je suis en tarif de base :rofl:
Mais @Tank you very muche :grin:

Franchement très bien ta carte… Beau boulot. Il va falloir que j’adapte à mes heures.

3h36 à 7h36 et de 12h36 à 16h36. Mercredi, samedi, dimanche et jours fériés HC toute la journée

Salut,

C’est qui ton fournisseur et quel est le nom de ton contrat ? Je pourrait éventuellement ajouter un système de sélection par fournisseurs et contrats. Après, 07h36 c’est pas top avec une barre qui va de 07h00 à 07h00… Ce qui est étonnant c’est que c’est Enedis qui détermine les plages d’heures creuses et s’ils commencent à mettre des plages de ce type, on va vite passer à 20 000 lignes de code :rofl:

Verte Electrique Auto - Option Heures Creuses + Week-End | 6 kVA | EDF

C’est donc bien Enedis qui t’a imposé des plages de type hh:36 ?

oui tout à fait. avec une voiture elec on avait ca comme contrat

du coup ça change complètement les prédicats que j’avais posé :

  • la durée des « heures creuses » est de 8 heures (sur 24 heures glissantes entre 07:00 et 07:00 du jour suivant) ;
  • les heures creuses sont constituées d’une plage principale située entre 23h00 et 07h00 du matin le jour suivant ;
  • la durée minimale de la plage principale est de 5 heures et en cas de plage secondaire, elle ne peut excéder 6 heures (il ne peut pas y avoir une plage principale d’une durée de 7 heures avec une plage secondaire d’une durée d’une heure) ;
  • une plage secondaire est possible (si la durée de la plage principale est inférieure à 8 heures). Dans ce cas, la plage secondaire est comprise entre 11h00 et 17h00 ;
  • la durée de la plage secondaire est fonction de la durée de la plage principale, elle peut donc être de 2 heures, 2 heures et 30 minutes ou 3 heures ;
  • il est possible d’avoir une plage « Heures Super Creuses » (offre Charge’Heures Électricité de Total Energies) située entre 02h00 et 06h00 du matin (uniquement possible quand il y a une plage principale d’une durée de 8 heures).

Ca change tout : le positionnement de la barre (dans ton cas, il faudrait qu’elle couvre 24 heures de 07h36 à 07h36). La durée des plages est elle aussi totalement différente (4 heures par plage). Il faudrait refaire l’ensemble du bloc de configuration avec une possiblité de définir le début de chaque plage à la minute et plus toutes les 30 minutes. C’est faisable mais il faudra passer par un système d’onglets si je veux garder une carte relativement compacte : un premier onglet pour la plage « principale » qui ne sera plus forcément une plage principale, un deuxième onglet pour la deuxième plage et un troisième onglet pour la sélection des jours avec HC toute la journée… Quand tu dis toute la journée, le mercredi de 07h36 jusqu’au jeudi 07h36 et idem pour le samedi, le dimanche et les jours fériés ?

Voilà ce que j’ai fait vite fait

type: custom:button-card
name: widget tarification
show_icon: false
show_state: false
show_label: false
entity: sensor.time
tap_action:
  action: none
hold_action:
  action: none
double_tap_action:
  action: none
custom_fields:
  bar:
    card:
      type: custom:button-card
      styles:
        card:
          - height: 18px
          - width: 432px
          - border-radius: 999px
          - border: none
          - padding: 0
          - background: |
              [[[
                const PX_H = 18;                    // 24h * 18px = 432px
                const TOTAL = 24 * PX_H;
                const C_HP = "rgba(240,160,40,1.0)"; // orange
                const C_HC = "rgba(80,170,80,1.0)";  // vert

                const toMin = (t) => {
                  const [h,m] = t.split(":").map(Number);
                  return h*60+m;
                };
                const minToPx = (m) => (m/60)*PX_H;

                const d = new Date();
                const wd = d.getDay(); // 0=dim ... 6=sam
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd); // dim, mer, sam

                let hc = [];
                if (special) {
                  hc = [[0,1440]];
                } else {
                  hc = [
                    [toMin("03:36"), toMin("07:36")],
                    [toMin("12:36"), toMin("16:36")]
                  ];
                }

                // normalisation (au cas où)
                const segs = [];
                hc.forEach(([s,e]) => {
                  if (e <= 1440) segs.push([s,e]);
                  else { segs.push([s,1440]); segs.push([0,e-1440]); }
                });

                let stops = [];
                let cursor = 0;
                const addSeg = (sPx, ePx, col) => {
                  if (sPx > cursor) {
                    stops.push(`${C_HP} ${cursor}px`, `${C_HP} ${sPx}px`);
                  }
                  stops.push(`${col} ${sPx}px`, `${col} ${ePx}px`);
                  cursor = ePx;
                };

                segs
                  .map(([s,e]) => [minToPx(s), minToPx(e)])
                  .sort((a,b)=>a[0]-b[0])
                  .forEach(([sPx,ePx]) => addSeg(sPx,ePx,C_HC));

                if (cursor < TOTAL) {
                  stops.push(`${C_HP} ${cursor}px`, `${C_HP} ${TOTAL}px`);
                }

                return `linear-gradient(to right, ${stops.join(",")})`;
              ]]]
  cursor:
    card:
      type: custom:button-card
      icon: mdi:water
      custom_fields:
        circle:
          card:
            type: custom:button-card
            styles:
              card:
                - width: 8px
                - height: 8px
                - border-radius: 50%
                - background: var(--card-background-color)
      styles:
        card:
          - background: transparent
          - border: none
          - box-shadow: none
          - width: 32px
          - height: 32px
          - border-radius: 50%
          - transform: translateX(-50%) scaleY(-1)
        icon:
          - width: 100%
          - color: rgba(90,90,90,1.0)
        custom_fields:
          circle:
            - position: absolute
            - bottom: 10px
            - left: 12px
  hc_matin_start:
    card:
      type: custom:button-card
      name: "03:36"
      styles:
        card:
          - background: transparent
          - border: none
          - display: |
              [[[
                const d = new Date();
                const wd = d.get = d.getDay();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd);
                return special ? "none" : "block";
              ]]]
        name:
          - font-size: 0.7rem
          - font-weight: 700
          - color: rgba(90,90,90,1.0)
  hc_matin_end:
    card:
      type: custom:button-card
      name: "07:36"
      styles:
        card:
          - background: transparent
          - border: none
          - display: |
              [[[
                const d = new Date();
                const wd = d.getDay();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd);
                return special ? "none" : "block";
              ]]]
        name:
          - font-size: 0.7rem
          - font-weight: 700
          - color: rgba(90,90,90,1.0)
  hc_aprem_start:
    card:
      type: custom:button-card
      name: "12:36"
      styles:
        card:
          - background: transparent
          - border: none
          - display: |
              [[[
                const d = new Date();
                const wd = d.getDay();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd);
                return special ? "none" : "block";
              ]]]
        name:
          - font-size: 0.7rem
          - font-weight: 700
          - color: rgba(90,90,90,1.0)
  hc_aprem_end:
    card:
      type: custom:button-card
      name: "16:36"
      styles:
        card:
          - background: transparent
          - border: none
          - display: |
              [[[
                const d = new Date();
                const wd = d.getDay();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd);
                return special ? "none" : "block";
              ]]]
        name:
          - font-size: 0.7rem
          - font-weight: 700
          - color: rgba(90,90,90,1.0)
  hc_journee:
    card:
      type: custom:button-card
      name: HC journée
      styles:
        card:
          - background: transparent
          - border: none
          - display: |
              [[[
                const d = new Date();
                const wd = d.getDay();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd);
                return special ? "block" : "none";
              ]]]
        name:
          - font-size: 0.75rem
          - font-weight: 800
          - color: rgba(90,90,90,1.0)
  icon:
    card:
      type: custom:button-card
      icon: mdi:circle
      styles:
        card:
          - height: 24px
          - width: 24px
          - background: transparent
          - border: none
        icon:
          - width: 100%
          - color: |
              [[[
                const d = new Date();
                const cur = d.getHours()*60 + d.getMinutes();
                const wd = d.getDay();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const special = ferie || [0,3,6].includes(wd);

                if (special) return "rgba(80,170,80,1.0)";

                const inRange = (s,e) => cur>=s && cur<e;
                const hc = inRange(3*60+36, 7*60+36) || inRange(12*60+36, 16*60+36);
                return hc ? "rgba(80,170,80,1.0)" : "rgba(240,160,40,1.0)";
              ]]]
  comment:
    card:
      type: custom:button-card
      name: |
        [[[
          const pad = (n) => String(n).padStart(2,"0");
          const d = new Date();
          const cur = d.getHours()*60 + d.getMinutes();

          const days = ["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"];
          const months = ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"];
          const dateTxt = `${days[d.getDay()]} ${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()} à ${pad(d.getHours())}h${pad(d.getMinutes())}`;

          const ferie = (states["calendar.france_2"]?.state === "on");
          const wd = d.getDay();
          const special = ferie || [0,3,6].includes(wd);

          const fmt = (mins) => mins < 60 ? `${mins} min` : `${Math.floor(mins/60)}h${pad(mins%60)}`;

          if (special) {
            const rem = 1440 - cur;
            return `${dateTxt}. HC toute la journée. Durée restante : ${fmt(rem)}`;
          }

          const ranges = [
            {s: 3*60+36, e: 7*60+36},
            {s: 12*60+36, e: 16*60+36}
          ];

          const inside = ranges.find(r => cur>=r.s && cur<r.e);
          if (inside) {
            return `${dateTxt}. Durée restante : ${fmt(inside.e - cur)}`;
          }

          const next = ranges
            .map(r => (r.s <= cur ? r.s + 1440 : r.s))
            .sort((a,b)=>a-b)[0];

          return `${dateTxt}. Dans ${fmt(next - cur)}, je serai en HC`;
        ]]]
      styles:
        card:
          - background: transparent
          - border: none
          - border-radius: 0
          - width: auto
          - height: 24px
        name:
          - color: rgba(90,90,90,1.0)
          - font-size: 0.8rem
  label:
    card:
      type: custom:button-card
      name: |
        [[[
          const d = new Date();
          const cur = d.getHours()*60 + d.getMinutes();
          const ferie = (states["calendar.france_2"]?.state === "on");
          const wd = d.getDay();
          const special = ferie || [0,3,6].includes(wd);

          if (special) return "Heures creuses";
          const inRange = (s,e) => cur>=s && cur<e;
          const hc = inRange(3*60+36, 7*60+36) || inRange(12*60+36, 16*60+36);
          return hc ? "Heures creuses" : "Heures pleines";
        ]]]
      styles:
        card:
          - height: 22px
          - border-radius: 999px
          - border: none
          - background: |
              [[[
                const d = new Date();
                const cur = d.getHours()*60 + d.getMinutes();
                const ferie = (states["calendar.france_2"]?.state === "on");
                const wd = d.getDay();
                const special = ferie || [0,3,6].includes(wd);

                if (special) return "rgba(80,170,80,1.0)";
                const inRange = (s,e) => cur>=s && cur<e;
                const hc = inRange(3*60+36, 7*60+36) || inRange(12*60+36, 16*60+36);
                return hc ? "rgba(80,170,80,1.0)" : "rgba(240,160,40,1.0)";
              ]]]
        name:
          - color: var(--card-background-color)
          - padding: 0 10px
          - font-size: 0.8rem
styles:
  card:
    - aspect-ratio: 3/1
    - position: relative
    - border: none
    - background: |-
        linear-gradient(to bottom,
          rgba(114,157,133,1.0) 0px,
          rgba(114,157,133,1.0) 28px,
          var(--card-background-color) 28px)
    - pointer-events: none
  name:
    - align-self: start
    - margin-top: "-14px"
    - color: var(--card-background-color)
  custom_fields:
    bar:
      - position: absolute
      - left: 24px
      - top: 64px
    hc_matin_start:
      - position: absolute
      - top: 48px
      - left: |
          [[[
            const PX_H = 18;
            const startMin = 3*60+36;
            return `${24 + (startMin/60)*PX_H - 10}px`;
          ]]]
    hc_matin_end:
      - position: absolute
      - top: 48px
      - left: |
          [[[
            const PX_H = 18;
            const endMin = 7*60+36;
            return `${24 + (endMin/60)*PX_H - 10}px`;
          ]]]
    hc_aprem_start:
      - position: absolute
      - top: 48px
      - left: |
          [[[
            const PX_H = 18;
            const startMin = 12*60+36;
            return `${24 + (startMin/60)*PX_H - 12}px`;
          ]]]
    hc_aprem_end:
      - position: absolute
      - top: 48px
      - left: |
          [[[
            const PX_H = 18;
            const endMin = 16*60+36;
            return `${24 + (endMin/60)*PX_H - 12}px`;
          ]]]
    hc_journee:
      - position: absolute
      - top: 48px
      - left: 200px
    cursor:
      - position: absolute
      - top: 32px
      - left: |
          [[[
            const PX_H = 18;
            const d = new Date();
            const nowMin = d.getHours()*60 + d.getMinutes();
            return `${24 + (nowMin/60)*PX_H}px`;
          ]]]
    icon:
      - position: absolute
      - top: 110px
      - left: 12px
    comment:
      - position: absolute
      - top: 110px
      - left: 40px
    label:
      - position: absolute
      - top: 110px
      - right: 10px

Je n’ai plus le temps ce soir. Je regarde ça demain, mais mon but est de mettre à disposition une carte dont tout le monde puisse se servir facilement (pas de modification du code). Dis moi si tout fonctionne pour toi et ça me servira de base pour reprendre le code complet. Il me faudra un maximum d’exemples d’utilisateurs du forum pour pouvoir générer une carte « universelle ».

Ma cate que j’ai paste fonctionne

Un grand merci @btncrd :slight_smile:

vu l’infinie paramétrabilité et le nombre de lignes qui en découlent, est ce qu’il vaudrait mieux pas envisager un script qui crée la carte en fonction des réponses de l’utilisateur ?

Bonne idée… ca peut être génial