diff --git a/README.md b/README.md index 437a5bc..cfac396 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ outdoor_pm25_entity: sensor.outdoor_pm25 | `hours_to_show` | number | No | 24 | Hours of history to display (1-168) | | `temperature_unit` | string | No | "auto" | Temperature unit: "auto" (detect from HA), "F" (Fahrenheit), or "C" (Celsius) | | `radon_unit` | string | No | "auto" | Radon unit: "auto" (detect from sensor), "pCi/L" (US), or "Bq/m3" (International) | +| `order` | array | No | default | Custom display order for metrics (see [Sensor Order](#sensor-order)) | | `outdoor_co2_entity` | string | No | - | Outdoor CO2 sensor for comparison | | `outdoor_pm25_entity` | string | No | - | Outdoor PM2.5 sensor for comparison | | `outdoor_pm1_entity` | string | No | - | Outdoor PM1 sensor for comparison | @@ -113,6 +114,27 @@ outdoor_pm25_entity: sensor.outdoor_pm25 \* At least one sensor entity is required. Use any combination that fits your setup. +### Sensor Order + +Customize which sensors come first on the card. Provide a list of metric names — any metric you don't list keeps its default position and stays visible. + +Valid metric names: `co`, `radon`, `co2`, `pm25`, `pm10`, `pm1`, `pm03`, `pm4`, `hcho`, `tvoc`, `nox`, `humidity`, `temperature`. + +```yaml +type: custom:air-quality-card +co2_entity: sensor.air_quality_co2 +humidity_entity: sensor.air_quality_humidity +temperature_entity: sensor.air_quality_temp +pm10_entity: sensor.air_quality_pm10 +pm25_entity: sensor.air_quality_pm25 +order: + - temperature + - humidity + - co2 + - pm10 + - pm25 +``` + ### Outdoor Sensors Configure outdoor sensor entities to see a **dashed comparison line** on each graph showing outdoor conditions alongside indoor readings. When outdoor sensors are configured: diff --git a/air-quality-card.js b/air-quality-card.js index 4e62067..c51df92 100644 --- a/air-quality-card.js +++ b/air-quality-card.js @@ -66,6 +66,28 @@ class AirQualityCard extends HTMLElement { this._updateStates(); } + // Resolve the metric display order. User's `order` wins; anything they + // didn't list is appended in the default order so users never lose a + // configured metric by forgetting to mention it. + _getMetricOrder() { + const all = ['co', 'radon', 'co2', 'pm25', 'pm10', 'pm1', 'pm03', 'pm4', 'hcho', 'tvoc', 'nox', 'humidity', 'temperature']; + if (!Array.isArray(this._config.order) || !this._config.order.length) return all; + const valid = this._config.order.filter(m => all.includes(m)); + const remaining = all.filter(m => !valid.includes(m)); + return [...valid, ...remaining]; + } + + // Reorder graph cards via flexbox `order` rather than rebuilding the DOM — + // .graphs is already display:flex, so setting style.order on each container + // is enough to reflow them visually. + _applyMetricOrder() { + if (!Array.isArray(this._config.order) || !this._config.order.length) return; + this._getMetricOrder().forEach((metric, idx) => { + const container = this.shadowRoot.getElementById(`${metric}-graph-container`); + if (container) container.style.order = idx; + }); + } + getCardSize() { let size = 3; // Base size for header and recommendation if (this._config.co_entity) size += 1; @@ -1113,6 +1135,8 @@ class AirQualityCard extends HTMLElement { `; + + this._applyMetricOrder(); } _updateStates() { @@ -1887,7 +1911,8 @@ if (LitElement && !customElements.get('air-quality-card-editor')) { hours_to_show: 'Graph History', temperature_unit: 'Temperature Unit', radon_unit: 'Radon Unit', - tvoc_unit: 'tVOC Measurement Type' + tvoc_unit: 'tVOC Measurement Type', + order: 'Sensor Order (list of metric names)' }; return labels[schema.name] || schema.name; } @@ -2000,6 +2025,7 @@ if (LitElement && !customElements.get('air-quality-card-editor')) { schema: [ { name: 'air_quality_entity', selector: { entity: { domain: 'sensor' } } }, { name: 'hours_to_show', selector: { number: { min: 1, max: 168, mode: 'box', unit_of_measurement: 'hours' } } }, + { name: 'order', selector: { object: {} } }, { name: 'temperature_unit', selector: { select: { options: [{ value: 'auto', label: 'Auto (from HA)' }, { value: 'F', label: 'Fahrenheit (°F)' }, { value: 'C', label: 'Celsius (°C)' }], mode: 'dropdown' } } }, { name: 'radon_unit', selector: { select: { options: [{ value: 'auto', label: 'Auto (from sensor)' }, { value: 'pCi/L', label: 'pCi/L (US)' }, { value: 'Bq/m³', label: 'Bq/m³ (International)' }], mode: 'dropdown' } } }, { name: 'tvoc_unit', selector: { select: { options: [{ value: 'auto', label: 'Auto-detect' }, { value: 'ppb', label: 'Absolute (ppb)' }, { value: 'index', label: 'VOC Index (Sensirion)' }], mode: 'dropdown' } } }, diff --git a/test.js b/test.js index e33924f..d333aa6 100644 --- a/test.js +++ b/test.js @@ -492,6 +492,57 @@ try { } assert(configValid, 'radon_longterm_entity alone is valid config'); +// ============================================================ +// METRIC ORDERING (issue #19) +// ============================================================ + +section('Metric order — default'); + +const defaultOrderCard = new CardClass(); +defaultOrderCard.setConfig({ co2_entity: 'sensor.co2' }); +const defaultOrder = defaultOrderCard._getMetricOrder(); +assert(defaultOrder[0] === 'co', 'default order: co first'); +assert(defaultOrder[defaultOrder.length - 1] === 'temperature', 'default order: temperature last'); +assert(defaultOrder.length === 13, 'default order: all 13 metrics'); + +section('Metric order — user override'); + +const reorderedCard = new CardClass(); +reorderedCard.setConfig({ + co2_entity: 'sensor.co2', + order: ['temperature', 'humidity', 'co2', 'pm10', 'pm25'] +}); +const reordered = reorderedCard._getMetricOrder(); +assert(reordered[0] === 'temperature', 'user order: temperature first'); +assert(reordered[1] === 'humidity', 'user order: humidity second'); +assert(reordered[2] === 'co2', 'user order: co2 third'); +assert(reordered[3] === 'pm10', 'user order: pm10 fourth'); +assert(reordered[4] === 'pm25', 'user order: pm25 fifth'); +// Unmentioned metrics get appended in default order — user never loses a sensor +assert(reordered.includes('radon'), 'unmentioned metrics still present'); +assert(reordered.length === 13, 'user order: total still 13'); + +section('Metric order — invalid input'); + +const badOrderCard = new CardClass(); +badOrderCard.setConfig({ co2_entity: 'sensor.co2', order: 'not an array' }); +const fallback = badOrderCard._getMetricOrder(); +assert(fallback[0] === 'co', 'non-array order falls back to defaults'); + +const partialBadCard = new CardClass(); +partialBadCard.setConfig({ + co2_entity: 'sensor.co2', + order: ['temperature', 'invalid_metric', 'co2'] +}); +const filtered = partialBadCard._getMetricOrder(); +assert(filtered.indexOf('temperature') === 0, 'invalid metrics are dropped, valid ones preserved'); +assert(filtered.indexOf('co2') === 1, 'invalid entries skipped in order'); +assert(!filtered.includes('invalid_metric'), 'invalid metric never appears'); + +const emptyOrderCard = new CardClass(); +emptyOrderCard.setConfig({ co2_entity: 'sensor.co2', order: [] }); +assert(emptyOrderCard._getMetricOrder()[0] === 'co', 'empty array → default order'); + // ============================================================ // CARD SIZE TESTS // ============================================================