Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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:
Expand Down
28 changes: 27 additions & 1 deletion air-quality-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1113,6 +1135,8 @@ class AirQualityCard extends HTMLElement {
</div>
</ha-card>
`;

this._applyMetricOrder();
}

_updateStates() {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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' } } },
Expand Down
51 changes: 51 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================
Expand Down
Loading