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
13 changes: 13 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,16 @@ Use this sequence for changes with runtime impact:
4. Verify `GET /healthz`.
5. Confirm MQTT ingest, Nodus metadata, Home Assistant discovery, or farmOS
status as appropriate for the change.

Barometric pressure gauges use the altitude entered in General Settings
(`[Astral].ALTITUDE`, metres) to center raw/absolute pressure ranges on standard
atmospheric pressure at that elevation, with at least 50 hPa of weather margin
on either side. Metric bounds round outward to 10 hPa; Imperial bounds round
outward to 0.1 inHg. Refresh the dashboard after changing altitude. Blank or
invalid altitude retains the broad default range for raw pressure.

Sea-level-corrected readings (Ecowitt relative pressure, WeeWX barometer, and
BME/VPD/AQI sensors with altitude calibration) instead use a sea-level range:
960–1070 hPa or 28.4–31.4 inHg. Explicit absolute-pressure metrics always use
site altitude. Gauge scaling does not change measurements, history, MQTT, or
calibration values.
2 changes: 1 addition & 1 deletion sensorius/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
release notes, and supporting tooling can report a consistent build identity.
"""

__version__ = "v0.26.261.6"
__version__ = "v0.26.261.7"
9 changes: 6 additions & 3 deletions sensorius/saiHtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def normalize_dashboard_metric_set(value: object) -> str:
return "All" if compact in {"all", "showall"} else "Pick 6"


def render_dashboard(sensor_id, sensor, available, all_values, all_stats, mqtt_ingest, switch_controllers=None, sensor_locations=None, gauge_config=None, gauge_size="Small", expected_gauge_map=None, expected_display_style_map=None, display_style=None, astro_payload=None, biodynamic_payload=None, weather_forecast_provider="met_no", weather_forecast_theme="pollinator", dashboard_background_theme="leaf", dashboard_metric_set="Pick 6", dashboard_custom_theme_style=""):
def render_dashboard(sensor_id, sensor, available, all_values, all_stats, mqtt_ingest, switch_controllers=None, sensor_locations=None, gauge_config=None, gauge_size="Small", expected_gauge_map=None, expected_display_style_map=None, display_style=None, astro_payload=None, biodynamic_payload=None, weather_forecast_provider="met_no", weather_forecast_theme="pollinator", dashboard_background_theme="leaf", dashboard_metric_set="Pick 6", dashboard_custom_theme_style="", pressure_altitude=None, pressure_sensor_context=None):
"""Yield the complete Sensorius dashboard HTML document."""

import json
Expand Down Expand Up @@ -2324,6 +2324,9 @@ def _format_stat_value(stat_value):

yield "<script type='module'>"
yield "\"use strict\";"
yield f"import {{ pressureGaugeConfig }} from '/ui_static/js/pressure_gauges.js?v={APP_VERSION}';"
yield f"const pressureAltitude = {json.dumps(pressure_altitude)};"
yield f"const pressureSensorContext = {json.dumps(pressure_sensor_context or {})};"
yield f"import {{ startRainGauges }} from '/ui_static/js/rain_gauges.js?v={APP_VERSION}';"

yield "let stepCount = 0;"
Expand Down Expand Up @@ -4860,7 +4863,7 @@ def _format_stat_value(stat_value):
yield " const canvas = document.getElementById(canvasId);"
yield " const label = document.getElementById(labelId);"
yield " if (!canvas || !label) return;"
yield " const config = gaugeConfig?.[metric];"
yield " const config = pressureGaugeConfig(metric, gaugeConfig?.[metric], pressureAltitude, pressureSensorContext[sensor]);"
yield " if (!config) return;"
yield " let value = currentValues?.[sensor]?.[metric];"
yield " const isNull = (value == null);"
Expand Down Expand Up @@ -5397,7 +5400,7 @@ def _format_stat_value(stat_value):
yield " const val = vset[metric];"
yield " const labelEl = document.getElementById(`${safe}_val`);"
yield " const g = window[`${safe}_gauge`];"
yield " const metricConfig = gaugeConfig?.[metric] || {};"
yield " const metricConfig = pressureGaugeConfig(metric, gaugeConfig?.[metric], pressureAltitude, pressureSensorContext[sid]) || {};"
yield " const renderMode = ((metricConfig.render) || '').toLowerCase();"
yield " const valueMetric = metricConfig.value_metric || metric;"
yield " const statsMetric = metricConfig.stats_metric || metric;"
Expand Down
14 changes: 14 additions & 0 deletions sensorius/saiWebRoutes.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ def _get_cached_display_settings() -> dict[str, object]:
return dict(_DASHBOARD_DISPLAY_SETTINGS_CACHE[1])
fresh_settings = saiSettings(apply_live=False)
payload = {
"pressure_altitude": fresh_settings.get_setting("Astral", "ALTITUDE", ""),
"gauge_size": fresh_settings.get_setting("Display", "gauge_size") or "Small",
"display_style": fresh_settings.get_setting("Display", "display_style") or "Gauge",
"unit_system": normalize_display_unit_system(
Expand Down Expand Up @@ -3104,7 +3105,18 @@ def _is_weewx_dashboard_sensor(sid: str) -> bool:
sensor_mgr = sensor_settings_mgr or _get_sensor_settings_manager()
expected_gauge_map = {}
expected_display_style_map = {}
pressure_sensor_context = {}
for sid in all_values:
if sensor_mgr is not None:
try:
pressure_doc = await asyncio.to_thread(sensor_mgr.load, sid)
except FileNotFoundError:
pressure_doc = {}
pressure_sensor_context[sid] = {
"device": (pressure_doc.get("Sensor") or {}).get("DEVICE", ""),
"altitude": ((pressure_doc.get("Calibration") or {}).get("Device") or {}).get("ALTITUDE_METERS"),
"weewx": _is_weewx_dashboard_sensor(sid),
}
try:
configured_metrics = sensor_mgr.get_display_metrics(sid)
except Exception:
Expand Down Expand Up @@ -3595,6 +3607,8 @@ def _resolve_meas_status_for_sid(sid: str) -> str:
sensor_locations = sensor_locations,
gauge_config=gauge_config,
gauge_size = gaugeSize,
pressure_altitude=display_settings.get("pressure_altitude"),
pressure_sensor_context=pressure_sensor_context,
expected_gauge_map = expected_gauge_map,
expected_display_style_map = expected_display_style_map,
display_style = displayStyle,
Expand Down
20 changes: 20 additions & 0 deletions testApparatus/playwright_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,23 @@ def weather_climate() -> dict:
"start_date": "1991-01-01", "end_date": "2026-09-13", "averages": {
"temperature_c": 20, "humidity_pct": 55, "wind_kmh": 16.09344, "rain_mm": 2.54, "samples": 35,
}}


@app.get("/pressure-gauges", response_class=HTMLResponse)
def pressure_gauges(unit_system: str = "Metric", altitude: str = "1609.3") -> HTMLResponse:
"""Render raw and corrected pressure sensors together for browser checks."""
from sensorius.saiDisplayUnits import apply_display_units_to_gauge_config

values = {
"raw": {"Baro-Pressure": 835.0, "Plant Baro-Pressure": 836.0},
"corrected": {"Baro-Pressure": 1022.5},
"ecowitt": {"Gateway Baro-Pressure": 1032.5, "Gateway Absolute Baro-Pressure": 840.0},
}
return HTMLResponse("".join(render_dashboard(
"All", None, list(values), values, {}, SimpleNamespace(expected_gauge_map={}),
gauge_config=apply_display_units_to_gauge_config(get_gauge_config(), unit_system),
expected_gauge_map={sid: list(metrics) for sid, metrics in values.items()},
expected_display_style_map={sid: {} for sid in values}, display_style="Gauge",
pressure_altitude=altitude,
pressure_sensor_context={"corrected": {"device": "bme280", "altitude": 1609.3}},
)))
76 changes: 76 additions & 0 deletions tests/pressure-gauges.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { test, expect } from '@playwright/test';
import { pressureGaugeConfig } from '../ui_static/js/pressure_gauges.js';

const base = { unit: 'hPa', min: 700, max: 1100 };
test('pressure ranges cover altitude bounds, fallback and corrected readings', () => {
for (const altitude of ['', null, undefined, 'NaN', Infinity, -501, 10001]) {
expect(pressureGaugeConfig('Baro-Pressure', base, altitude)).toBe(base);
}
for (const altitude of [-500, 0, 1609.3, 10000]) {
const scale = pressureGaugeConfig('Pressure', base, altitude);
const center = 1013.25 * Math.pow(1 - altitude / 44330, 5.255);
expect(center - scale.min).toBeGreaterThanOrEqual(50);
expect(scale.max - center).toBeGreaterThanOrEqual(50);
expect(scale.ticks[0]).toBe(scale.min);
expect(scale.ticks.at(-1)).toBe(scale.max);
}
for (const device of ['bme280', 'bme680', 'bme688', 'vpd', 'avpd', 'apvpd', 'aqi', 'ecowitt', 'weewx']) {
const context = { device, altitude: 1609.3 };
expect(pressureGaugeConfig('Baro-Pressure', base, 1609.3, context).min).toBe(960);
expect(pressureGaugeConfig('Absolute Baro-Pressure', base, 1609.3, context).min).toBe(780);
}
expect(pressureGaugeConfig('Temperature', base, 1609.3)).toBe(base);
expect(base).toEqual({ unit: 'hPa', min: 700, max: 1100 });
});

for (const units of ['Metric', 'Imperial']) {
test(`mixed pressure sensors render altitude scales in ${units}`, async ({ page }, testInfo) => {
await page.route('https://cdn.jsdelivr.net/**', route => route.fulfill({ contentType: 'application/javascript', body: '' }));
await page.addInitScript(() => {
window.pressureTestGauges = [];
window.Gauge = class {
constructor(canvas) { this.id = canvas.id; window.pressureTestGauges.push(this); }
setOptions(options) { this.options = options; return this; }
setMinValue(value) { this.min = value; }
set(value) { this.value = value; }
render() {}
};
});
await page.goto(`/pressure-gauges?unit_system=${units}`, { waitUntil: 'domcontentloaded' });
await expect.poll(() => page.evaluate(() => window.pressureTestGauges.length)).toBe(5);
const gauges = await page.evaluate(() => window.pressureTestGauges.map(g => ({ id: g.id, min: g.min, max: g.maxValue, ticks: g.options.staticLabels.labels, value: g.value })));
for (const gauge of gauges) {
const corrected = gauge.id.startsWith('corrected_') || gauge.id === 'ecowitt_Gateway_Baro-PressureGauge';
const raw = gauge.id.startsWith('raw_') || gauge.id.includes('Absolute');
expect(corrected || raw).toBe(true);
expect([gauge.min, gauge.max]).toEqual(units === 'Metric' ? (corrected ? [960, 1070] : [780, 890]) : (corrected ? [28.4, 31.4] : [23.1, 26.2]));
expect(gauge.ticks[0]).toBe(gauge.min);
expect(gauge.ticks.at(-1)).toBe(gauge.max);
}
await expect(page.locator('#corrected_Baro-Pressure_val')).toContainText(units === 'Metric' ? '1022.5 hPa' : '30.19 inHg');
await page.screenshot({ path: testInfo.outputPath(`pressure-${units}.png`), fullPage: true });
});
}

test('fallback canvas keeps altitude bounds after a live update', async ({ page }, testInfo) => {
await page.route('https://cdn.jsdelivr.net/**', route => route.fulfill({ contentType: 'application/javascript', body: '' }));
await page.route('**/*json_only=true*', route => route.fulfill({ json: {
available: ['raw'], values: { raw: { 'Baro-Pressure': 845 } }, timestamps: { raw: '2026-09-18T12:00:00' }, stats: {},
} }));
await page.addInitScript(() => {
window.pressureLabels = [];
const fillText = CanvasRenderingContext2D.prototype.fillText;
CanvasRenderingContext2D.prototype.fillText = function(text, ...args) {
if (this.canvas.id === 'raw_Baro-PressureGauge') window.pressureLabels.push(String(text));
return fillText.call(this, text, ...args);
};
});
await page.goto('/pressure-gauges', { waitUntil: 'load' });
await page.evaluate(() => { window.pressureLabels = []; return window.updateGauges({ ignoreVisibility: true, ignoreModal: true }); });
await expect(page.locator('#raw_Baro-Pressure_val')).toContainText('845.0 hPa');
const labels = await page.evaluate(() => window.pressureLabels);
expect(labels).toContain('780');
expect(labels).toContain('890');
expect(labels).not.toContain('700');
await page.screenshot({ path: testInfo.outputPath('pressure-fallback.png'), fullPage: true });
});
31 changes: 31 additions & 0 deletions ui_static/js/pressure_gauges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** Display-only pressure ranges; stored measurements retain their original values. */
export function pressureGaugeConfig(metric, config, altitude, context = {}) {
if (!config || !(metric === 'Pressure' || metric === 'Baro-Pressure' || metric.endsWith(' Baro-Pressure'))) return config;
const device = String(context.device || '').trim().toLowerCase();
const calibrationAltitude = Number(context.altitude);
const corrected = !metric.includes('Absolute Baro-Pressure') && (
metric.startsWith('Gateway ') || context.weewx || device.includes('ecowitt') || device.includes('weewx') ||
(['aqi', 'bme680', 'bme688', 'vpd', 'avpd', 'bme280', 'apvpd'].includes(device) &&
Number.isFinite(calibrationAltitude) && calibrationAltitude !== 0)
);
let center = 1013.25;
if (!corrected) {
if (altitude == null || String(altitude).trim() === '') return config;
const meters = Number(altitude);
if (!Number.isFinite(meters) || meters < -500 || meters > 10000) return config;
center *= Math.pow(1 - meters / 44330, 5.255);
}
const imperial = config.unit === 'inHg';
const factor = imperial ? 0.0295299830714 : 1;
const rounding = imperial ? 0.1 : 10;
const step = imperial ? 0.5 : 20;
const clean = value => Number(value.toFixed(6));
const min = clean(Math.floor((center - 50) * factor / rounding) * rounding);
const max = clean(Math.ceil((center + 50) * factor / rounding) * rounding);
const ticks = [];
for (let tick = min; tick < max; tick = clean(tick + step)) {
if (tick === min || clean(max - tick) >= step) ticks.push(tick);
}
ticks.push(max);
return { ...config, min, max, ticks, zones: [{ min, max, strokeStyle: '#add8e6' }] };
}
Loading