J’aimerai bien connaitre la méthode de mesurage (apparemment c’est un compteur Geiger : mesure en coups par minutes) mais vu la taille des sondes alpha d’un compteur Geiger traditionnel, je ne vois pas comment ils ont pu adapter ça à un appareil domestique.
Bonjour,
Je trouve cela très réussi, je suggère une amélioration : ajouter la valeur min et max du jour pour le capteur considéré :
Qu’en pensez vous ?
C’est en place sur les jauges de température :
Il suffit d’avoir un sensor qui récupère ces valeurs et de mettre dans le code de la carte « Radon » les custom_fields « min_temp_week » et « max_temp_week » (après le avoir renommés) :
min_temp_week:
card:
type: custom:button-card
icon: m3of:vertical-align-bottom
show_name: false
show_state: true
layout: icon_state
state_display: |
[[[
return parseFloat(states['sensor.salle_a_manger_temperature_min_semaine'].state).toFixed(1) + "°";
]]]
styles:
card:
- width: 80px
- padding: 0
- border: none
- border-radius: 0
- background: none
state:
- font-size: 1.2rem
- align-self: end
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 600
icon:
- width: 20px
- color: rgba(167,176,205,1.0)
max_temp_week:
card:
type: custom:button-card
icon: m3of:vertical-align-top
show_name: false
show_state: true
layout: state_icon
state_display: |
[[[
return parseFloat(states['sensor.salle_a_manger_temperature_max_semaine'].state).toFixed(1) + "°";
]]]
styles:
card:
- width: 80px
- padding: 0
- border: none
- border-radius: 0
- background: none
state:
- font-size: 1.2rem
- align-self: end
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 600
- padding-right: 100px
icon:
- width: 20px
- color: rgba(167,176,205,1.0)
Sur la carte « Température extérieure », les sensors sont calibrés sur 30 jours.
EDIT : Merde, il faut que j’arrête et que j’aille prendre mes gouttes : j’intègre les améliorations avant même qu’elles ne soient demandées ![]()
Super, par contre j’ai rajouté bêtement le script que tu donnes et les min et max ne s’affichent pas, comment faut il faire ?
Tu as fait des sensors pour obtenir les valeurs ? Si oui, il faut ajouter le positionnement des custom_fields dans les styles. Donne moi ton code complet stp.
colle l’ensemble du code en mode formaté (</>) car là c’est pas exploitable
Le plus simple est de reprendre le code de la carte « Température intérieure » posté en 53/128 et d’y mettre tes capteurs, tu auras ainsi une structure correcte car là, en collant à la suite les blocs concernant les valeurs minimales et maximales, çe n’ira pas (tu n’as pas la position des deux nouveaux custom_fields dans les styles)
Parfait et encore bravo !!!
Reste à paramétrer un peu …
Tu crées pas mal de fois les constantes min, max, min_temp,min_max auxquelles tu donnes les mêmes valeurs, tu ne pourrais pas les déclarer en variable au début du script de la carte ?
Bonjour,
Très belle jauges. J’aime bien la version compact. Pouvez-vous partager le code pour la jauge d’humidité SVP?
Merci
@MichelJ ,
Le plus simple est de demander au concepteur des cartes, non ?
Après, c’est pas évident avec la solution mise sur un des posts du créateur du sujet ![]()
Il faut voir si la portée des variables va jusqu’aux custom_fields. Cela fonctionne pour des templates mais je ne suis sur de rien pour la portée carte parent → cartes filles.
disons que le compteur geiger est sur un autre ESP. ils sont regroupés sur la même carte, c’est tout.
c’est comme le compteur de puissance sur la prise connectée de ma pompe de relevage, c’est pas dans le même boitier, mais j’ai regrouppé ca dans ‹ environnement ›
Voici ce qu’a fiat l’IA pour mettre les variables en début de code, ça a l’air d’être ok :
type: custom:button-card
entity: sensor.temperature_salon
variables:
v_min: 14
v_max: 30
show_state: false
show_icon: false
show_name: false
tap_action: none
double_tap_action: none
hold_action: none
custom_fields:
icon_and_name:
card:
type: horizontal-stack
cards:
- type: custom:button-card
icon: mdi:thermometer
styles:
card:
- aspect-ratio: 1/1
- width: 50px
- padding: 0
- border: 4px solid rgba(167,176,205,1.0)
- border-radius: 50%
- background: none
icon:
- width: 90%
- color: white
- type: custom:button-card
name: Température salon
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
name:
- font-size: 1.4rem
- justify-self: start
- color: white
- font-weight: 500
- padding-top: 6%
dividing_line:
card:
type: custom:button-card
styles:
card:
- width: 600px
- height: 1px
- padding: 0
- border: 1px solid rgba(54,56,68,1.0)
- border-radius: 0
- background: none
- overflow: hidden
value_and_unit:
card:
type: horizontal-stack
cards:
- type: custom:button-card
show_name: false
show_icon: false
show_state: true
state_display: |
[[[
return parseFloat(entity.state).toFixed(1);
]]]
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
- margin-right: "-10%"
state:
- color: white
- font-size: 1.8rem
- font-weight: 800
- type: custom:button-card
name: °C
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
- margin-left: "-10%"
name:
- font-size: 1.2rem
- align-self: start
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 800
- padding-top: 40%
bar:
card:
type: custom:button-card
show_name: false
show_icon: false
show_state: false
styles:
card:
- height: 12px
- width: 360px
- border-radius: 999px
- border: 0
- padding: 0px
- background: >-
linear-gradient(to right, rgba(0,0,255,1.0) 0%,
rgba(0,165,255,1.0) 25%, rgba(0,255,0,1.0) 50%,
rgba(255,255,0,1.0) 62.5%, rgba(255,165,0,1.0) 75%,
rgba(255,0,0,1.0) 100%)
cursor:
card:
type: custom:button-card
show_name: false
show_icon: false
show_state: false
styles:
card:
- width: 16px
- height: 32px
- border-radius: 999px
- border: 4px solid rgba(42,45,54,1.0)
- background-color: |
[[[
const minTemp = variables.v_min;
const maxTemp = variables.v_max;
const temp = Number(entity.state) || minTemp;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
const stops = [
{ p: 0, c: [0, 0, 255] },
{ p: 25, c: [0, 165, 255] },
{ p: 50, c: [0, 255, 0] },
{ p: 62.5, c: [255, 255, 0] },
{ p: 75, c: [255, 165, 0] },
{ p: 100, c: [255, 0, 0] }
];
let i = 1;
while (i < stops.length && percent > stops[i].p) i++;
const a = stops[i - 1];
const b = stops[i];
const t = (percent - a.p) / (b.p - a.p);
const lerp = (x, y, t) => Math.round(x + (y - x) * t);
const r = lerp(a.c[0], b.c[0], t);
const g = lerp(a.c[1], b.c[1], t);
const bcol = lerp(a.c[2], b.c[2], t);
return `rgba(${r},${g},${bcol},1.0)`;
]]]
min_val:
card:
type: custom:button-card
name: "[[[ return variables.v_min + '°C' ]]]"
styles:
card:
- width: auto
- padding: 0
- border: none
- background: none
name:
- font-size: 1.0rem
- color: rgba(167,176,205,1.0)
- font-weight: 400
- white-space: nowrap
max_val:
card:
type: custom:button-card
name: "[[[ return variables.v_max + '°C' ]]]"
styles:
card:
- width: auto
- padding: 0
- border: none
- background: none
name:
- font-size: 1.0rem
- color: rgba(167,176,205,1.0)
- font-weight: 400
- white-space: nowrap
comment:
card:
type: custom:button-card
show_name: true
show_icon: false
show_state: false
name: |
[[[
const temp = Number(entity.state) || 0;
if (temp < 18) return "Trop froid";
if (temp < 20) return "Frais mais acceptable";
if (temp < 22) return "Confort idéal";
if (temp < 24) return "Légèrement chaud";
return "Trop chaud";
]]]
styles:
card:
- width: auto
- height: auto
- border-radius: 999px
- border: 0
- padding: 5px 10px 5px 10px
- background-color: |
[[[
const minTemp = variables.v_min;
const maxTemp = variables.v_max;
const temp = Number(entity.state) || minTemp;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
const stops = [
{ p: 0, c: [0, 0, 255] },
{ p: 25, c: [0, 165, 255] },
{ p: 50, c: [0, 255, 0] },
{ p: 62.5, c: [255, 255, 0] },
{ p: 75, c: [255, 165, 0] },
{ p: 100, c: [255, 0, 0] }
];
let i = 1;
while (i < stops.length && percent > stops[i].p) i++;
const a = stops[i - 1] || stops[0];
const b = stops[i] || stops[stops.length - 1];
const t = (percent - a.p) / (b.p - a.p);
const lerp = (x, y, t) => Math.round(x + (y - x) * t);
const r = lerp(a.c[0], b.c[0], t);
const g = lerp(a.c[1], b.c[1], t);
const bcol = lerp(a.c[2], b.c[2], t);
return `rgba(${r},${g},${bcol},0.5)`;
]]]
name:
- font-size: 1.0rem
- font-weight: 600
- color: white
min_temp_week:
card:
type: custom:button-card
icon: mdi:arrow-down-bold
show_name: false
show_state: true
layout: icon_state
state_display: |
[[[
return parseFloat(states['sensor.temperature_salon_min'].state).toFixed(1) + "°";
]]]
styles:
card:
- width: 80px
- padding: 0
- border: none
- border-radius: 0
- background: none
state:
- font-size: 1.2rem
- align-self: end
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 600
icon:
- width: 20px
- color: rgba(167,176,205,1.0)
max_temp_week:
card:
type: custom:button-card
icon: mdi:arrow-up-bold
show_name: false
show_state: true
layout: state_icon
state_display: |
[[[
return parseFloat(states['sensor.temperature_salon_max'].state).toFixed(1) + "°";
]]]
styles:
card:
- width: 80px
- padding: 0
- border: none
- border-radius: 0
- background: none
state:
- font-size: 1.2rem
- align-self: end
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 600
icon:
- width: 20px
- color: rgba(167,176,205,1.0)
styles:
card:
- background-color: rgba(42,45,54,1.0)
- aspect-ratio: 2.8/1
- cursor: default
custom_fields:
icon_and_name:
- position: absolute
- top: 2%
- left: 1%
dividing_line:
- position: absolute
- top: 35%
- left: 0%
value_and_unit:
- position: absolute
- top: 38%
- left: |
[[[
const value = Number(entity.state) || 0;
const min = variables.v_min;
const max = variables.v_max;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((value - min) / (max - min)) * 100, 0, 100);
const barWidth = 360;
const halfCursor = 8;
const offsetPx = (barWidth * percent) / 100;
return `calc(50% - ${barWidth/2}px + ${offsetPx}px - ${halfCursor}px)`;
]]]
- transform: translateX(-50%)
bar:
- position: absolute
- left: 50%
- top: 65%
- transform: translate(-50%, -50%)
cursor:
- position: absolute
- top: calc(65% - 16px)
- left: |
[[[
const value = Number(entity.state) || 0;
const min = variables.v_min;
const max = variables.v_max;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((value - min) / (max - min)) * 100, 0, 100);
const barWidth = 360;
const halfCursor = 8;
const offsetPx = (barWidth * percent) / 100;
return `calc(50% - ${barWidth/2}px + ${offsetPx}px - ${halfCursor}px)`;
]]]
min_val:
- position: absolute
- bottom: 17%
- left: 12%
- width: 60px
- display: flex
- align-items: center
max_val:
- position: absolute
- bottom: 17%
- right: 10%
- width: 60px
- display: flex
- align-items: center
- justify-content: flex-end
comment:
- position: absolute
- left: 50%
- bottom: "-3%"
- transform: translate(-50%, -50%)
min_temp_week:
- position: absolute
- top: 40%
- left: 5%
max_temp_week:
- position: absolute
- top: 40%
- right: 2%
Et tu trouves qu’il y a un gain ?
Parce que entre :
- background-color: |
[[[
const minTemp = variables.v_min;
const maxTemp = variables.v_max;
const temp = Number(entity.state) || minTemp;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
et
- background-color: |
[[[
const min = 14;
const max = 30;
const temp = Number(entity.state) || minTemp;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
Je ne suis pas sur qu’on soit au summum de l’amélioration ![]()
Le but à la base était de répondre à la demande de @Xris :
Et il est vrai que je ne me suis pas plus que ça penché sur l’optimisation du code ![]()
Au summum non, on est ok !!
Le gain c’est que les valeurs en dur ne sont présentes qu’une seule fois au début du script ?
Ce n’est pas une critique !!!
Vu les résultat je ne me permettais pas !
L’idéal serait d’avoir un template avec lequel tu puisses avoir ton modèle unique adaptable à toutes les cartes (humidité, température, co2, pollens, IQA, etc.). Il faudra que j’y travaille ![]()
T’inquiète, je ne prends pas tes remarques pour une critique. De toute façon, avec la tête dans le guidon, tu rates forcément des choses et toute remarque constructive est bonne à prendre ![]()
En effet !
Pour ceux qui ont normalisé les noms de leurs sensors :
sensor.temperature_salon
sensor.temperature_salon_min
sensor.temperature_salon_max
il serait possible de mettre states[entity.entity_id + ‹ _min ›].state au lieu de states[‹ sensor.temperature_salon_min ›].state
Bon j’essaye d’adapter le capteur température pour l’extérieur mais je dois passer à côté d’un truc, je pense dans le calcul du positionnement du curseur et pour que les commentaires fonctionnent.
Je crois que le capteur ne remonte pas le min et le max, du coup il faudrait jouer sur l’historique ?
Je n’ai pas attaqué encore l’adaptation pollen et qualité d’air, je me dis qu’il vaut mieux y aller l’un après l’autre.
Je ne suis plus très loin :

type: custom:button-card
entity: sensor.netatmo_jardin_temperature
show_state: false
show_icon: false
show_name: false
tap_action: none
double_tap_action: none
hold_action: none
custom_fields:
icon:
card:
type: custom:button-card
icon: mdi:thermometer
styles:
card:
- aspect-ratio: 1/1
- width: "[[[ return window.innerWidth <= 600 ? '20px' : '25px' ]]]"
- border: none
- border-radius: 50%
- background: none
icon:
- width: "[[[ return window.innerWidth <= 600 ? '18px' : '20px' ]]]"
- color: white
icon_border:
card:
type: custom:button-card
styles:
card:
- aspect-ratio: 1/1
- width: "[[[ return window.innerWidth <= 600 ? '23px' : '25px' ]]]"
- border: 1px solid white
- border-radius: 50%
- background: none
name:
card:
type: custom:button-card
name: Netatmo Jardin Température
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
name:
- font-size: "[[[ return window.innerWidth <= 600 ? '0.9rem' : '1.0rem' ]]]"
- align-self: start
- justify-self: start
- color: white
- font-weight: 500
bar:
card:
type: custom:button-card
show_name: false
show_icon: false
show_state: false
styles:
card:
- height: 6px
- width: |
[[[ return window.innerWidth <= 600 ? "220px" : "250px"; ]]]
- border-radius: 999px
- border: 0
- padding: 0
- background: |-
linear-gradient(to right, rgba(0,0,255,1.0) 0%,
rgba(0,165,255,1.0) 25%, rgba(0,255,0,1.0) 50%,
rgba(255,255,0,1.0) 75%, rgba(255,0,0,1.0) 100%)
cursor:
card:
type: custom:button-card
show_name: false
show_icon: false
show_state: false
styles:
card:
- width: 12px
- height: 25px
- border-radius: 999px
- border: 4px solid rgba(42,45,54,1.0)
- background-color: |
[[[
const minTemp = 14;
const maxTemp = 30;
const temp = Number(entity.state) || minTemp;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
const stops = [
{ p: 0, c: [0, 0, 255, 1.0] },
{ p: 25,c: [0, 165, 255, 1.0] },
{ p: 50, c: [0, 255, 0, 1.0] },
{ p: 75,c: [255, 255, 0, 1.0] },
{ p: 100, c: [255, 0, 0, 1.0] }
];
let i = 1;
while (i < stops.length && percent > stops[i].p) i++;
const a = stops[i - 1], b = stops[i];
const t = (percent - a.p) / (b.p - a.p);
const lerp = (x, y, t) => Math.round(x + (y - x) * t);
const r = lerp(a.c[0], b.c[0], t);
const g = lerp(a.c[1], b.c[1], t);
const bcol = lerp(a.c[2], b.c[2], t);
return `rgba(${r},${g},${bcol},1.0)`;
]]]
value:
card:
type: custom:button-card
show_name: false
show_icon: false
show_state: true
state_display: |
[[[
const v = Number(entity.state);
return isNaN(v) ? '—' : v.toFixed(1);
]]]
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
state:
- color: white
- font-size: "[[[ return window.innerWidth <= 600 ? '1.6rem' : '1.8rem' ]]]"
- font-weight: 700
unit:
card:
type: custom:button-card
name: °C
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
name:
- font-size: "[[[ return window.innerWidth <= 600 ? '1.1rem' : '1.3rem' ]]]"
- align-self: start
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 800
min_val:
card:
type: custom:button-card
name: -20°C
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
name:
- font-size: "[[[ return window.innerWidth <= 600 ? '0.8rem' : '1.0rem' ]]]"
- align-self: start
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 400
max_val:
card:
type: custom:button-card
name: 60°C
styles:
card:
- width: auto
- padding: 0
- border: none
- border-radius: 0
- background: none
name:
- font-size: "[[[ return window.innerWidth <= 600 ? '0.8rem' : '1.0rem' ]]]"
- align-self: start
- justify-self: start
- color: rgba(167,176,205,1.0)
- font-weight: 400
comment:
card:
type: custom:button-card
show_name: true
show_icon: false
show_state: false
name: |
[[[
const temp = Number(entity.state);
if (temp < -5) return "Froid extrême";
if (temp >= -5 && temp < 0) return "Très froid";
if (temp >= 0 && temp < 10) return "Frais";
if (temp >= 10 && temp < 20) return "Confortable";
if (temp >= 20 && temp < 30) return "Chaud";
if (temp >= 30 && temp < 40) return "Très chaud";
if (temp >= 40) return "Chaleur extrême";
]]]
styles:
card:
- height: 25px
- width: auto
- border-radius: 999px
- border: none
- padding: 0px 8px 0px 8px
- background: white
- background-color: |
[[[
const minTemp = -20;
const maxTemp = 60;
const temp = Number(entity.state);
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = isNaN(temp) ? 0 : clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
const stops = [
{ p: 0, c: [0, 0, 255, 1.0] },
{ p: 25,c: [0, 165, 255, 1.0] },
{ p: 50, c: [0, 255, 0, 1.0] },
{ p: 75,c: [255, 255, 0, 1.0] },
{ p: 100, c: [255, 0, 0, 1.0] }
];
let i = 1;
while (i < stops.length && percent > stops[i].p) i++;
const a = stops[i - 1], b = stops[i];
const t = (percent - a.p) / (b.p - a.p);
const lerp = (x, y, t) => Math.round(x + (y - x) * t);
const r = lerp(a.c[0], b.c[0], t);
const g = lerp(a.c[1], b.c[1], t);
const bcol = lerp(a.c[2], b.c[2], t);
return `rgba(${r},${g},${bcol},0.5)`;
]]]
name:
- font-size: "[[[ return window.innerWidth <= 600 ? '0.8rem' : '0.9rem' ]]]"
- font-weight: 600
- line-height: |
[[[
return window.innerWidth <= 600 ? "0.9" : "normal";
]]]
- color: |
[[[
const minTemp = -20;
const maxTemp = 60;
const temp = Number(entity.state);
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = isNaN(temp) ? 0 : clamp(((temp - minTemp) / (maxTemp - minTemp)) * 100, 0, 100);
const stops = [
{ p: 0, c: [0, 0, 255, 1.0] },
{ p: 25,c: [0, 165, 255, 1.0] },
{ p: 50, c: [0, 255, 0, 1.0] },
{ p: 75,c: [255, 255, 0, 1.0] },
{ p: 100, c: [255, 0, 0, 1.0] }
];
let i = 1;
while (i < stops.length && percent > stops[i].p) i++;
const a = stops[i - 1], b = stops[i];
const t = (percent - a.p) / (b.p - a.p);
const lerp = (x, y, t) => Math.round(x + (y - x) * t);
const r = lerp(a.c[0], b.c[0], t);
const g = lerp(a.c[1], b.c[1], t);
const bcol = lerp(a.c[2], b.c[2], t);
return `rgba(${r},${g},${bcol},1.0)`;
]]]
styles:
card:
- background-color: rgba(42,45,54,1.0)
- aspect-ratio: 7/1
- cursor: default
custom_fields:
icon:
- position: absolute
- top: 55%
- left: 4%
- transform: translate(-50%, -50%)
icon_border:
- position: absolute
- top: "[[[ return window.innerWidth <= 600 ? '56%' : '55%' ]]]"
- left: 4%
- transform: translate(-50%, -50%)
name:
- position: absolute
- top: "[[[ return window.innerWidth <= 600 ? '1%' : '3%' ]]]"
- left: 8%
value:
- position: absolute
- top: 0%
- right: 6.5%
- margin-top: "-0.5%"
unit:
- position: absolute
- right: 2%
- top: 1%
- margin-left: 1%
min_val:
- position: absolute
- bottom: 2%
- left: "[[[ return window.innerWidth <= 600 ? '34%' : '38%' ]]]"
max_val:
- position: absolute
- bottom: 2%
- right: "[[[ return window.innerWidth <= 600 ? '2%' : '4%' ]]]"
comment:
- position: absolute
- top: 55%
- left: |
[[[
const iconLeftPercent = 1;
const iconDiameterPx = window.innerWidth <= 600 ? 20 : 25;
const barWidthPx = window.innerWidth <= 600 ? 220 : 250;
const barRightPercent = window.innerWidth <= 600 ? 4 : 7;
const iconRightExpr = `calc(${iconLeftPercent}% + ${iconDiameterPx}px)`;
const barLeftExpr = `calc(100% - ${barRightPercent}% - ${barWidthPx}px)`;
return `calc( ( ${iconRightExpr} + ${barLeftExpr} ) / 2 )`;
]]]
- transform: translate(-50%, -50%)
bar:
- position: absolute
- right: "[[[ return window.innerWidth <= 600 ? '4%' : '7%' ]]]"
- top: 55%
- transform: translateY(-50%)
cursor:
- position: absolute
- top: "[[[ return 'calc(55% - 12.5px)'; ]]]"
- left: |
[[[
const temp = Number(entity.state);
const value = isNaN(temp) ? -20 : temp;
const min = -20;
const max = 60;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const percent = clamp(((value - min) / (max - min)) * 100, 0, 100);
const barWidth = (window.innerWidth <= 600) ? 300 : 360;
const halfCursor = 8;
const offsetPx = (barWidth * percent) / 100;
return `calc(50% - ${barWidth/2}px + ${offsetPx}px - ${halfCursor}px)`;
]]]





