Skip to content

Commit

Permalink
v0.3.5 (#14)
Browse files Browse the repository at this point in the history
- fix: sensor.hassio_ups_remaining_capacity mAh to Wh #12 
- feat: "addr" config parameter for device i2c address #6 
- feat: ina219 mock for dev env
- feat: readme update
  • Loading branch information
odya authored Jul 6, 2024
1 parent 6bcfe1e commit 26887c7
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 39 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
custom_components/ina219_ups_hat/__pycache__
custom_components/ina219_ups_hat/ina219/__pycache__/*
69 changes: 44 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ This integration allows you to monitor any INA219 based UPS hat (e.g. [Waveshare
</table>

## Installation

### HACS

If you use [HACS](https://hacs.xyz/) you can install and update this component.
1. Go into HACS -> CUSTOM REPOSITORIES and add url: https://github.com/odya/hass-ina219-ups-hat with type "integration"
2. Go to integration, search "ina219_ups_hat" and click *Install*.

1. Go into HACS -> CUSTOM REPOSITORIES and add url: <https://github.com/odya/hass-ina219-ups-hat> with type "integration"
2. Go to integration, search "ina219*ups_hat" and click \_Install*.

### Manual

Download and unzip or clone this repository and copy `custom_components/ina219_ups_hat/` to your configuration directory of Home Assistant, e.g. `~/.homeassistant/custom_components/`.

In the end your file structure should look like that:

```
~/.homeassistant/custom_components/ina219_ups_hat/__init__.py
~/.homeassistant/custom_components/ina219_ups_hat/manifest.json
Expand All @@ -31,56 +35,64 @@ In the end your file structure should look like that:
```

## Configuration

### Sensor
Create a new sensor entry in your `configuration.yaml`

Create a new sensor entry in your `configuration.yaml`

```yaml
sensor:
- platform: ina219_ups_hat
name: Hassio UPS # Optional
unique_id: hassio_ups # Optional
scan_interval: 60 # Optional
batteries_count: 3 # Optional
max_soc: 91 # Optional
battery_capacity: 9000 # Optional
sma_samples: 5 # Optional
min_online_current: -100 # Optional, mA
min_charging_current: 55 # Optional, mA
name: Hassio UPS # Optional
unique_id: hassio_ups # Optional
scan_interval: 60 # Optional
addr: 0x41 # Required
batteries_count: 3 # Optional
max_soc: 91 # Optional
battery_capacity: 9000 # Optional
sma_samples: 5 # Optional
min_online_current: -100 # Optional, mA
min_charging_current: 55 # Optional, mA
```
Following data can be read:
- SoC (State of Charge)
- Voltage
- Current
- Power
- Charging Status
- Online Status
- Remaining Capacity
- Remaining Time
- SoC (State of Charge)
- Voltage
- Current
- Power
- Charging Status
- Online Status
- Remaining Capacity
- Remaining Time
If you consistently experience capacity below 100% when the device is fully charged, you can adjust it using the `max_soc` property.

```yaml
sensor:
- platform: ina219_ups_hat
max_soc: 91
max_soc: 91
```

#### SMA Filtering

By default, the SMA5 filter is applied to the measurements from INA219. That's necessary to filter out noise from the switching power supply and provide smoother readings. You can control the window size with the `sma_samples` property.

```yaml
sensor:
- platform: ina219_ups_hat
max_soc: 91
sma_samples: 10
sma_samples: 10
```

*Tip:* Doubled window size is used for calculation of SoC, Remaining Battery Capacity and Remaining Time

#### Batteries Count

The original Waveshare UPS Hat has 2 batteries in series (8.4V), but some versions of the UPS Hats may have 3 batteries (12.6V). If you have more than 2 batteries in series, use the `batteries_count` parameter.

### Example automations

Copy contents of [examples/automations.yaml](/examples/automations.yaml) to your `automations.yaml`. Customize.

## Directions for installing smbus support on Raspberry Pi
Expand All @@ -101,7 +113,7 @@ $ sudo raspi-config

Select `Interfacing options->I2C` choose `<Yes>` and hit `Enter`, then go to `Finish` and you'll be prompted to reboot.

Install dependencies for use the `smbus-cffi` module and enable your `homeassistant` user to join the _i2c_ group:
Install dependencies for use the `smbus-cffi` module and enable your `homeassistant` user to join the *i2c* group:

```bash
# pi user environment: Install i2c dependencies and utilities
Expand All @@ -114,7 +126,11 @@ $ sudo addgroup homeassistant i2c
$ sudo reboot
```

#### Check the i2c address of the sensor
#### Check the i2c address of the sensor (using HassOS I2C Configurator)

You may use [HassOS I2C Configurator](https://community.home-assistant.io/t/add-on-hassos-i2c-configurator/264167) to activate i2c on your Hass host and search available devices addresses

#### Check the i2c address of the sensor (using i2c-tools)

After installing `i2c-tools`, a new utility is available to scan the addresses of the connected sensors:

Expand All @@ -137,11 +153,14 @@ It will output a table like this:
```

## Thanks

Most of the code was written by [@mykhailog](https://github.com/mykhailog), the author of the original [integration](https://github.com/mykhailog/hacs_waveshare_ups_hat). Unfortunately, his repository seems to be inactive, so I decided to fork it.

## Notes

- Original [waveshare_ups_hat](https://github.com/mykhailog/hacs_waveshare_ups_hat) integration by [@mykhailog](https://github.com/mykhailog) (seems to be dead)
- Cheap [INA219 UPS Hat](https://www.aliexpress.com/item/1005005071564178.html) module from AliExpress

## License
MIT 2023

MIT 2024
1 change: 1 addition & 0 deletions custom_components/ina219_ups_hat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DEFAULT_NAME = "ina219_ups_hat"

CONF_BATTERY_CAPACITY = "battery_capacity"
CONF_ADDR = "addr"
CONF_MAX_SOC = "max_soc"
CONF_SMA_SAMPLES = "sma_samples"
CONF_MIN_ONLINE_CURRENT = "min_online_current"
Expand Down
13 changes: 8 additions & 5 deletions custom_components/ina219_ups_hat/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from .const import CONF_BATTERIES_COUNT, CONF_BATTERY_CAPACITY, CONF_MAX_SOC, CONF_SMA_SAMPLES, CONF_MIN_CHARGING_CURRENT, CONF_MIN_ONLINE_CURRENT
from .ina219 import INA219
from .const import CONF_ADDR, CONF_BATTERIES_COUNT, CONF_BATTERY_CAPACITY, CONF_MAX_SOC, CONF_SMA_SAMPLES, CONF_MIN_CHARGING_CURRENT, CONF_MIN_ONLINE_CURRENT
from .ina219.config import get_ina219_class
from .ina219_wrapper import INA219Wrapper
from homeassistant import core
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID
Expand All @@ -21,14 +21,16 @@ def __init__(self, hass: core.HomeAssistant, config: ConfigType) -> None:
self.name_prefix = config.get(CONF_NAME)
self.id_prefix = config.get(CONF_UNIQUE_ID)

self._addr = config.get(CONF_ADDR)
self._max_soc = config.get(CONF_MAX_SOC)
self._battery_capacity = config.get(CONF_BATTERY_CAPACITY)
self._batteries_count = config.get(CONF_BATTERIES_COUNT)
self._sma_samples = config.get(CONF_SMA_SAMPLES)
self._min_online_current = config.get(CONF_MIN_ONLINE_CURRENT)
self._min_charging_current = config.get(CONF_MIN_CHARGING_CURRENT)

self._ina219 = INA219(addr=0x41)
INA219 = get_ina219_class()
self._ina219 = INA219(addr=int(self._addr, 16))
self._ina219_wrapper = INA219Wrapper(self._ina219, self._sma_samples)

super().__init__(
Expand All @@ -45,6 +47,7 @@ async def _update_data(self):

bus_voltage = ina219_wrapper.getBusVoltageSMA_V() # voltage on V- (load side)
shunt_voltage = (ina219_wrapper.getShuntVoltageSMA_mV() / 1000) # voltage between V+ and V- across the shunt
total_voltage = bus_voltage + shunt_voltage
current = ina219_wrapper.getCurrentSMA_mA() # current in mA
power = ina219_wrapper.getPowerSMA_W() # power in W

Expand Down Expand Up @@ -84,11 +87,11 @@ async def _update_data(self):
remaining_time = None

return {
"voltage": round(bus_voltage + shunt_voltage, 2),
"voltage": round(total_voltage, 2),
"current": round(current / 1000, 2),
"power": round(power_calculated, 2),
"soc": round(soc, 1),
"remaining_battery_capacity": round(remaining_battery_capacity, 0),
"remaining_battery_capacity": round((remaining_battery_capacity * total_voltage) / 1000, 2),
"remaining_time": remaining_time,
"online": online,
"charging": charging,
Expand Down
8 changes: 8 additions & 0 deletions custom_components/ina219_ups_hat/ina219/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import os
from .ina219 import INA219
from .ina219_mock import MockINA219

def get_ina219_class():
if os.getenv('ENV') == 'dev':
return MockINA219
return INA219
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# https://github.com/waveshare/UPS-Power-Module/blob/master/ups_display/ina219.py
import smbus2 as smbus
import time
from .ina219_interface import INA219Interface

# Config Register (R/W)
_REG_CONFIG = 0x00
Expand Down Expand Up @@ -58,9 +59,9 @@ class Mode:
SANDBVOLT_CONTINUOUS = 0x07 # shunt and bus voltage continuous


class INA219:
class INA219(INA219Interface):
def __init__(self, i2c_bus=1, addr=0x40):
self.bus = smbus.SMBus(i2c_bus);
self.bus = smbus.SMBus(i2c_bus)
self.addr = addr

# Set chip to known config values to start
Expand Down
31 changes: 31 additions & 0 deletions custom_components/ina219_ups_hat/ina219/ina219_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from abc import ABC, abstractmethod

# Define the interface for INA219
class INA219Interface(ABC):
@abstractmethod
def read(self, address):
pass

@abstractmethod
def write(self, address, data):
pass

@abstractmethod
def set_calibration_32V_2A(self):
pass

@abstractmethod
def getShuntVoltage_mV(self):
pass

@abstractmethod
def getBusVoltage_V(self):
pass

@abstractmethod
def getCurrent_mA(self):
pass

@abstractmethod
def getPower_W(self):
pass
69 changes: 69 additions & 0 deletions custom_components/ina219_ups_hat/ina219/ina219_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from .ina219_interface import INA219Interface


class MockSMBus:
def __init__(self, bus):
self.regs = {}
self.addr = None

def read_i2c_block_data(self, addr, reg, length):
self.addr = addr
return [(self.regs.get(reg, 0) >> 8) & 0xFF, self.regs.get(reg, 0) & 0xFF]

def write_i2c_block_data(self, addr, reg, data):
self.addr = addr
self.regs[reg] = (data[0] << 8) | data[1]

# Constants
_REG_CONFIG = 0x00
_REG_SHUNTVOLTAGE = 0x01
_REG_BUSVOLTAGE = 0x02
_REG_POWER = 0x03
_REG_CURRENT = 0x04
_REG_CALIBRATION = 0x05

class MockINA219(INA219Interface):
def __init__(self, i2c_bus=1, addr=0x40):
self.bus = MockSMBus(i2c_bus)
self.addr = addr
self._cal_value = 0
self._current_lsb = 0
self._power_lsb = 0
self.set_calibration_32V_2A()

def read(self, address):
data = self.bus.read_i2c_block_data(self.addr, address, 2)
return (data[0] << 8) + data[1]

def write(self, address, data):
self.bus.write_i2c_block_data(self.addr, address, [(data >> 8) & 0xFF, data & 0xFF])

def set_calibration_32V_2A(self):
self._current_lsb = 0.1
self._cal_value = 4096
self._power_lsb = 0.002
self.write(_REG_CALIBRATION, self._cal_value)
self.config = 0x399F # example config value
self.write(_REG_CONFIG, self.config)

def getShuntVoltage_mV(self):
value = self.read(_REG_SHUNTVOLTAGE)
if value > 32767:
value -= 65535
return value * 0.01

def getBusVoltage_V(self):
value = self.read(_REG_BUSVOLTAGE)
return (value >> 3) * 0.004

def getCurrent_mA(self):
value = self.read(_REG_CURRENT)
if value > 32767:
value -= 65535
return value * self._current_lsb

def getPower_W(self):
value = self.read(_REG_POWER)
if value > 32767:
value -= 65535
return value * self._power_lsb
5 changes: 3 additions & 2 deletions custom_components/ina219_ups_hat/ina219_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import numpy as np
from collections import deque
from .ina219 import INA219

from .ina219.ina219_interface import INA219Interface

COEF_SMAx2 = 2

class INA219Wrapper:
def __init__(self, ina219 : INA219, samples_cnt : int):
def __init__(self, ina219 : INA219Interface, samples_cnt : int):
self._ina219 = ina219
self._bus_voltage_buf = deque(maxlen=samples_cnt * COEF_SMAx2)
self._shunt_voltage_buf = deque(maxlen=samples_cnt)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ina219_ups_hat/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/odya/hass-ina219-ups-hat/issues",
"requirements": ["smbus2>=0.4.2","numpy>=1.23.2"],
"version": "0.3.4"
"version": "0.3.5"
}
11 changes: 7 additions & 4 deletions custom_components/ina219_ups_hat/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
UnitOfElectricPotential,
UnitOfPower,
UnitOfTime,
UnitOfEnergy,
)

import homeassistant.helpers.config_validation as cv
Expand All @@ -23,6 +24,7 @@
from .entity import INA219UpsHatEntity
from .coordinator import INA219UpsHatCoordinator
from .const import (
CONF_ADDR,
CONF_BATTERIES_COUNT,
CONF_BATTERY_CAPACITY,
CONF_MAX_SOC,
Expand All @@ -41,9 +43,10 @@
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MAX_SOC, default=100): cv.positive_int,
vol.Optional(CONF_BATTERY_CAPACITY): cv.positive_int,
vol.Optional(CONF_BATTERIES_COUNT, default=2): cv.positive_int,
vol.Optional(CONF_ADDR): cv.string,
vol.Optional(CONF_MAX_SOC, default=91): cv.positive_int,
vol.Optional(CONF_BATTERY_CAPACITY, 9000): cv.positive_int,
vol.Optional(CONF_BATTERIES_COUNT, default=3): cv.positive_int,
vol.Optional(CONF_SMA_SAMPLES, default=5): cv.positive_int,
vol.Optional(CONF_MIN_ONLINE_CURRENT, default=-100): int,
vol.Optional(CONF_MIN_CHARGING_CURRENT, default=50): cv.positive_int,
Expand Down Expand Up @@ -143,7 +146,7 @@ class RemainingCapacitySensor(INA219UpsHatSensor):
def __init__(self, coordinator) -> None:
super().__init__(coordinator)
self._name = "Remaining Capacity"
self._attr_native_unit_of_measurement = "mAh"
self._attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
self._attr_device_class = SensorDeviceClass.ENERGY_STORAGE
self._attr_suggested_display_precision = 0

Expand Down

0 comments on commit 26887c7

Please sign in to comment.