Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 83 additions & 29 deletions usermods/INA226_v2/INA226_v2.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
#include "wled.h"
#include <INA226_WE.h>

#ifndef INA226_ADDRESS
#define INA226_ADDRESS 0x40 // Default I2C address for INA226
#endif

#define DEFAULT_CHECKINTERVAL 60000
#define DEFAULT_CHECKINTERVAL 1000
#define DEFAULT_INASAMPLES 128
#define DEFAULT_INASAMPLESENUM AVERAGE_128
#define DEFAULT_INACONVERSIONTIME 1100
#define DEFAULT_INACONVERSIONTIMEENUM CONV_TIME_1100

// Compile-time defaults for shunt resistor (micro-ohms), current range (mA), and current offset (mA)
// These can be overridden via -D flags in platformio.ini / platformio_override.ini
#ifndef INA226_SHUNT_MICRO_OHMS
#define INA226_SHUNT_MICRO_OHMS 1000000 // 1 Ohm = 1,000,000 μΩ
#endif

#ifndef INA226_DEFAULT_CURRENT_RANGE
#define INA226_DEFAULT_CURRENT_RANGE 1000 // 1000 mA = 1 A
#endif

#ifndef INA226_CURRENT_OFFSET_MA
#define INA226_CURRENT_OFFSET_MA 0 // No offset by default
#endif

// A packed version of all INA settings enums and their human friendly counterparts packed into a 32 bit structure
// Some values are shifted and need to be preprocessed before usage
struct InaSettingLookup
Expand Down Expand Up @@ -81,10 +97,11 @@ class UsermodINA226 : public Usermod
uint16_t _settingInaSamples : 11; // Number of samples for averaging, max 1024

uint8_t _i2cAddress;
uint16_t _checkInterval; // milliseconds, user settings is in seconds
float _decimalFactor; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)
uint16_t _shuntResistor; // Shunt resistor value in milliohms
uint16_t _currentRange; // Expected maximum current in milliamps
uint16_t _checkIntervalMs; // milliseconds, user settings is in seconds
float _decimalFactor; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)
uint32_t _shuntResistorUOhm; // Shunt resistor value in micro-ohms (μΩ)
uint16_t _currentRangeMa; // Expected maximum current in milliamps
int16_t _currentOffsetMa; // Current offset in milliamps, subtracted from readings

uint8_t _lastStatus = 0;
float _lastCurrent = 0;
Expand Down Expand Up @@ -118,9 +135,11 @@ class UsermodINA226 : public Usermod
_ina226 = new INA226_WE(_i2cAddress);
if (!_ina226->init())
{
DEBUG_PRINTLN(F("INA226 initialization failed!"));
DEBUG_PRINTLN(F("INA226: init failed!"));
return;
}
DEBUG_PRINTF_P(PSTR("INA226: addr=0x%02X shunt=%luμΩ range=%umA offset=%dmA\n"),
_i2cAddress, _shuntResistorUOhm, _currentRangeMa, _currentOffsetMa);
_ina226->setCorrectionFactor(1.0);

uint16_t tmpShort = _settingInaSamples;
Expand All @@ -129,7 +148,7 @@ class UsermodINA226 : public Usermod
tmpShort = _settingInaConversionTimeUs << 2;
_ina226->setConversionTime(getConversionTimeEnum(tmpShort));

if (_checkInterval >= 20000)
if (_checkIntervalMs >= 20000)
{
_isTriggeredOperationMode = true;
_ina226->setMeasureMode(TRIGGERED);
Expand All @@ -140,7 +159,11 @@ class UsermodINA226 : public Usermod
_ina226->setMeasureMode(CONTINUOUS);
}

_ina226->setResistorRange(static_cast<float>(_shuntResistor) / 1000.0, static_cast<float>(_currentRange) / 1000.0);
_ina226->setResistorRange(static_cast<float>(_shuntResistorUOhm) / 1000000.0f, static_cast<float>(_currentRangeMa) / 1000.0f);

