diff --git a/lib/bme280/bme280/device.py b/lib/bme280/bme280/device.py index 7e12e834..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.""" @@ -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) @@ -250,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 @@ -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 @@ -348,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 bce19124..b39b2cea 100644 --- a/tests/scenarios/bme280.yaml +++ b/tests/scenarios/bme280.yaml @@ -471,3 +471,113 @@ 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, _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 + 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: "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: | + 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 + 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]