From 2416ac36247f3017928c636dabd7b5b112736d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 13:58:47 +0200 Subject: [PATCH 1/3] feat(bme280): Implement _ensure_data() auto-trigger pattern. --- lib/bme280/bme280/device.py | 43 +++++++++++++++++++++--- tests/scenarios/bme280.yaml | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index 7e12e834..89aac97f 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -239,6 +239,21 @@ def humidity_ready(self): # Forced measurement trigger # -------------------------------------------------- + def _is_sleep_mode(self): + """Return True if the sensor is in sleep mode.""" + return (self._read_reg(REG_CTRL_MEAS) & MODE_MASK) == MODE_SLEEP + + def _ensure_data(self): + """Trigger a forced measurement if the sensor is in sleep mode. + + In normal mode this is a no-op. In sleep mode it triggers a + single conversion and waits for completion so that subsequent + register reads return fresh data. + """ + if self._is_sleep_mode(): + self.trigger_one_shot() + self._wait_measurement() + def trigger_one_shot(self): """Trigger a single forced measurement. Poll data_ready() for completion.""" ctrl = self._read_reg(REG_CTRL_MEAS) @@ -323,24 +338,44 @@ def _compensate_humidity(self, raw_hum): # -------------------------------------------------- def temperature(self): - """Return compensated temperature in °C (float).""" + """Return compensated temperature in °C (float). + + If the sensor is in sleep mode, a forced measurement is triggered + automatically before reading. + """ + self._ensure_data() raw_temp, _, _ = self._read_raw() return self._compensate_temperature(raw_temp) / 100.0 def pressure_hpa(self): - """Return compensated pressure in hPa (float).""" + """Return compensated pressure in hPa (float). + + If the sensor is in sleep mode, a forced measurement is triggered + automatically before reading. + """ + self._ensure_data() raw_temp, raw_press, _ = self._read_raw() self._compensate_temperature(raw_temp) return self._compensate_pressure(raw_press) / 25600.0 def humidity(self): - """Return compensated relative humidity in %RH (float).""" + """Return compensated relative humidity in %RH (float). + + If the sensor is in sleep mode, a forced measurement is triggered + automatically before reading. + """ + self._ensure_data() raw_temp, _, raw_hum = self._read_raw() self._compensate_temperature(raw_temp) return self._compensate_humidity(raw_hum) / 1024.0 def read(self): - """Return (temperature_c, pressure_hpa, humidity_rh) tuple.""" + """Return (temperature_c, pressure_hpa, humidity_rh) tuple. + + If the sensor is in sleep mode, a forced measurement is triggered + automatically before reading. + """ + self._ensure_data() raw_temp, raw_press, raw_hum = self._read_raw() temp_c = self._compensate_temperature(raw_temp) / 100.0 press_hpa = self._compensate_pressure(raw_press) / 25600.0 diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index bce19124..e03e98a9 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -471,3 +471,69 @@ tests: prompt: "Do the values look reasonable?" expect_true: true mode: [hardware] + + # ----- Auto-trigger (_ensure_data) ----- + + - name: "_is_sleep_mode returns True after init" + action: script + script: | + result = dev._is_sleep_mode() + expect_true: true + mode: [mock] + + - name: "_is_sleep_mode returns False in normal mode" + action: script + script: | + dev.power_on() + result = not dev._is_sleep_mode() + expect_true: true + mode: [mock] + + - name: "_ensure_data triggers forced mode in sleep" + action: script + script: | + dev.power_off() + i2c.clear_write_log() + dev._ensure_data() + log = i2c.get_write_log() + triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) + result = triggered + expect_true: true + mode: [mock] + + - name: "_ensure_data is no-op in normal mode" + action: script + script: | + dev.power_on() + i2c.clear_write_log() + dev._ensure_data() + log = i2c.get_write_log() + # In normal mode, only a read of ctrl_meas (no write to 0xF4) + wrote_forced = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) + result = not wrote_forced + expect_true: true + mode: [mock] + + - name: "temperature() auto-triggers in sleep mode" + action: script + script: | + dev.power_off() + i2c.clear_write_log() + t = dev.temperature() + log = i2c.get_write_log() + triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) + result = triggered and abs(t - 25.08) < 0.1 + expect_true: true + mode: [mock] + + - name: "read() auto-triggers in sleep mode" + action: script + script: | + dev.power_off() + i2c.clear_write_log() + t, p, h = dev.read() + log = i2c.get_write_log() + triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) + result = triggered and abs(t - 25.08) < 0.1 + expect_true: true + mode: [mock] From d39f46791c87976efbe84617ab82faca41316d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 20:47:34 +0200 Subject: [PATCH 2/3] fix(bme280): Avoid double-trigger in read_one_shot and add missing tests. --- lib/bme280/bme280/device.py | 12 ++++++++-- tests/scenarios/bme280.yaml | 48 +++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index 89aac97f..3a0e5e06 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -383,7 +383,15 @@ def read(self): return temp_c, press_hpa, hum_rh def read_one_shot(self): - """Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh).""" + """Trigger a forced measurement, wait, and return (temp_c, press_hpa, hum_rh). + + Reads registers directly without calling _ensure_data() to avoid + a double trigger (forced mode returns the sensor to sleep). + """ self.trigger_one_shot() self._wait_measurement() - return self.read() + raw_temp, raw_press, raw_hum = self._read_raw() + temp_c = self._compensate_temperature(raw_temp) / 100.0 + press_hpa = self._compensate_pressure(raw_press) / 25600.0 + hum_rh = self._compensate_humidity(raw_hum) / 1024.0 + return temp_c, press_hpa, hum_rh diff --git a/tests/scenarios/bme280.yaml b/tests/scenarios/bme280.yaml index e03e98a9..b39b2cea 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -508,7 +508,7 @@ tests: i2c.clear_write_log() dev._ensure_data() log = i2c.get_write_log() - # In normal mode, only a read of ctrl_meas (no write to 0xF4) + # In normal mode, _ensure_data must not write to ctrl_meas (0xF4) wrote_forced = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) result = not wrote_forced expect_true: true @@ -526,6 +526,30 @@ tests: expect_true: true mode: [mock] + - name: "pressure_hpa() auto-triggers in sleep mode" + action: script + script: | + dev.power_off() + i2c.clear_write_log() + p = dev.pressure_hpa() + log = i2c.get_write_log() + triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) + result = triggered and abs(p - 1009.21) < 0.5 + expect_true: true + mode: [mock] + + - name: "humidity() auto-triggers in sleep mode" + action: script + script: | + dev.power_off() + i2c.clear_write_log() + h = dev.humidity() + log = i2c.get_write_log() + triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) + result = triggered and abs(h - 50.57) < 0.5 + expect_true: true + mode: [mock] + - name: "read() auto-triggers in sleep mode" action: script script: | @@ -534,6 +558,26 @@ tests: t, p, h = dev.read() log = i2c.get_write_log() triggered = any(reg == 0xF4 and (data[0] & 0x03) == 0x01 for reg, data in log) - result = triggered and abs(t - 25.08) < 0.1 + result = ( + triggered + and abs(t - 25.08) < 0.1 + and abs(p - 1009.21) < 0.5 + and abs(h - 50.57) < 0.5 + ) + expect_true: true + mode: [mock] + + - name: "read_one_shot does not double-trigger" + action: script + script: | + dev.power_off() + i2c.clear_write_log() + t, p, h = dev.read_one_shot() + log = i2c.get_write_log() + # Should only trigger forced mode once (not twice via _ensure_data) + forced_writes = [ + 1 for reg, data in log if reg == 0xF4 and (data[0] & 0x03) == 0x01 + ] + result = len(forced_writes) == 1 and abs(t - 25.08) < 0.1 expect_true: true mode: [mock] From 3e6fd4f9ed3992cb19adfd9f727436ed346fb184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20NEDJAR?= Date: Sun, 29 Mar 2026 21:05:18 +0200 Subject: [PATCH 3/3] fix(bme280): Use OSError for timeouts per convention established in #44. --- lib/bme280/bme280/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index 3a0e5e06..7ec28c83 100644 --- a/lib/bme280/bme280/device.py +++ b/lib/bme280/bme280/device.py @@ -30,7 +30,7 @@ STATUS_IM_UPDATE, STATUS_MEASURING, ) -from bme280.exceptions import BME280Error, BME280InvalidDevice, BME280NotFound +from bme280.exceptions import BME280InvalidDevice, BME280NotFound class BME280(object): @@ -125,7 +125,7 @@ def _wait_boot(self, timeout_ms=50): if not (self._read_reg(REG_STATUS) & STATUS_IM_UPDATE): return sleep_ms(5) - raise BME280Error("BME280 NVM copy timeout") + raise OSError("BME280 NVM copy timeout") def device_id(self): """Read chip ID register. Expected: 0x60.""" @@ -265,7 +265,7 @@ def _wait_measurement(self, timeout_ms=100): if self.data_ready(): return sleep_ms(5) - raise BME280Error("BME280 measurement timeout") + raise OSError("BME280 measurement timeout") # -------------------------------------------------- # Raw data burst read