Skip to content

Instantly share code, notes, and snippets.

@hectorzin
Last active August 26, 2025 06:56
Show Gist options
  • Select an option

  • Save hectorzin/eaf4f74b2ab5fc1b3858136c561aa0d8 to your computer and use it in GitHub Desktop.

Select an option

Save hectorzin/eaf4f74b2ab5fc1b3858136c561aa0d8 to your computer and use it in GitHub Desktop.
Panel calidad del aire
type: vertical-stack
cards:
- type: custom:button-card
name: Calidad del aire exterior
entity: sensor.o_1pst_temperatura
double_tap_action:
action: more-info
entity: sensor.o_1pst_humedad
show_state: true
show_icon: false
custom_fields:
pill_t: |
[[[
const t = Number(states['sensor.o_1pst_temperatura']?.state) || 0;
return (t.toFixed(1) + ' °C');
]]]
pill_h: |
[[[
const h = Number(states['sensor.o_1pst_humedad']?.state) || 0;
return (h.toFixed(0) + ' %');
]]]
styles:
grid:
- grid-template-areas: "\"n pill_t pill_h\" \"s pill_t pill_h\""
- grid-template-columns: 1fr auto auto
- grid-template-rows: auto auto
- column-gap: 8px
- row-gap: 2px
card:
- padding: 16px
- border-radius: 18px
- box-shadow: none
- color: black
- background: |
[[[
const co2 = Number(states['sensor.o_1pst_dioxido_de_carbono']?.state) || 0;
const pm25 = Number(states['sensor.o_1pst_pm2_5']?.state) || 0;
const pm10 = Number(states['sensor.o_1pst_pm10']?.state) || 0;
const voc = Number(states['sensor.o_1pst_indice_cov']?.state) || 0;
const nox = Number(states['sensor.o_1pst_indice_nox']?.state) || 0;
const sevCo2 = v => v<=800?0 : v<=1000?1 : v<=1400?2 : v<=2000?3 : 4;
const sevPM25 = v => v<=10?0 : v<=15?1 : v<=25?2 : v<=35?3 : v<=55?4 : 5;
const sevPM10 = v => v<=20?0 : v<=45?2 : v<=90?3 : 4;
const sevVOC = v => v<=100?0 : v<=200?2 : v<=300?3 : 4;
const sevNOx = v => v<=50?0 : v<=100?2 : v<=200?3 : 4;
const worst = Math.max(sevCo2(co2), sevPM25(pm25), sevPM10(pm10), sevVOC(voc), sevNOx(nox));
const colors = [
'linear-gradient(135deg,#00C853,#2ECC71)', // verde vivo
'linear-gradient(135deg,#C6FF00,#AEEA00)', // lima brillante
'linear-gradient(135deg,#FFEB3B,#FDD835)', // amarillo
'linear-gradient(135deg,#FFA726,#FB8C00)', // naranja
'linear-gradient(135deg,#FF5252,#E53935)', // rojo
'linear-gradient(135deg,#BA68C8,#8E24AA)' // morado (PM2.5 muy alto)
];
return colors[Math.min(worst,5)];
]]]
name:
- font-size: 18px
- font-weight: 700
state:
- font-size: 14px
- opacity: 0.9
custom_fields:
pill_t:
- align-self: center
- justify-self: end
- padding: 6px 10px
- border-radius: 999px
- font-weight: 700
- color: black
- background: |
[[[
const t = Number(states['sensor.o_1pst_temperatura']?.state) || 0;
if (t<10) return 'linear-gradient(135deg,#90CAF9,#64B5F6)'; // frío (azules vivos, claros)
if (t<18) return 'linear-gradient(135deg,#64B5F6,#42A5F5)';
if (t<=24) return 'linear-gradient(135deg,#00C853,#2ECC71)'; // templado (verde)
if (t<=28) return 'linear-gradient(135deg,#FFEB3B,#FDD835)'; // cálido
return 'linear-gradient(135deg,#FF5252,#E53935)'; // calor
]]]
pill_h:
- align-self: center
- justify-self: end
- padding: 6px 10px
- border-radius: 999px
- font-weight: 700
- color: black
- background: |
[[[
const h = Number(states['sensor.o_1pst_humedad']?.state) || 0;
if (h<30) return 'linear-gradient(135deg,#FFB74D,#FF9800)'; // seco
if (h<40) return 'linear-gradient(135deg,#FFEB3B,#FDD835)'; // seco-aceptable
if (h<=60) return 'linear-gradient(135deg,#00C853,#2ECC71)'; // confortable
if (h<=80) return 'linear-gradient(135deg,#90CAF9,#64B5F6)'; // húmedo
return 'linear-gradient(135deg,#64B5F6,#42A5F5)'; // muy húmedo (azules vivos)
]]]
state_display: |
[[[
const co2 = Number(states['sensor.o_1pst_dioxido_de_carbono']?.state) || 0;
const pm25 = Number(states['sensor.o_1pst_pm2_5']?.state) || 0;
const pm10 = Number(states['sensor.o_1pst_pm10']?.state) || 0;
const voc = Number(states['sensor.o_1pst_indice_cov']?.state) || 0;
const nox = Number(states['sensor.o_1pst_indice_nox']?.state) || 0;
const label = v => ['Bueno','Aceptable','Moderado','Regular','Malo','Muy malo'][v];
const sevCo2 = v => v<=800?0 : v<=1000?1 : v<=1400?2 : v<=2000?3 : 4;
const sevPM25 = v => v<=10?0 : v<=15?1 : v<=25?2 : v<=35?3 : v<=55?4 : 5;
const sevPM10 = v => v<=20?0 : v<=45?2 : v<=90?3 : 4;
const sevVOC = v => v<=100?0 : v<=200?2 : v<=300?3 : 4;
const sevNOx = v => v<=50?0 : v<=100?2 : v<=200?3 : 4;
const worst = Math.max(sevCo2(co2), sevPM25(pm25), sevPM10(pm10), sevVOC(voc), sevNOx(nox));
return label(worst);
]]]
- type: grid
columns: 2
square: false
cards:
- type: custom:button-card
entity: sensor.o_1pst_pm2_5
name: PM2.5 (µg/m³)
icon: mdi:blur
show_state: true
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(1)+' µg/m³';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=10) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=15) return 'linear-gradient(135deg,#C6FF00,#AEEA00)';
if (v<=25) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=35) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
if (v<=55) return 'linear-gradient(135deg,#FF9800,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_pm10
name: PM10 (µg/m³)
icon: mdi:blur-linear
show_state: true
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(0)+' µg/m³';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=20) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=45) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=90) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_dioxido_de_carbono
name: CO₂ (ppm)
icon: mdi:molecule-co2
show_state: true
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(0)+' ppm';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=800) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=1000) return 'linear-gradient(135deg,#C6FF00,#AEEA00)';
if (v<=1400) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=2000) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_indice_cov
name: COV — índice
icon: mdi:chemical-weapon
show_state: true
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=100) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=200) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=300) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_indice_nox
name: NOx — índice
icon: mdi:tailwind
show_state: true
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=50) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=100) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=200) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_pm1
name: PM1 (µg/m³)
icon: mdi:blur-radial
show_state: true
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(1)+' µg/m³';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=10) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=25) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=35) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: custom:button-card
name: Calidad del aire exterior
entity: sensor.o_1pst_pm2_5
tap_action:
action: more-info
show_state: true
show_icon: false
styles:
card:
- height: 56px
- padding: 12px 16px
- border-radius: 18px
- box-shadow: none
- color: black
- background: |
[[[
const co2 = Number(states['sensor.o_1pst_dioxido_de_carbono']?.state)||0;
const pm25 = Number(states['sensor.o_1pst_pm2_5']?.state)||0;
const pm10 = Number(states['sensor.o_1pst_pm10']?.state)||0;
const voc = Number(states['sensor.o_1pst_indice_cov']?.state)||0;
const nox = Number(states['sensor.o_1pst_indice_nox']?.state)||0;
const sevCo2 = v => v<=800?0 : v<=1000?1 : v<=1400?2 : v<=2000?3 : 4;
const sevPM25 = v => v<=10?0 : v<=15?1 : v<=25?2 : v<=35?3 : v<=55?4 : 5;
const sevPM10 = v => v<=20?0 : v<=45?2 : v<=90?3 : 4;
const sevVOC = v => v<=100?0 : v<=200?2 : v<=300?3 : 4;
const sevNOx = v => v<=50?0 : v<=100?2 : v<=200?3 : 4;
const worst = Math.max(sevCo2(co2), sevPM25(pm25), sevPM10(pm10), sevVOC(voc), sevNOx(nox));
const colors = [
'linear-gradient(135deg,#00C853,#2ECC71)',
'linear-gradient(135deg,#C6FF00,#AEEA00)',
'linear-gradient(135deg,#FFEB3B,#FDD835)',
'linear-gradient(135deg,#FFA726,#FB8C00)',
'linear-gradient(135deg,#FF5252,#E53935)',
'linear-gradient(135deg,#BA68C8,#8E24AA)'
];
return colors[Math.min(worst,5)];
]]]
name:
- font-size: 18px
- font-weight: 700
state:
- font-size: 14px
- opacity: 0.9
state_display: |
[[[
const co2 = Number(states['sensor.o_1pst_dioxido_de_carbono']?.state)||0;
const pm25 = Number(states['sensor.o_1pst_pm2_5']?.state)||0;
const pm10 = Number(states['sensor.o_1pst_pm10']?.state)||0;
const voc = Number(states['sensor.o_1pst_indice_cov']?.state)||0;
const nox = Number(states['sensor.o_1pst_indice_nox']?.state)||0;
const label = v => ['Bueno','Aceptable','Moderado','Regular','Malo','Muy malo'][v];
const sevCo2 = v => v<=800?0 : v<=1000?1 : v<=1400?2 : v<=2000?3 : 4;
const sevPM25 = v => v<=10?0 : v<=15?1 : v<=25?2 : v<=35?3 : v<=55?4 : 5;
const sevPM10 = v => v<=20?0 : v<=45?2 : v<=90?3 : 4;
const sevVOC = v => v<=100?0 : v<=200?2 : v<=300?3 : 4;
const sevNOx = v => v<=50?0 : v<=100?2 : v<=200?3 : 4;
const worst = Math.max(sevCo2(co2), sevPM25(pm25), sevPM10(pm10), sevVOC(voc), sevNOx(nox));
return label(worst);
]]]
- type: custom:button-card
entity: sensor.o_1pst_temperatura
tap_action:
action: more-info
show_name: false
show_icon: false
show_state: true
state_display: |
[[[
const v = Number(entity.state)||0; return v.toFixed(1)+' °C';
]]]
styles:
card:
- width: 92px
- height: 56px
- margin: 0 0 0 8px
- border-radius: 999px
- box-shadow: none
- color: black
- font-weight: 700
- background: |
[[[
const t = Number(entity.state)||0;
if (t<10) return 'linear-gradient(135deg,#90CAF9,#64B5F6)';
if (t<18) return 'linear-gradient(135deg,#64B5F6,#42A5F5)';
if (t<=24) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (t<=28) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_humedad
tap_action:
action: more-info
show_name: false
show_icon: false
show_state: true
state_display: |
[[[
const v = Number(entity.state)||0; return v.toFixed(0)+' %';
]]]
styles:
card:
- width: 92px
- height: 56px
- margin: 0 0 0 8px
- border-radius: 999px
- box-shadow: none
- color: black
- font-weight: 700
- background: |
[[[
const h = Number(entity.state)||0;
if (h<30) return 'linear-gradient(135deg,#FFB74D,#FF9800)';
if (h<40) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (h<=60) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (h<=80) return 'linear-gradient(135deg,#90CAF9,#64B5F6)';
return 'linear-gradient(135deg,#64B5F6,#42A5F5)';
]]]
- type: grid
columns: 2
square: false
cards:
- type: custom:button-card
entity: sensor.o_1pst_pm2_5
name: PM2.5 (µg/m³)
icon: mdi:blur
show_state: true
tap_action:
action: more-info
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(1)+' µg/m³';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=10) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=15) return 'linear-gradient(135deg,#C6FF00,#AEEA00)';
if (v<=25) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=35) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
if (v<=55) return 'linear-gradient(135deg,#FF9800,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_pm10
name: PM10 (µg/m³)
icon: mdi:blur-linear
show_state: true
tap_action:
action: more-info
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(0)+' µg/m³';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=20) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=45) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=90) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_dioxido_de_carbono
name: CO₂ (ppm)
icon: mdi:molecule-co2
show_state: true
tap_action:
action: more-info
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(0)+' ppm';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=800) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=1000) return 'linear-gradient(135deg,#C6FF00,#AEEA00)';
if (v<=1400) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=2000) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_indice_cov
name: COV — índice
icon: mdi:chemical-weapon
show_state: true
tap_action:
action: more-info
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=100) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=200) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=300) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_indice_nox
name: NOx — índice
icon: mdi:tailwind
show_state: true
tap_action:
action: more-info
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=50) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=100) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=200) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
- type: custom:button-card
entity: sensor.o_1pst_pm1
name: PM1 (µg/m³)
icon: mdi:blur-radial
show_state: true
tap_action:
action: more-info
state_display: |
[[[
const v = Number(entity.state)||0;
return v.toFixed(1)+' µg/m³';
]]]
styles:
card:
- border-radius: 16px
- box-shadow: none
- color: black
- padding: 14px
- background: |
[[[
const v = Number(entity.state)||0;
if (v<=10) return 'linear-gradient(135deg,#00C853,#2ECC71)';
if (v<=25) return 'linear-gradient(135deg,#FFEB3B,#FDD835)';
if (v<=35) return 'linear-gradient(135deg,#FFA726,#FB8C00)';
return 'linear-gradient(135deg,#FF5252,#E53935)';
]]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment