Compare commits

..

No commits in common. "main" and "v2.1.0" have entirely different histories.
main ... v2.1.0

3 changed files with 12 additions and 180 deletions

View file

@ -1,18 +0,0 @@
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
validate-hacs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: HACS Validation
uses: hacs/action@main
with:
category: plugin

View file

@ -55,7 +55,6 @@ temperature_entity: sensor.air_quality_temperature
air_quality_entity: sensor.air_quality_index
recommendation_entity: sensor.air_quality_recommendation
hours_to_show: 24
temperature_unit: C
```
### Configuration Options
@ -70,7 +69,6 @@ temperature_unit: C
| `air_quality_entity` | string | No | - | Overall air quality index entity |
| `recommendation_entity` | string | No | - | Recommendation template sensor |
| `hours_to_show` | number | No | 24 | Hours of history to display (1-168) |
| `temperature_unit` | string | No | "F" | Temperature unit: "F" (Fahrenheit) or "C" (Celsius) |
## Recommendation Sensor

View file

@ -6,7 +6,7 @@
* https://github.com/KadenThomp36/air-quality-card
*/
const CARD_VERSION = '2.2.0';
const CARD_VERSION = '2.1.0';
class AirQualityCard extends HTMLElement {
// Visual editor using getConfigForm (preferred modern approach)
@ -21,13 +21,6 @@ class AirQualityCard extends HTMLElement {
{ name: 'pm25_entity', selector: { entity: { domain: 'sensor' } } },
]
},
{
type: 'grid',
schema: [
{ name: 'hcho_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'tvoc_entity', selector: { entity: { domain: 'sensor' } } },
]
},
{
type: 'grid',
schema: [
@ -42,7 +35,6 @@ class AirQualityCard extends HTMLElement {
{ name: 'air_quality_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'recommendation_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'hours_to_show', selector: { number: { min: 1, max: 168, mode: 'box', unit_of_measurement: 'hours' } } },
{ name: 'temperature_unit', selector: { select: { options: [{ value: 'F', label: 'Fahrenheit (°F)' }, { value: 'C', label: 'Celsius (°C)' }], mode: 'dropdown' } } },
]
}
],
@ -54,11 +46,8 @@ class AirQualityCard extends HTMLElement {
humidity_entity: 'Humidity Sensor (optional)',
temperature_entity: 'Temperature Sensor (optional)',
air_quality_entity: 'Air Quality Index (optional)',
hcho_entity: 'Formaldehyde (HCHO; CH2O) Sensor (optional)',
tvoc_entity: 'Volatile Organic Compounds (tVOC) Sensor (optional)',
recommendation_entity: 'Recommendation Sensor (optional)',
hours_to_show: 'Graph History',
temperature_unit: 'Temperature Unit'
hours_to_show: 'Graph History'
};
return labels[schema.name] || schema.name;
}
@ -100,7 +89,6 @@ class AirQualityCard extends HTMLElement {
this._config = {
name: 'Air Quality',
hours_to_show: 24,
temperature_unit: 'F',
...config
};
this._rendered = false;
@ -121,8 +109,6 @@ class AirQualityCard extends HTMLElement {
let size = 3; // Base size for header and recommendation
if (this._config.co2_entity) size += 1;
if (this._config.pm25_entity) size += 1;
if (this._config.hcho_entity) size += 1;
if (this._config.tvoc_entity) size += 1;
if (this._config.humidity_entity) size += 1;
if (this._config.temperature_entity) size += 1;
return size;
@ -146,14 +132,6 @@ class AirQualityCard extends HTMLElement {
promises.push(this._fetchHistory(this._config.pm25_entity, startTime, endTime));
keys.push('pm25');
}
if (this._config.hcho_entity) {
promises.push(this._fetchHistory(this._config.hcho_entity, startTime, endTime));
keys.push('hcho');
}
if (this._config.tvoc_entity) {
promises.push(this._fetchHistory(this._config.tvoc_entity, startTime, endTime));
keys.push('voc');
}
if (this._config.humidity_entity) {
promises.push(this._fetchHistory(this._config.humidity_entity, startTime, endTime));
keys.push('humidity');
@ -218,22 +196,6 @@ class AirQualityCard extends HTMLElement {
return '#f44336';
}
_getHCHOColor(value) {
if (value < 20) return '#4caf50';
if (value < 50) return '#8bc34a';
if (value < 100) return '#ffc107';
if (value < 200) return '#ff9800';
return '#f44336';
}
_getTVOCColor(value) {
if (value < 100) return '#4caf50';
if (value < 300) return '#8bc34a';
if (value < 500) return '#ffc107';
if (value < 1000) return '#ff9800';
return '#f44336';
}
_getHumidityColor(value) {
if (value < 30) return '#ff9800';
if (value < 40) return '#8bc34a';
@ -242,22 +204,7 @@ class AirQualityCard extends HTMLElement {
return '#ff9800';
}
_isCelsius() {
return this._config.temperature_unit === 'C';
}
_getTempUnit() {
return this._isCelsius() ? '°C' : '°F';
}
_getTempColor(value) {
if (this._isCelsius()) {
if (value < 18) return '#2196f3';
if (value < 20) return '#03a9f4';
if (value < 22) return '#4caf50';
if (value < 24) return '#ff9800';
return '#f44336';
}
if (value < 65) return '#2196f3';
if (value < 68) return '#03a9f4';
if (value < 72) return '#4caf50';
@ -337,8 +284,6 @@ class AirQualityCard extends HTMLElement {
_initialRender() {
const showCO2 = !!this._config.co2_entity;
const showPM25 = !!this._config.pm25_entity;
const showHCHO = !!this._config.hcho_entity;
const showTVOC = !!this._config.tvoc_entity;
const showHumidity = !!this._config.humidity_entity;
const showTemp = !!this._config.temperature_entity;
@ -601,45 +546,6 @@ class AirQualityCard extends HTMLElement {
<div class="graph-time-axis" id="pm25-time-axis"></div>
</div>
` : ''}
${showHCHO ? `
<div class="graph-container" id="hcho-graph-container" data-entity="${this._config.hcho_entity}">
<div class="graph-header">
<span class="graph-label">HCHO / CH₂O</span>
<span class="graph-value" id="hcho-value">-- <span class="unit">ppm</span><span class="status" id="hcho-status"></span></span>
</div>
<div class="graph-wrapper">
<div class="graph" id="hcho-graph">
<svg id="hcho-svg" viewBox="0 0 300 50" preserveAspectRatio="none"></svg>
</div>
<div class="graph-cursor" id="hcho-cursor"></div>
<div class="graph-tooltip" id="hcho-tooltip">
<div class="graph-tooltip-value"></div>
<div class="graph-tooltip-time"></div>
</div>
</div>
<div class="graph-time-axis" id="hcho-time-axis"></div>
</div>
` : ''}
${showTVOC ? `
<div class="graph-container" id="tvoc-graph-container" data-entity="${this._config.tvoc_entity}">
<div class="graph-header">
<span class="graph-label">tVOC</span>
<span class="graph-value" id="tvoc-value">-- <span class="unit">μg/</span><span class="status" id="tvoc-status"></span></span>
</div>
<div class="graph-wrapper">
<div class="graph" id="tvoc-graph">
<svg id="tvoc-svg" viewBox="0 0 300 50" preserveAspectRatio="none"></svg>
</div>
<div class="graph-cursor" id="tvoc-cursor"></div>
<div class="graph-tooltip" id="tvoc-tooltip">
<div class="graph-tooltip-value"></div>
<div class="graph-tooltip-time"></div>
</div>
</div>
<div class="graph-time-axis" id="tvoc-time-axis"></div>
</div>
` : ''}
${showHumidity ? `
<div class="graph-container" id="humidity-graph-container" data-entity="${this._config.humidity_entity}">
@ -665,7 +571,7 @@ class AirQualityCard extends HTMLElement {
<div class="graph-container" id="temperature-graph-container" data-entity="${this._config.temperature_entity}">
<div class="graph-header">
<span class="graph-label">Temperature</span>
<span class="graph-value" id="temperature-value">-- <span class="unit">${this._getTempUnit()}</span><span class="status" id="temperature-status"></span></span>
<span class="graph-value" id="temperature-value">-- <span class="unit">°F</span><span class="status" id="temperature-status"></span></span>
</div>
<div class="graph-wrapper">
<div class="graph" id="temperature-graph">
@ -691,8 +597,6 @@ class AirQualityCard extends HTMLElement {
const co2 = this._config.co2_entity ? this._getNumericState(this._config.co2_entity) : null;
const pm25 = this._config.pm25_entity ? this._getNumericState(this._config.pm25_entity) : null;
const hcho = this._config.hcho_entity ? this._getNumericState(this._config.hcho_entity) : null;
const tvoc = this._config.tvoc_entity ? this._getNumericState(this._config.tvoc_entity) : null;
const humidity = this._config.humidity_entity ? this._getNumericState(this._config.humidity_entity) : null;
const temp = this._config.temperature_entity ? this._getNumericState(this._config.temperature_entity) : null;
const recommendation = this._getRecommendation();
@ -778,34 +682,6 @@ class AirQualityCard extends HTMLElement {
}
}
// Update HCHO
if (hcho !== null) {
const hchoColor = this._getHCHOColor(hcho);
const hchoValueEl = this.shadowRoot.getElementById('hcho-value');
if (hchoValueEl) {
hchoValueEl.innerHTML = `${hcho.toFixed(1)} <span class="unit">ppb</span><span class="status" id="hcho-status"></span>`;
const statusEl = hchoValueEl.querySelector('.status');
statusEl.textContent = hcho < 20 ? 'Excellent' : hcho < 50 ? 'Good' : hcho < 100 ? 'Moderate' : hcho < 200 ? 'Elevated' : 'Poor';
statusEl.style.background = hchoColor + '22';
statusEl.style.color = hchoColor;
hchoValueEl.style.color = hchoColor;
}
}
// Update tVOC
if (tvoc !== null) {
const tvocColor = this._getTVOCColor(tvoc);
const tvocValueEl = this.shadowRoot.getElementById('tvoc-value');
if (tvocValueEl) {
tvocValueEl.innerHTML = `${tvoc.toFixed(1)} <span class="unit">ppb</span><span class="status" id="tvoc-status"></span>`;
const statusEl = tvocValueEl.querySelector('.status');
statusEl.textContent = tvoc < 100 ? 'Excellent' : tvoc < 300 ? 'Good' : tvoc < 500 ? 'Moderate' : tvoc < 1000 ? 'Elevated' : 'Poor';
statusEl.style.background = tvocColor + '22';
statusEl.style.color = tvocColor;
tvocValueEl.style.color = tvocColor;
}
}
// Update Humidity
if (humidity !== null) {
const humidityColor = this._getHumidityColor(humidity);
@ -828,23 +704,15 @@ class AirQualityCard extends HTMLElement {
// Update Temperature
if (temp !== null) {
const tempColor = this._getTempColor(temp);
const tempUnit = this._getTempUnit();
const tempValueEl = this.shadowRoot.getElementById('temperature-value');
if (tempValueEl) {
tempValueEl.innerHTML = `${Math.round(temp)} <span class="unit">${tempUnit}</span><span class="status" id="temperature-status"></span>`;
tempValueEl.innerHTML = `${Math.round(temp)} <span class="unit">°F</span><span class="status" id="temperature-status"></span>`;
const statusEl = tempValueEl.querySelector('.status');
let tempStatus = 'Comfortable';
if (this._isCelsius()) {
if (temp < 18) tempStatus = 'Cold';
else if (temp < 20) tempStatus = 'Cool';
else if (temp > 24) tempStatus = 'Hot';
else if (temp > 22) tempStatus = 'Warm';
} else {
if (temp < 65) tempStatus = 'Cold';
else if (temp < 68) tempStatus = 'Cool';
else if (temp > 76) tempStatus = 'Hot';
else if (temp > 72) tempStatus = 'Warm';
}
if (temp < 65) tempStatus = 'Cold';
else if (temp < 68) tempStatus = 'Cool';
else if (temp > 76) tempStatus = 'Hot';
else if (temp > 72) tempStatus = 'Warm';
statusEl.textContent = tempStatus;
statusEl.style.background = tempColor + '22';
statusEl.style.color = tempColor;
@ -862,20 +730,11 @@ class AirQualityCard extends HTMLElement {
if (this._config.pm25_entity && this._history.pm25.length) {
this._renderGraph('pm25', this._history.pm25, this._getPM25Color.bind(this), 0, 60, 'μg/m³');
}
if (this._config.hcho_entity && this._history.hcho.length) {
this._renderGraph('hcho', this._history.hcho, this._getHCHOColor.bind(this), 0, 60, 'ppb');
}
if (this._config.tvoc_entity && this._history.tvoc.length) {
this._renderGraph('tvoc', this._history.tvoc, this._getTVOCColor.bind(this), 0, 60, 'ppb');
}
if (this._config.humidity_entity && this._history.humidity.length) {
this._renderGraph('humidity', this._history.humidity, this._getHumidityColor.bind(this), 0, 100, '%');
}
if (this._config.temperature_entity && this._history.temperature.length) {
const tempUnit = this._getTempUnit();
const tempMin = this._isCelsius() ? 10 : 50;
const tempMax = this._isCelsius() ? 32 : 90;
this._renderGraph('temperature', this._history.temperature, this._getTempColor.bind(this), tempMin, tempMax, tempUnit);
this._renderGraph('temperature', this._history.temperature, this._getTempColor.bind(this), 50, 90, '°F');
}
this._setupGraphInteractions();
@ -1056,7 +915,7 @@ class AirQualityCard extends HTMLElement {
if (valueEl) {
let displayValue;
if (data.unit === 'ppm') displayValue = Math.round(closest.value);
else if (data.unit === '%' || data.unit === '°F' || data.unit === '°C') displayValue = Math.round(closest.value);
else if (data.unit === '%' || data.unit === '°F') displayValue = Math.round(closest.value);
else displayValue = closest.value.toFixed(1);
valueEl.textContent = `${displayValue} ${data.unit}`;
valueEl.style.color = closest.color;
@ -1116,7 +975,6 @@ if (LitElement && !customElements.get('air-quality-card-editor')) {
this._config = {
name: 'Air Quality',
hours_to_show: 24,
temperature_unit: 'F',
...config
};
}
@ -1126,14 +984,11 @@ if (LitElement && !customElements.get('air-quality-card-editor')) {
name: 'Card Name',
co2_entity: 'CO₂ Sensor',
pm25_entity: 'PM2.5 Sensor',
hcho_entity: 'HCHO Sensor (optional)',
tvoc_entity: 'tVOC Sensor (optional)',
humidity_entity: 'Humidity Sensor (optional)',
temperature_entity: 'Temperature Sensor (optional)',
air_quality_entity: 'Air Quality Index (optional)',
recommendation_entity: 'Recommendation Sensor (optional)',
hours_to_show: 'Graph History (hours)',
temperature_unit: 'Temperature Unit'
hours_to_show: 'Graph History (hours)'
};
return labels[schema.name] || schema.name;
}
@ -1143,14 +998,11 @@ if (LitElement && !customElements.get('air-quality-card-editor')) {
{ name: 'name', selector: { text: {} } },
{ name: 'co2_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'pm25_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'hcho_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'tvoc_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'humidity_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'temperature_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'air_quality_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'recommendation_entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'hours_to_show', selector: { number: { min: 1, max: 168, mode: 'box' } } },
{ name: 'temperature_unit', selector: { select: { options: [{ value: 'F', label: 'Fahrenheit (°F)' }, { value: 'C', label: 'Celsius (°C)' }], mode: 'dropdown' } } }
{ name: 'hours_to_show', selector: { number: { min: 1, max: 168, mode: 'box' } } }
];
}