
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.