""" ENS210 relative humidity and temperature sensor plug-in. Requires: pip install smbus2 Config example: - type: ens210 name: "ambient" enabled: true auto_install_packages: false settings: i2c_address: 0x43 # Default ENS210 I2C address bus_number: 1 # I2C bus number (1 for Raspberry Pi default) read_timeout_seconds: 1.0 # Max time to wait for valid data (polls every 50 ms) """ from __future__ import annotations import time from typing import Any, Dict, Optional from .base import SensorBase from .registry import SensorRegistry # ENS210 register addresses _REG_SENS_RUN = 0x21 _REG_SENS_START = 0x22 _REG_T_VAL = 0x30 _REG_H_VAL = 0x33 @SensorRegistry.register("ens210") class ENS210Sensor(SensorBase): sensor_type = "ens210" def __init__(self, name: str, config: Optional[Dict[str, Any]] = None, log=None): super().__init__(name=name, config=config, log=log) self.i2c_address = int(self.settings.get("i2c_address", 0x43)) self.bus_number = int(self.settings.get("bus_number", 1)) self._poll_interval = 0.05 # 50 ms between validity checks self._poll_attempts = max( 1, int(float(self.settings.get("read_timeout_seconds", 1.0)) / self._poll_interval) ) self.available = False if not self.ensure_python_modules( [ ("smbus2", "smbus2"), ] ): return try: import smbus2 # type: ignore[import-not-found] self._smbus2 = smbus2 # Verify the bus is accessible smbus2.SMBus(self.bus_number).close() self.available = True self.log.info( "ENS210 initialized (addr=0x%02X, bus=%d)", self.i2c_address, self.bus_number, ) except Exception as exc: self.log.warning( "ENS210 init failed (addr=0x%02X, bus=%d): %s", self.i2c_address, self.bus_number, exc, ) self.available = False def _read(self) -> Dict[str, Any]: """Read temperature and humidity from ENS210.""" if not self.available: raise RuntimeError("ENS210 device not available") bus = self._smbus2.SMBus(self.bus_number) try: bus.write_byte_data(self.i2c_address, _REG_SENS_RUN, 0x03) bus.write_byte_data(self.i2c_address, _REG_SENS_START, 0x03) for _ in range(self._poll_attempts): time.sleep(self._poll_interval) t_data = bus.read_i2c_block_data(self.i2c_address, _REG_T_VAL, 3) h_data = bus.read_i2c_block_data(self.i2c_address, _REG_H_VAL, 3) if (t_data[2] & 0x01) and (h_data[2] & 0x01): break else: raise RuntimeError( f"ENS210 measurement timed out after {self._poll_attempts * self._poll_interval:.1f}s" ) t_raw = t_data[0] | (t_data[1] << 8) h_raw = h_data[0] | (h_data[1] << 8) return { "temperature_c": round(t_raw / 64.0 - 273.15, 2), "humidity_pct": round(h_raw / 512.0, 2), } except RuntimeError: raise except Exception as exc: raise RuntimeError(f"ENS210 read failed: {exc}") from exc finally: bus.close()