""" Waveshare UPS HAT (E) battery monitor plug-in — BMS MCU at I2C 0x2D. The HAT (E) uses a dedicated BMS chip (not an INA219) that exposes charge state, pack voltage/current, per-cell voltages, remaining capacity in mAh, and time-to-empty / time-to-full estimates directly via I2C registers. Requires: pip install smbus2 Config example: - type: waveshare_ups_e name: "battery" enabled: true auto_install_packages: false settings: i2c_address: 0x2D # Waveshare UPS HAT (E) fixed address bus_number: 1 # I2C bus number (1 for Raspberry Pi default) low_cell_mv: 3150 # Per-cell warning threshold in mV """ from __future__ import annotations from typing import Any, Dict, Optional from .base import SensorBase from .registry import SensorRegistry # Register map _REG_STATUS = 0x02 # 1 byte — charge state flags _REG_VBUS = 0x10 # 6 bytes — input (VBUS) voltage, current, power _REG_BATT = 0x20 # 12 bytes — pack voltage, current, percent, mAh, time _REG_CELLS = 0x30 # 8 bytes — four cell voltages (LE uint16 each) # Charge state flag bits _FLAG_FAST_CHARGE = 0x40 _FLAG_CHARGING = 0x80 _FLAG_DISCHARGING = 0x20 def _u16le(data: list, offset: int) -> int: return data[offset] | (data[offset + 1] << 8) @SensorRegistry.register("waveshare_ups_e") class WaveshareUpsESensor(SensorBase): sensor_type = "waveshare_ups_e" def __init__(self, name: str, config: Optional[Dict[str, Any]] = None, log=None): super().__init__(name=name, config=config, log=log) addr = self.settings.get("i2c_address", 0x2D) self.i2c_address = int(addr, 0) if isinstance(addr, str) else int(addr) self.bus_number = int(self.settings.get("bus_number", 1)) self.low_cell_mv = int(self.settings.get("low_cell_mv", 3150)) self.available = False if not self.ensure_python_modules([("smbus2", "smbus2")]): return try: import smbus2 # type: ignore[import-not-found] self._smbus2 = smbus2 bus = smbus2.SMBus(self.bus_number) try: bus.read_i2c_block_data(self.i2c_address, _REG_STATUS, 1) finally: bus.close() self.available = True self.log.info( "Waveshare UPS HAT (E) initialized (addr=0x%02X, bus=%d)", self.i2c_address, self.bus_number, ) except Exception as exc: self.log.warning( "Waveshare UPS HAT (E) init failed (addr=0x%02X, bus=%d): %s", self.i2c_address, self.bus_number, exc, ) def _read(self) -> Dict[str, Any]: """Read all battery state from the HAT (E) BMS.""" if not self.available: raise RuntimeError("Waveshare UPS HAT (E) not available") try: bus = self._smbus2.SMBus(self.bus_number) try: status = bus.read_i2c_block_data(self.i2c_address, _REG_STATUS, 1)[0] vb = bus.read_i2c_block_data(self.i2c_address, _REG_VBUS, 6) bd = bus.read_i2c_block_data(self.i2c_address, _REG_BATT, 12) cd = bus.read_i2c_block_data(self.i2c_address, _REG_CELLS, 8) finally: bus.close() # Charge state if status & _FLAG_FAST_CHARGE: charge_state = "fast_charging" elif status & _FLAG_CHARGING: charge_state = "charging" elif status & _FLAG_DISCHARGING: charge_state = "discharging" else: charge_state = "idle" # VBUS (input power from charger) vbus_voltage_mv = _u16le(vb, 0) vbus_current_ma = _u16le(vb, 2) vbus_power_mw = _u16le(vb, 4) # Battery pack batt_voltage_mv = _u16le(bd, 0) batt_current_ma = _u16le(bd, 2) if batt_current_ma > 0x7FFF: # signed 16-bit batt_current_ma -= 0xFFFF batt_percent = _u16le(bd, 4) remaining_mah = _u16le(bd, 6) time_remaining = _u16le(bd, 8) time_to_full = _u16le(bd, 10) # Per-cell voltages (4 cells) cells_mv = [ _u16le(cd, 0), _u16le(cd, 2), _u16le(cd, 4), _u16le(cd, 6), ] result: Dict[str, Any] = { "charge_state": charge_state, "battery_voltage_mv": batt_voltage_mv, "battery_current_ma": batt_current_ma, "battery_percent": batt_percent, "remaining_capacity_mah": remaining_mah, "vbus_voltage_mv": vbus_voltage_mv, "vbus_current_ma": vbus_current_ma, "vbus_power_mw": vbus_power_mw, "cell_voltages_mv": cells_mv, "low_cell_warning": any(0 < v < self.low_cell_mv for v in cells_mv), } # Only include whichever time estimate is relevant if batt_current_ma < 0: result["time_to_empty_min"] = time_remaining else: result["time_to_full_min"] = time_to_full return result except Exception as exc: raise RuntimeError(f"Waveshare UPS HAT (E) read failed: {exc}") from exc