Affichage graphique des Heures pleines/creuses

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.

6 « J'aime »