From 844810256bb876a0eb1467d5202c0e946d367fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Mon, 30 Mar 2026 12:14:32 +0200 Subject: [PATCH 1/3] feat(bme280): Add measurement time estimation from oversampling config. --- lib/bme280/README.md | 18 ++++++++++++++++- lib/bme280/bme280/device.py | 20 +++++++++++++++++++ tests/scenarios/bme280.yaml | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/lib/bme280/README.md b/lib/bme280/README.md index 9432bb9e..60b85202 100644 --- a/lib/bme280/README.md +++ b/lib/bme280/README.md @@ -196,6 +196,22 @@ Returns the dew point temperature in **degrees Celsius**, computed from the curr --- +### Measurement Time + +```python +ms = sensor.measurement_time_ms() +``` + +Returns the maximum measurement time in **milliseconds** based on the current oversampling settings (datasheet section 9.1). Useful for sleeping instead of polling in forced mode: + +```python +sensor.trigger_one_shot() +sleep_ms(int(sensor.measurement_time_ms()) + 1) +temperature, pressure, humidity = sensor.read() +``` + +--- + ## Data-Ready Status ```python @@ -331,7 +347,7 @@ Performs a soft reset, re-reads calibration data, and re-applies default configu | read_one_shot | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | set_continuous | ✅ | ❌ | ⚠️ Via mode | ❌ | ❌ | ❌ | | Integer compensation | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | -| Measurement time estimate | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| Measurement time estimate | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | | Multi-unit temperature | ❌ | ❌ | ❌ | ✅ C/F/K | ❌ | ❌ | | BMP280 compatibility | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | | Dedicated exceptions | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index c2ef8bab..5feaf856 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -212,6 +212,26 @@ def set_standby(self, standby): config = (config & ~(0x07 << STANDBY_SHIFT)) | (standby << STANDBY_SHIFT) self._write_reg(REG_CONFIG, config) + def measurement_time_ms(self): + """Return the maximum measurement time in milliseconds. + + Computed from the current oversampling settings using the formula + from the BME280 datasheet (section 9.1). Useful for sleeping + instead of polling in forced mode. + """ + ctrl_meas = self._read_reg(REG_CTRL_MEAS) + osrs_t = (ctrl_meas >> OSRS_T_SHIFT) & 0x07 + osrs_p = (ctrl_meas >> OSRS_P_SHIFT) & 0x07 + osrs_h = self._read_reg(REG_CTRL_HUM) & 0x07 + t_ms = 1.25 + if osrs_t: + t_ms += 2.3 * (1 << (osrs_t - 1)) + if osrs_p: + t_ms += 2.3 * (1 << (osrs_p - 1)) + 0.575 + if osrs_h: + t_ms += 2.3 * (1 << (osrs_h - 1)) + 0.575 + return t_ms + # -------------------------------------------------- # Status # -------------------------------------------------- diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index c849596a..d15ca69e 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -654,3 +654,42 @@ tests: result = abs(dp - expected) < 0.01 expect_true: true mode: [mock] + + # ----- Measurement time ----- + + - name: "measurement_time_ms with default x1 oversampling" + action: script + script: | + # Default: OSRS_X1 for all three → 1.25 + 2.3 + (2.3+0.575) + (2.3+0.575) = 9.3 ms + t = dev.measurement_time_ms() + result = abs(t - 9.3) < 0.01 + expect_true: true + mode: [mock] + + - name: "measurement_time_ms with weather station config" + action: script + script: | + from bme280.const import OSRS_X2, OSRS_X16 + dev.set_oversampling(temperature=OSRS_X2, pressure=OSRS_X16, humidity=OSRS_X2) + t = dev.measurement_time_ms() + # 1.25 + 2.3*2 + (2.3*16+0.575) + (2.3*2+0.575) = 48.4 ms + result = abs(t - 48.4) < 0.01 + # Restore default + from bme280.const import OSRS_X1 + dev.set_oversampling(temperature=OSRS_X1, pressure=OSRS_X1, humidity=OSRS_X1) + expect_true: true + mode: [mock] + + - name: "measurement_time_ms with all channels skipped" + action: script + script: | + from bme280.const import OSRS_SKIP + dev.set_oversampling(temperature=OSRS_SKIP, pressure=OSRS_SKIP, humidity=OSRS_SKIP) + t = dev.measurement_time_ms() + # Only base time: 1.25 ms + result = abs(t - 1.25) < 0.01 + # Restore default + from bme280.const import OSRS_X1 + dev.set_oversampling(temperature=OSRS_X1, pressure=OSRS_X1, humidity=OSRS_X1) + expect_true: true + mode: [mock] From c7447bb1c11b9afb9535fbef95d0d8270bfe2883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Mon, 30 Mar 2026 12:20:21 +0200 Subject: [PATCH 2/3] fix(bme280): Fix README measurement time example to avoid double-trigger. --- lib/bme280/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/bme280/README.md b/lib/bme280/README.md index 60b85202..380d69b6 100644 --- a/lib/bme280/README.md +++ b/lib/bme280/README.md @@ -202,14 +202,14 @@ Returns the dew point temperature in **degrees Celsius**, computed from the curr ms = sensor.measurement_time_ms() ``` -Returns the maximum measurement time in **milliseconds** based on the current oversampling settings (datasheet section 9.1). Useful for sleeping instead of polling in forced mode: +Returns the maximum measurement time in **milliseconds** based on the current oversampling settings (datasheet section 9.1). Useful for estimating how long a forced measurement takes: ```python -sensor.trigger_one_shot() -sleep_ms(int(sensor.measurement_time_ms()) + 1) -temperature, pressure, humidity = sensor.read() +print("Measurement takes up to", sensor.measurement_time_ms(), "ms") ``` +Note: in practice, prefer `read_one_shot()` which handles triggering and waiting automatically. + --- ## Data-Ready Status From 45f83a3894b317be31469764a612bae2476f277d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Mon, 30 Mar 2026 12:28:35 +0200 Subject: [PATCH 3/3] fix(bme280): Return int ceiling from measurement_time_ms and derive timeout. --- lib/bme280/README.md | 2 +- lib/bme280/bme280/device.py | 18 ++++++++++++------ tests/scenarios/bme280.yaml | 27 ++++++++++++++++++++------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/bme280/README.md b/lib/bme280/README.md index 380d69b6..40d45d20 100644 --- a/lib/bme280/README.md +++ b/lib/bme280/README.md @@ -202,7 +202,7 @@ Returns the dew point temperature in **degrees Celsius**, computed from the curr ms = sensor.measurement_time_ms() ``` -Returns the maximum measurement time in **milliseconds** based on the current oversampling settings (datasheet section 9.1). Useful for estimating how long a forced measurement takes: +Returns the maximum measurement time in **milliseconds** (integer, rounded up) based on the current oversampling settings (datasheet section 9.1). The result can be passed directly to `sleep_ms()`. Useful for estimating how long a forced measurement takes: ```python print("Measurement takes up to", sensor.measurement_time_ms(), "ms") diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index 5feaf856..02189060 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -213,11 +213,11 @@ def set_standby(self, standby): self._write_reg(REG_CONFIG, config) def measurement_time_ms(self): - """Return the maximum measurement time in milliseconds. + """Return the maximum measurement time in milliseconds (int, rounded up). Computed from the current oversampling settings using the formula - from the BME280 datasheet (section 9.1). Useful for sleeping - instead of polling in forced mode. + from the BME280 datasheet (section 9.1). The result is always an + integer ceiling so it can be passed directly to ``sleep_ms()``. """ ctrl_meas = self._read_reg(REG_CTRL_MEAS) osrs_t = (ctrl_meas >> OSRS_T_SHIFT) & 0x07 @@ -230,7 +230,7 @@ def measurement_time_ms(self): t_ms += 2.3 * (1 << (osrs_p - 1)) + 0.575 if osrs_h: t_ms += 2.3 * (1 << (osrs_h - 1)) + 0.575 - return t_ms + return int(t_ms) + (1 if t_ms != int(t_ms) else 0) # -------------------------------------------------- # Status @@ -280,8 +280,14 @@ def trigger_one_shot(self): ctrl = self._read_reg(REG_CTRL_MEAS) self._write_reg(REG_CTRL_MEAS, (ctrl & ~MODE_MASK) | MODE_FORCED) - def _wait_measurement(self, timeout_ms=100): - """Wait for measurement to complete. Raises on timeout.""" + def _wait_measurement(self): + """Wait for measurement to complete. Raises on timeout. + + The timeout is derived from :meth:`measurement_time_ms` with a + 2x safety margin so that high-oversampling configurations (e.g. + x16/x16/x16 ≈ 113 ms) never hit a spurious timeout. + """ + timeout_ms = self.measurement_time_ms() * 2 + 10 for _ in range(timeout_ms // 5): if self.data_ready(): return diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index d15ca69e..5d542b77 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -657,12 +657,12 @@ tests: # ----- Measurement time ----- - - name: "measurement_time_ms with default x1 oversampling" + - name: "measurement_time_ms returns int ceiling with default x1" action: script script: | - # Default: OSRS_X1 for all three → 1.25 + 2.3 + (2.3+0.575) + (2.3+0.575) = 9.3 ms + # Default: OSRS_X1 for all three → ceil(9.3) = 10 ms t = dev.measurement_time_ms() - result = abs(t - 9.3) < 0.01 + result = t == 10 and isinstance(t, int) expect_true: true mode: [mock] @@ -672,8 +672,8 @@ tests: from bme280.const import OSRS_X2, OSRS_X16 dev.set_oversampling(temperature=OSRS_X2, pressure=OSRS_X16, humidity=OSRS_X2) t = dev.measurement_time_ms() - # 1.25 + 2.3*2 + (2.3*16+0.575) + (2.3*2+0.575) = 48.4 ms - result = abs(t - 48.4) < 0.01 + # ceil(1.25 + 2.3*2 + (2.3*16+0.575) + (2.3*2+0.575)) = ceil(48.4) = 49 ms + result = t == 49 # Restore default from bme280.const import OSRS_X1 dev.set_oversampling(temperature=OSRS_X1, pressure=OSRS_X1, humidity=OSRS_X1) @@ -686,10 +686,23 @@ tests: from bme280.const import OSRS_SKIP dev.set_oversampling(temperature=OSRS_SKIP, pressure=OSRS_SKIP, humidity=OSRS_SKIP) t = dev.measurement_time_ms() - # Only base time: 1.25 ms - result = abs(t - 1.25) < 0.01 + # ceil(1.25) = 2 ms + result = t == 2 # Restore default from bme280.const import OSRS_X1 dev.set_oversampling(temperature=OSRS_X1, pressure=OSRS_X1, humidity=OSRS_X1) expect_true: true mode: [mock] + + - name: "measurement_time_ms handles x16 on all channels" + action: script + script: | + from bme280.const import OSRS_X16 + dev.set_oversampling(temperature=OSRS_X16, pressure=OSRS_X16, humidity=OSRS_X16) + t = dev.measurement_time_ms() + # ceil(1.25 + 2.3*16 + (2.3*16+0.575) + (2.3*16+0.575)) = ceil(112.8) = 113 ms + result = t == 113 + from bme280.const import OSRS_X1 + dev.set_oversampling(temperature=OSRS_X1, pressure=OSRS_X1, humidity=OSRS_X1) + expect_true: true + mode: [mock]