DEBUG_PRINTF_P(PSTR("INA226: mode=%s interval=%ums samples=%u convTime=%uμs\n"),
_isTriggeredOperationMode ? "triggered" : "continuous",
_checkIntervalMs, _settingInaSamples, _settingInaConversionTimeUs << 2);
Comment on lines +164 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use %lu format specifier for uint32_t.

_checkIntervalMs is uint32_t, but %u is for unsigned int. While this works on ESP32/ESP8266 where both are 32-bit, %lu is the correct portable specifier for uint32_t.

Proposed fix
-        DEBUG_PRINTF_P(PSTR("INA226: mode=%s interval=%ums samples=%u convTime=%uμs\n"),
+        DEBUG_PRINTF_P(PSTR("INA226: mode=%s interval=%lums samples=%u convTime=%uμs\n"),
                        _isTriggeredOperationMode ? "triggered" : "continuous",
                        _checkIntervalMs, _settingInaSamples, _settingInaConversionTimeUs << 2);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/INA226_v2/INA226_v2.cpp` around lines 164 - 166, The debug printf
uses the wrong format specifier for a uint32_t: in the DEBUG_PRINTF_P call that
prints _checkIntervalMs (and related fields) replace the `%u` used for
_checkIntervalMs with `%lu` (or cast _checkIntervalMs to unsigned long and use
`%lu`) so the format matches the uint32_t type; update the format string in the
DEBUG_PRINTF_P invocation that references _isTriggeredOperationMode,
_checkIntervalMs, _settingInaSamples, and _settingInaConversionTimeUs
accordingly to avoid undefined behavior.

}

void fetchAndPushValues()
Expand All @@ -150,17 +173,17 @@ class UsermodINA226 : public Usermod
if (_lastStatus != 0)
return;

float current = truncateDecimals(_ina226->getCurrent_mA() / 1000.0);
float current = truncateDecimals((_ina226->getCurrent_mA() - _currentOffsetMa) / 1000.0f);
float voltage = truncateDecimals(_ina226->getBusVoltage_V());
float power = truncateDecimals(_ina226->getBusPower() / 1000.0);
float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_V());
float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_mV());
bool overflow = _ina226->overflow;

#ifndef WLED_DISABLE_MQTT
mqttPublishIfChanged(F("current"), _lastCurrentSent, current, 0.01f);
mqttPublishIfChanged(F("voltage"), _lastVoltageSent, voltage, 0.01f);
mqttPublishIfChanged(F("power"), _lastPowerSent, power, 0.1f);
mqttPublishIfChanged(F("shunt_voltage"), _lastShuntVoltageSent, shuntVoltage, 0.01f);
mqttPublishIfChanged(F("shunt_voltage_drop"), _lastShuntVoltageSent, shuntVoltage, 0.01f);
mqttPublishIfChanged(F("overflow"), _lastOverflowSent, overflow);
#endif

Expand All @@ -169,6 +192,9 @@ class UsermodINA226 : public Usermod
_lastPower = power;
_lastShuntVoltage = shuntVoltage;
_lastOverflow = overflow;

DEBUG_PRINTF_P(PSTR("INA226: %.3fA %.2fV %.2fW shunt=%.2fmV%s\n"),
current, voltage, power, shuntVoltage, overflow ? " OVF" : "");
}

void handleTriggeredMode(unsigned long currentTime)
Expand All @@ -188,7 +214,7 @@ class UsermodINA226 : public Usermod
}
else
{
if (currentTime - _lastLoopCheck >= _checkInterval)
if (currentTime - _lastLoopCheck >= _checkIntervalMs)
{
// Start a measurement and use isBusy() later to determine when it is done
_ina226->startSingleMeasurementNoWait();
Expand All @@ -201,7 +227,7 @@ class UsermodINA226 : public Usermod

void handleContinuousMode(unsigned long currentTime)
{
if (currentTime - _lastLoopCheck >= _checkInterval)
if (currentTime - _lastLoopCheck >= _checkIntervalMs)
{
_lastLoopCheck = currentTime;
fetchAndPushValues();
Expand All @@ -224,8 +250,8 @@ class UsermodINA226 : public Usermod
snprintf_P(topic, 127, "%s/power", mqttDeviceTopic);
mqttCreateHassSensor(F("Power"), topic, F("power"), F("W"));

snprintf_P(topic, 127, "%s/shunt_voltage", mqttDeviceTopic);
mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("V"));
snprintf_P(topic, 127, "%s/shunt_voltage_drop", mqttDeviceTopic);
mqttCreateHassSensor(F("Shunt Voltage Drop"), topic, F("voltage"), F("mV"));

snprintf_P(topic, 127, "%s/overflow", mqttDeviceTopic);
mqttCreateHassBinarySensor(F("Overflow"), topic);
Expand Down Expand Up @@ -315,14 +341,23 @@ class UsermodINA226 : public Usermod
UsermodINA226()
{
// Default values
_settingEnabled = true;
_settingInaSamples = DEFAULT_INASAMPLES;
_settingInaConversionTimeUs = DEFAULT_INACONVERSIONTIME;

_i2cAddress = INA226_ADDRESS;
_checkInterval = DEFAULT_CHECKINTERVAL;
_checkIntervalMs = DEFAULT_CHECKINTERVAL;
_decimalFactor = 100;
_shuntResistor = 1000;
_currentRange = 1000;
_shuntResistorUOhm = INA226_SHUNT_MICRO_OHMS;
_currentRangeMa = INA226_DEFAULT_CURRENT_RANGE;
_currentOffsetMa = INA226_CURRENT_OFFSET_MA;

_mqttPublish = false;
_mqttPublishAlways = false;
_mqttHomeAssistant = false;
_initDone = false;
_isTriggeredOperationMode = false;
_measurementTriggered = false;
}

void setup()
Expand Down Expand Up @@ -399,7 +434,7 @@ class UsermodINA226 : public Usermod
JsonArray jsonCurrent = user.createNestedArray(F("Current"));
JsonArray jsonVoltage = user.createNestedArray(F("Voltage"));
JsonArray jsonPower = user.createNestedArray(F("Power"));
JsonArray jsonShuntVoltage = user.createNestedArray(F("Shunt Voltage"));
JsonArray jsonShuntVoltage = user.createNestedArray(F("Shunt Voltage Drop"));
JsonArray jsonOverflow = user.createNestedArray(F("Overflow"));

if (_lastLoopCheck == 0)
Expand Down Expand Up @@ -432,7 +467,7 @@ class UsermodINA226 : public Usermod
jsonPower.add(F("W"));

jsonShuntVoltage.add(_lastShuntVoltage);
jsonShuntVoltage.add(F("V"));
jsonShuntVoltage.add(F("mV"));

jsonOverflow.add(_lastOverflow ? F("true") : F("false"));
}
Expand All @@ -442,12 +477,13 @@ class UsermodINA226 : public Usermod
JsonObject top = root.createNestedObject(FPSTR(_name));
top[F("Enabled")] = _settingEnabled;
top[F("I2CAddress")] = static_cast<uint8_t>(_i2cAddress);
top[F("CheckInterval")] = _checkInterval / 1000;
top[F("CheckInterval")] = _checkIntervalMs / 1000;
top[F("INASamples")] = _settingInaSamples;
top[F("INAConversionTime")] = _settingInaConversionTimeUs << 2;
top[F("Decimals")] = log10f(_decimalFactor);
top[F("ShuntResistor")] = _shuntResistor;
top[F("CurrentRange")] = _currentRange;
top[F("ShuntResistor")] = static_cast<float>(_shuntResistorUOhm) / 1000.0f;
top[F("CurrentRange")] = _currentRangeMa;
top[F("CurrentOffset")] = _currentOffsetMa;
#ifndef WLED_DISABLE_MQTT
top[F("MqttPublish")] = _mqttPublish;
top[F("MqttPublishAlways")] = _mqttPublishAlways;
Expand All @@ -457,6 +493,17 @@ class UsermodINA226 : public Usermod
DEBUG_PRINTLN(F("INA226 config saved."));
}

void appendConfigData() override
{
oappend(F("addInfo('INA226:CheckInterval',1,'seconds');"));
oappend(F("addInfo('INA226:INASamples',1,'samples (1-1024)');"));
oappend(F("addInfo('INA226:INAConversionTime',1,'&micro;s');"));
oappend(F("addInfo('INA226:Decimals',1,'(0-5)');"));
oappend(F("addInfo('INA226:ShuntResistor',1,'m&Omega;');"));
oappend(F("addInfo('INA226:CurrentRange',1,'mA');"));
oappend(F("addInfo('INA226:CurrentOffset',1,'mA');"));
}

bool readFromConfig(JsonObject &root) override
{
JsonObject top = root[FPSTR(_name)];
Expand All @@ -472,12 +519,12 @@ class UsermodINA226 : public Usermod
configComplete = false;

configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress);
if (getJsonValue(top[F("CheckInterval")], _checkInterval))
if (getJsonValue(top[F("CheckInterval")], _checkIntervalMs))
{
if (1 <= _checkInterval && _checkInterval <= 600)
_checkInterval *= 1000;
if (1 <= _checkIntervalMs && _checkIntervalMs <= 600)
_checkIntervalMs *= 1000;
else
_checkInterval = DEFAULT_CHECKINTERVAL;
_checkIntervalMs = DEFAULT_CHECKINTERVAL;
}
else
configComplete = false;
Expand Down Expand Up @@ -511,8 +558,15 @@ class UsermodINA226 : public Usermod
else
configComplete = false;

configComplete &= getJsonValue(top[F("ShuntResistor")], _shuntResistor);
configComplete &= getJsonValue(top[F("CurrentRange")], _currentRange);
float shuntMilliOhms;
if (getJsonValue(top[F("ShuntResistor")], shuntMilliOhms))
_shuntResistorUOhm = static_cast<uint32_t>(shuntMilliOhms * 1000.0f + 0.5f);
else
configComplete = false;

configComplete &= getJsonValue(top[F("CurrentRange")], _currentRangeMa);
if (!getJsonValue(top[F("CurrentOffset")], _currentOffsetMa))
_currentOffsetMa = INA226_CURRENT_OFFSET_MA; // Use compile-time default if missing from config

#ifndef WLED_DISABLE_MQTT
if (getJsonValue(top[F("MqttPublish")], tmpBool))
Expand Down
29 changes: 28 additions & 1 deletion usermods/INA226_v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The following settings can be configured in the Usermod Menu:
- **Decimals**: Number of decimals in the output.
- **ShuntResistor**: Shunt resistor value in milliohms. An R100 shunt resistor should be written as "100", while R010 should be "10".
- **CurrentRange**: Expected maximum current in milliamps (e.g., 5 A = 5000 mA).
- **CurrentOffset**: Current offset in milliamps, subtracted from raw readings. Useful for compensating a consistent bias in the sensor. Default is 0.
- **MqttPublish**: Enable or disable MQTT publishing.
- **MqttPublishAlways**: Publish always, regardless if there is a change.
- **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery.
Expand Down Expand Up @@ -63,4 +64,30 @@ extends = env:esp32dev
custom_usermods = ${env:esp32dev.custom_usermods} INA226
build_flags = ${env:esp32dev.build_flags}
; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal
```
```

### Compile-time Defaults

Several parameters can be overridden at compile time via `-D` build flags. This is useful for setting board-specific defaults so the device works correctly on first boot without manual configuration.

| Build Flag | Default | Unit | Description |
|---|---|---|---|
| `INA226_ADDRESS` | `0x40` | — | I2C address of the INA226 |
| `INA226_SHUNT_MICRO_OHMS` | `1000000` | μΩ | Shunt resistor value (1 000 000 μΩ = 1 Ω) |
| `INA226_DEFAULT_CURRENT_RANGE` | `1000` | mA | Expected maximum current (1000 mA = 1 A) |
| `INA226_CURRENT_OFFSET_MA` | `0` | mA | Current offset subtracted from readings |

Example for a board with a 4.8 mΩ shunt, 10 A range, and 53 mA offset:

```ini
[env:my_board]
extends = env:esp32dev
custom_usermods = ${env:esp32dev.custom_usermods} INA226
build_flags = ${env:esp32dev.build_flags}
-D USERMOD_INA226
-D INA226_SHUNT_MICRO_OHMS=4800
-D INA226_DEFAULT_CURRENT_RANGE=10000
-D INA226_CURRENT_OFFSET_MA=53
```

All compile-time defaults can still be changed at runtime through the Usermod settings page.
Comment on lines +69 to +93
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document the MQTT/topic migration in this section.

This PR intentionally renames shunt_voltage to shunt_voltage_drop and changes the unit from V to mV, but the README still does not call out that existing dashboards and automations need to be updated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/INA226_v2/README.md` around lines 69 - 93, Update the Compile-time
Defaults (or a newly added "MQTT/topic migration" subsection) to document that
the MQTT field/topic formerly named "shunt_voltage" has been renamed to
"shunt_voltage_drop" and its unit changed from V to mV; explicitly call out that
existing dashboards, automations, and integrations must be updated to use the
new name and unit, show the old → new mapping (shunt_voltage →
shunt_voltage_drop) and unit conversion (V → mV) so users can adjust
thresholds/graphs, and mention any retained backward-compatibility behavior (if
none, state that topics/payloads are breaking changes).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this belongs in the README.md, but rather in the release notes. Anyone have an opinion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daggerrz, that's a fair point — release notes are the natural place to call out breaking changes for a specific version. However, there's a case for a small note in the README as well: release notes tend to get buried over time, while the README is what a user sees when they first look at the module or when they're troubleshooting a broken dashboard months after upgrading. Many projects (e.g., Home Assistant integrations, Mosquitto-based tools) keep a short "Upgrading / Breaking Changes" section in the README alongside the main changelog entry, so the information stays discoverable long-term.

A lightweight middle ground could be:

  • Release notes – the primary call-out, with the full old → new mapping.
  • README – a brief, permanent note (one or two lines) in the configuration section under ShuntResistor / MQTT settings, e.g.:

⚠️ Breaking change (v<X>): The MQTT topic shunt_voltage (V) has been renamed to shunt_voltage_drop (mV). Existing dashboards and automations must be updated.

That way users who land on the README without reading the release notes are still warned. But if the project's convention is to keep only release notes for such notices, that's perfectly reasonable too — ultimately it's your call as the maintainer.


🧠 Learnings used
Learnt from: netmindz
Repo: wled/WLED PR: 5311
File: platformio.ini:395-395
Timestamp: 2026-01-31T18:19:33.055Z
Learning: In WLED, usermods can be referenced as out-of-tree library dependencies (e.g., via lib_deps pointing to external repositories) while the `custom_usermods` setting in platformio.ini controls which in-tree usermods are enabled. Setting `custom_usermods = ` (empty) only disables in-tree usermods; out-of-tree usermods pulled via library dependencies remain active.

Learnt from: KrX3D
Repo: wled/WLED PR: 4237
File: usermods/ina2xx_v2/ina2xx_v2.cpp:178-185
Timestamp: 2025-05-06T20:31:05.208Z
Learning: KrX3D plans to add INA226 support to the INA2xx usermod in a future update. Currently, the code only instantiates INA219_WE objects despite the more generic naming.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:612-617
Timestamp: 2026-02-18T00:41:15.185Z
Learning: In the seven_segment_display_reloaded_v2 usermod (PR `#4585`), the `addToConfig()` method intentionally calls `_updateMQTT()` to publish MQTT state on every config save. This behavior is by design and should not be flagged as an issue.