""" Waveshare UPS HAT (D) battery monitor plug-in — INA219 at I2C 0x43. Reads a single 21700 Li-ion cell via INA219. Reports voltage, current, power, battery percent, and charge state. Requires: pip install smbus2 Config example: - type: waveshare_ups_d name: "battery" enabled: true auto_install_packages: false settings: i2c_address: 0x43 # Waveshare UPS HAT (D) fixed address bus_number: 1 # I2C bus number (1 for Raspberry Pi default) """ from __future__ import annotations import time from typing import Any, Dict, Optional from .base import SensorBase from .registry import SensorRegistry # INA219 register addresses _REG_CONFIG = 0x00 _REG_SHUNT = 0x01 _REG_BUS = 0x02 _REG_POWER = 0x03 _REG_CURRENT = 0x04 _REG_CAL = 0x05 # 32V range, ±320mV gain, 128-sample averaging, continuous shunt+bus _CONFIG_VALUE = 0x3FFF # Waveshare UPS HAT (D) calibration — 0.01Ω shunt (per Waveshare sample code) # current_lsb ≈ 0.1524 mA/LSB, power_lsb = current_lsb × 20 _CAL_VALUE = 26868 _CURRENT_LSB = 0.0001524 # A per LSB _POWER_LSB = _CURRENT_LSB * 20.0 # W per LSB def _voltage_to_percent(v: float) -> int: """Piecewise linear SoC estimate for a single 21700 Li-ion cell (3.0–4.2 V).""" if v >= 4.20: return 100 if v >= 4.00: return int(85 + (v - 4.00) / 0.20 * 15) if v >= 3.80: return int(60 + (v - 3.80) / 0.20 * 25) if v >= 3.70: return int(40 + (v - 3.70) / 0.10 * 20) if v >= 3.50: return int(15 + (v - 3.50) / 0.20 * 25) if v >= 3.00: return int((v - 3.00) / 0.50 * 15) return 0 @SensorRegistry.register("waveshare_ups_d") class WaveshareUpsDSensor(SensorBase): sensor_type = "waveshare_ups_d" 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.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: self._write(bus, _REG_CONFIG, _CONFIG_VALUE) self._write(bus, _REG_CAL, _CAL_VALUE) # 128-sample averaging takes ~68 ms; wait for first conversion time.sleep(0.15) finally: bus.close() self.available = True self.log.info( "Waveshare UPS HAT (D) INA219 initialized (addr=0x%02X, bus=%d)", self.i2c_address, self.bus_number, ) except Exception as exc: self.log.warning( "Waveshare UPS HAT (D) init failed (addr=0x%02X, bus=%d): %s", self.i2c_address, self.bus_number, exc, ) def _write(self, bus, reg: int, val: int) -> None: bus.write_i2c_block_data(self.i2c_address, reg, [(val >> 8) & 0xFF, val & 0xFF]) def _read_u(self, bus, reg: int) -> int: d = bus.read_i2c_block_data(self.i2c_address, reg, 2) return (d[0] << 8) | d[1] def _read_s(self, bus, reg: int) -> int: v = self._read_u(bus, reg) return v - 0x10000 if v & 0x8000 else v def _read(self) -> Dict[str, Any]: """Read voltage, current, power, and derived battery state.""" if not self.available: raise RuntimeError("Waveshare UPS HAT (D) not available") try: bus = self._smbus2.SMBus(self.bus_number) try: # Re-apply calibration in case of external reset self._write(bus, _REG_CAL, _CAL_VALUE) bus_v = (self._read_u(bus, _REG_BUS) >> 3) * 4 / 1000.0 shunt_mv = self._read_s(bus, _REG_SHUNT) * 0.01 current_ma = self._read_s(bus, _REG_CURRENT) * _CURRENT_LSB * 1000.0 power_mw = self._read_u(bus, _REG_POWER) * _POWER_LSB * 1000.0 finally: bus.close() pct = _voltage_to_percent(bus_v) # HAT (D) sign convention: negative = charging, positive = discharging if current_ma < -50: state = "charging" elif current_ma > 50: state = "discharging" else: state = "idle" return { "bus_voltage_v": round(bus_v, 3), "shunt_voltage_mv": round(shunt_mv, 2), "current_ma": round(current_ma, 1), "power_mw": round(power_mw, 1), "battery_percent": pct, "charge_state": state, } except Exception as exc: raise RuntimeError(f"Waveshare UPS HAT (D) read failed: {exc}") from exc