diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..3387332
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,18 @@
+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
diff --git a/air-quality-card.js b/air-quality-card.js
index 0203a5a..81b4ba0 100644
--- a/air-quality-card.js
+++ b/air-quality-card.js
@@ -21,6 +21,13 @@ 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: [
@@ -47,6 +54,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'
@@ -112,6 +121,8 @@ 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;
@@ -135,6 +146,14 @@ 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');
@@ -199,6 +218,22 @@ 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';
@@ -302,6 +337,8 @@ 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;
@@ -564,6 +601,45 @@ class AirQualityCard extends HTMLElement {
` : ''}
+ ${showHCHO ? `
+
+ ` : ''}
+
+ ${showTVOC ? `
+
+ ` : ''}
${showHumidity ? `
@@ -615,6 +691,8 @@ 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();
@@ -700,6 +778,34 @@ 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)} ppb`;
+ 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)} ppb`;
+ 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);
@@ -756,6 +862,12 @@ 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, '%');
}
@@ -1014,6 +1126,8 @@ 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)',
@@ -1029,6 +1143,8 @@ 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' } } },