I2CDevices: Add support for the INA219 energy meter

master
Michael Zanetti 2021-11-03 00:35:39 +01:00
parent 74290ca1a3
commit ce599e43c9
7 changed files with 439 additions and 1 deletions

View File

@ -99,3 +99,26 @@ more than 16 channels are required.
Additional information ca be found at the devices users guide at
[https://www.alchemy-power.com/wp-content/uploads/2017/03/Pi-16ADC-User-Guide.pdf](https://www.alchemy-power.com/wp-content/uploads/2017/03/Pi-16ADC-User-Guide.pdf).
## INA219
The INA219 is a voltage/current meter by Texas Instruments.
### Usage
In order to use this device within nymea, it needs to be connected to the I²C bus. At least SDA,
SCL, GND and VDD must be connected. Normally the device comes with a 0.1 Ohm shunt resistor. If
replacing the shunt resistor on the device with something else, the according value needs to be
given during the setup in nymea.
The measured input value will be a floating point value from 0 to 1, depending on the selected input gain.
For instance, if the selected input gain is 4.096V, a voltage of 0V will be indicated in nymea as
0 while an input voltage of 4.096V will be represented as 1.
Setup can be done by performing a discovery for it in nymea. Please verify that the found results
are matching with the address configuration of the device. If the address selector pins are unmodified,
the I²C address will be 64 (0x48). It can be configured to another I²C address by bridging the addrss
selector pins on the device. The INA219 has selectable addresses from 0x40 tox 0x4A.
The device will represent itself as energy meter in nymea and if used, for example in a caravan, it ca
cater as the root meter for the caravans energy system.

View File

@ -1,12 +1,14 @@
include(../plugins.pri)
HEADERS += \
ina219.h \
integrationplugini2cdevices.h \
ads1115channel.h \
pi16adcchannel.h
SOURCES += \
ina219.cpp \
integrationplugini2cdevices.cpp \
ads1115channel.cpp \
pi16adcchannel.cpp

187
i2cdevices/ina219.cpp Normal file
View File

@ -0,0 +1,187 @@
#include "ina219.h"
#include <unistd.h>
#include <QtDebug>
#include <QThread>
#include <QDebug>
#include <QJsonDocument>
#include "extern-plugininfo.h"
#define INA219_REGISTER_CONFIGURATION 0x00
#define INA219_REGISTER_SHUNT_VOLTAGE 0x01
#define INA219_REGISTER_BUS_VOLTAGE 0x02
#define INA219_REGISTER_POWER 0x03
#define INA219_REGISTER_CURRENT 0x04
#define INA219_REGISTER_CALIBRATION 0x05
#define INA219_ADDRESS 0x80
#define INA219_READ 0x01
#define SHUNT_MILLIVOLTS_LSB 0.01 //# 10uV
#define BUS_MILLIVOLTS_LSB 4 // 4mV
#define CALIBRATION_FACTOR 0.04096
#define MAX_CALIBRATION_VALUE 0xFFFE // Max value supported (65534 decimal)
// # In the spec (p17) the current LSB factor for the minimum LSB is
// # documented as 32767, but a larger value (100.1% of 32767) is used
// # to guarantee that current overflow can always be detected.
#define CURRENT_LSB_FACTOR 32800
#define OVERFLOW_VALUE 1
#define INA219_CONFIG_BIT_RST 15
#define INA219_CONFIG_BIT_BRNG 13
#define INA219_CONFIG_BIT_PG1 12
#define INA219_CONFIG_BIT_PG0 11
#define INA219_CONFIG_BIT_BADC4 10
#define INA219_CONFIG_BIT_BADC3 9
#define INA219_CONFIG_BIT_BADC2 8
#define INA219_CONFIG_BIT_BADC1 7
#define INA219_CONFIG_BIT_SADC4 6
#define INA219_CONFIG_BIT_SADC3 5
#define INA219_CONFIG_BIT_SADC2 4
#define INA219_CONFIG_BIT_SADC1 3
#define INA219_CONFIG_BIT_MODE3 2
#define INA219_CONFIG_BIT_MODE2 1
#define INA219_CONFIG_BIT_MODE1 0
Ina219::Ina219(const QString &portName, int address, double shuntOhms, VoltageRange voltageRange, QObject *parent):
I2CDevice(portName, address, parent),
m_shuntOhms(shuntOhms),
m_voltageRange(voltageRange)
{
}
bool Ina219::writeData(int fileDescriptor, const QByteArray &data)
{
Q_UNUSED(data)
char buf[3] = {0};
// Calibration
double gain;
switch (m_gainVolts) {
case GainVolts032:
gain = 0.32;
break;
case GainVolts016:
gain = 0.16;
break;
case GainVolts008:
gain = 0.08;
break;
case GainVolts004:
default:
gain = 0.04;
break;
}
double maxPossibleAmps = gain / m_shuntOhms;
m_currentLSB = maxPossibleAmps / CURRENT_LSB_FACTOR;
quint16 calibration = CALIBRATION_FACTOR / (m_currentLSB * m_shuntOhms);
buf[0] = INA219_REGISTER_CALIBRATION;
buf[1] = calibration >> 8;
buf[2] = calibration & 0xFF;
qCDebug(dcI2cDevices()) << "INA219 writing calibration:" << QString::number(calibration, 16) << QByteArray(buf, 3).toHex();
int ret = write(fileDescriptor, buf, 3);
if (ret != 3) {
qCWarning(dcI2cDevices()) << "Failed to write calibration to INA219.";
return false;
}
// Configuration
quint16 configuration = m_voltageRange << INA219_CONFIG_BIT_BRNG;
configuration |= m_gainVolts << INA219_CONFIG_BIT_PG0;
configuration |= m_busADC << INA219_CONFIG_BIT_BADC1;
configuration |= m_shuntADC << INA219_CONFIG_BIT_SADC1;
configuration |= m_operationMode;
buf[0] = INA219_REGISTER_CONFIGURATION;
buf[1] = configuration >> 8;
buf[2] = configuration & 0xFF;
qCDebug(dcI2cDevices()) << "INA219 writing configuration:" << QString::number(configuration, 16) << QByteArray(buf, 3).toHex();
ret = write(fileDescriptor, buf, 3);
if (ret != 3) {
qCWarning(dcI2cDevices()) << "Failed to write configuration to INA219.";
return false;
}
return true;
}
QByteArray Ina219::readData(int fileDescriptor)
{
char buf[2] = {0};
buf[0] = INA219_REGISTER_SHUNT_VOLTAGE;
int ret = write(fileDescriptor, buf, 1);
if (ret != 1) {
qCWarning(dcI2cDevices()) << "Failed to select shunt voltage register on INA219";
return QByteArray();
}
ret = read(fileDescriptor, buf, 2);
if (ret != 2) {
qCWarning(dcI2cDevices()) << "Failed to read shunt voltage register on INA219";
return QByteArray();
}
int shuntVoltageRaw = ((buf[0] << 8) | buf[1]);
double shuntVoltage = shuntVoltageRaw * SHUNT_MILLIVOLTS_LSB / 1000;
buf[0] = INA219_REGISTER_BUS_VOLTAGE;
ret = write(fileDescriptor, buf, 1);
if (ret != 1) {
qCWarning(dcI2cDevices()) << "Failed to select bus voltage register on INA219";
return QByteArray();
}
ret = read(fileDescriptor, buf, 2);
if (ret != 2) {
qCWarning(dcI2cDevices()) << "Failed to read bus voltage register on INA219";
return QByteArray();
}
int busVoltageRaw = ((buf[0] << 8) | buf[1]);
bool overflow = (busVoltageRaw & OVERFLOW_VALUE) == 1;
busVoltageRaw = busVoltageRaw >> 3; // Registers are not right_aligned
double busVoltage = 1.0 * busVoltageRaw * BUS_MILLIVOLTS_LSB / 1000;
buf[0] = INA219_REGISTER_POWER;
ret = write(fileDescriptor, buf, 1);
if (ret != 1) {
qCWarning(dcI2cDevices()) << "Failed to select power register on INA219";
return QByteArray();
}
ret = read(fileDescriptor, buf, 2);
if (ret != 2) {
qCWarning(dcI2cDevices()) << "Failed to read power register on INA219";
return QByteArray();
}
int powerRaw = ((buf[0] << 8) | buf[1]);
double powerLSB = m_currentLSB * 20;
double power = powerRaw * powerLSB;
buf[0] = INA219_REGISTER_CURRENT;
ret = write(fileDescriptor, buf, 1);
if (ret != 1) {
qCWarning(dcI2cDevices()) << "Failed to select current register on INA219";
return QByteArray();
}
ret = read(fileDescriptor, buf, 2);
if (ret != 2) {
qCWarning(dcI2cDevices()) << "Failed to read current register on INA219";
return QByteArray();
}
int currentRaw = ((buf[0] << 8) | buf[1]);
double current = 1.0 * currentRaw * m_currentLSB;
qCDebug(dcI2cDevices()).nospace().noquote() << "INA219 Shunt voltage: " << shuntVoltage << "mV, Bus voltage: " << busVoltage << "V, Power: " << power << "W, Current: " << current << "A, Overflow: " << overflow;
QVariantMap readings;
readings.insert("shuntVoltage", shuntVoltage);
readings.insert("busVoltage", busVoltage);
readings.insert("power", power);
readings.insert("current", current);
readings.insert("overflow", overflow);
return QJsonDocument::fromVariant(readings).toJson(QJsonDocument::Compact);
}

64
i2cdevices/ina219.h Normal file
View File

@ -0,0 +1,64 @@
#ifndef INA219_H
#define INA219_H
#include <QObject>
#include <hardware/i2c/i2cdevice.h>
class Ina219 : public I2CDevice
{
Q_OBJECT
public:
enum VoltageRange {
VoltageRange16 = 0,
VoltageRange32 = 1
};
Q_ENUM(VoltageRange)
enum GainVolts {
GainVolts004 = 0,
GainVolts008 = 1,
GainVolts016 = 2,
GainVolts032 = 3,
};
Q_ENUM(GainVolts)
enum ADCBits {
ADCBits9 = 0,
ADCBits10 = 1,
ADCBits11 = 2,
ADCBits12 = 3
};
Q_ENUM(ADCBits)
enum OperationMode {
OperationModePowerDown = 0,
OperationModeShuntVoltageTriggered = 1,
OperationModeBusVoltageTriggered = 2,
OperationModeShuntAndBusTriggered = 3,
OperationModeADCOff = 4,
OperationModeShuntVoltageContinuous = 5,
OperationModeBusVoltageContinuous = 6,
OperationModeShuntAndBusContinuous = 7
};
Q_ENUM(OperationMode)
explicit Ina219(const QString &portName, int address, double shuntOhms, VoltageRange voltageRange, QObject *parent = nullptr);
bool writeData(int fileDescriptor, const QByteArray &data) override;
QByteArray readData(int fileDescriptor) override;
signals:
void measurementAvailable();
private:
double m_shuntOhms = 0.1;
VoltageRange m_voltageRange = VoltageRange32;
GainVolts m_gainVolts = GainVolts004;
ADCBits m_busADC = ADCBits12;
ADCBits m_shuntADC = ADCBits12;
OperationMode m_operationMode = OperationModeShuntAndBusContinuous;
double m_currentLSB = 0;
};
#endif // INA219_H

View File

@ -33,10 +33,12 @@
#include "pi16adcchannel.h"
#include "ads1115channel.h"
#include "ina219.h"
#include <hardware/i2c/i2cmanager.h>
#include <QDebug>
#include <QJsonDocument>
IntegrationPluginI2CDevices::IntegrationPluginI2CDevices(): IntegrationPlugin()
{
@ -107,7 +109,7 @@ void IntegrationPluginI2CDevices::discoverThings(ThingDiscoveryInfo *info)
}
if (info->thingClassId() == ads1115ThingClassId) {
// The ADS1115 has selectable addresses from 0x48 to 0x51
// The ADS1115 has selectable addresses from 0x48 to 0x4B
if (scanResult.address >= 0x48 && scanResult.address <= 0x4B) {
ThingDescriptor descriptor(ads1115ThingClassId, "ADS1113/ADS1114/ADS1115", QString("%1: 0x%2").arg(scanResult.portName).arg(scanResult.address, 0, 16));
ParamList params;
@ -117,6 +119,18 @@ void IntegrationPluginI2CDevices::discoverThings(ThingDiscoveryInfo *info)
info->addThingDescriptor(descriptor);
}
}
if (info->thingClassId() == ina219ThingClassId) {
// The INA219 has selectable addresses from 0x040 tox 0x4A
if (scanResult.address >= 0x40 && scanResult.address <= 0x4A) {
ThingDescriptor descriptor(ina219ThingClassId, "INA219", QString("%1: 0x%2").arg(scanResult.portName).arg(scanResult.address, 0, 16));
ParamList params;
params << Param(ina219ThingI2cPortParamTypeId, scanResult.portName);
params << Param(ina219ThingI2cAddressParamTypeId, scanResult.address);
descriptor.setParams(params);
info->addThingDescriptor(descriptor);
}
}
}
info->finish(Thing::ThingErrorNoError);
@ -200,6 +214,56 @@ void IntegrationPluginI2CDevices::setupThing(ThingSetupInfo *info)
}
info->finish(Thing::ThingErrorNoError);
}
if (info->thing()->thingClassId() == ina219ThingClassId) {
QString i2cPortName = info->thing()->paramValue(ina219ThingI2cPortParamTypeId).toString();
int i2cAddress = info->thing()->paramValue(ina219ThingI2cAddressParamTypeId).toInt();
double shuntOhms = info->thing()->paramValue(ina219ThingShuntOhmsParamTypeId).toDouble();
Ina219::VoltageRange voltageRange = info->thing()->paramValue(ina219ThingVoltageRangeParamTypeId).toUInt() == 16 ? Ina219::VoltageRange16 : Ina219::VoltageRange32;
Ina219 *ina219 = new Ina219(i2cPortName, i2cAddress, shuntOhms, voltageRange, this);
if (!hardwareManager()->i2cManager()->open(ina219)) {
delete ina219;
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Failed to open I2C port."));
return;
}
Thing *thing = info->thing();
connect(ina219, &Ina219::readingAvailable, thing, [thing](const QByteArray &data){
QJsonParseError error;
QVariantMap values = QJsonDocument::fromJson(data, &error).toVariant().toMap();
if (error.error != QJsonParseError::NoError) {
qCWarning(dcI2cDevices()) << thing->name() << "Failed to read data from INA219";
return;
}
double currentPower = values.value("power").toDouble();
thing->setStateValue(ina219CurrentPowerStateTypeId, currentPower);
thing->setStateValue(ina219VoltagePhaseAStateTypeId, values.value("busVoltage").toDouble());
thing->setStateValue(ina219CurrentPhaseAStateTypeId, values.value("current").toDouble());
thing->setStateValue(ina219OverflowStateTypeId, values.value("overflow").toBool());
// Calculate an estimate of totalEnergyConsumed
QDateTime lastUpdate = thing->property("lastUpdate").toDateTime();
if (lastUpdate.isNull()) {
lastUpdate = QDateTime::currentDateTime();
}
double hoursPassed = lastUpdate.msecsTo(QDateTime::currentDateTime()) / 1000 / 60 / 60;
if (currentPower >= 0) {
double totalEnergyConsumed = thing->stateValue(ina219TotalEnergyConsumedStateTypeId).toDouble();
totalEnergyConsumed += currentPower / 1000 * hoursPassed;
thing->setStateValue(ina219TotalEnergyConsumedStateTypeId, totalEnergyConsumed);
} else {
double totalEnergyReturned = thing->stateValue(ina219TotalEnergyProducedStateTypeId).toDouble();
totalEnergyReturned += -currentPower / 1000 * hoursPassed;
thing->setStateValue(ina219TotalEnergyProducedStateTypeId, totalEnergyReturned);
}
});
hardwareManager()->i2cManager()->writeData(ina219, "init");
hardwareManager()->i2cManager()->startReading(ina219, 5000);
info->finish(Thing::ThingErrorNoError);
}
}
void IntegrationPluginI2CDevices::thingRemoved(Thing *thing)

View File

@ -33,6 +33,8 @@
#include <integrations/integrationplugin.h>
#include "extern-plugininfo.h"
class I2CDevice;
class IntegrationPluginI2CDevices: public IntegrationPlugin

View File

@ -449,6 +449,102 @@
"defaultValue": false
}
]
},
{
"id": "95239915-fe8e-403f-b1b8-9132c4cbef3c",
"name": "ina219",
"displayName": "INA219",
"createMethods": ["discovery"],
"interfaces": ["energymeter"],
"paramTypes": [
{
"id": "28ef56f1-8057-4ca5-879f-20914a63982a",
"name": "i2cPort",
"displayName": "I2C port",
"type": "QString"
},
{
"id": "380d18b4-551d-4b47-99e4-bfc231ff9d9d",
"name": "i2cAddress",
"displayName": "I2C address",
"type": "int"
},
{
"id": "80b72157-24f9-4bb3-ab89-78feaf0248ff",
"name": "shuntOhms",
"displayName": "Shunt resistor",
"type": "double",
"unit": "Ohm",
"minValue": 0,
"maxValue": 1000000,
"defaultValue": 0.1
},
{
"id": "40b3d7da-908e-4b17-a483-28580b598c69",
"name": "voltageRange",
"displayName": "Voltage range",
"type": "uint",
"unit": "Volt",
"allowedValues": [16, 32],
"defaultValue": 16
}
],
"stateTypes": [
{
"id": "49fdc415-f270-48ef-9b8f-1212d0cb39e4",
"name": "currentPower",
"displayName": "Current power",
"displayNameEvent": "Current power changed",
"type": "double",
"unit": "Watt",
"defaultValue": 0
},
{
"id": "64856549-f445-4c15-bdda-5a1513604a88",
"name": "totalEnergyConsumed",
"displayName": "Total consumed energy",
"displayNameEvent": "Total consumed energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "ff6461fb-ebbf-4acf-bcbb-8de2ddd3b15b",
"name": "totalEnergyProduced",
"displayName": "Total returned energy",
"displayNameEvent": "Total returned energy changed",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
},
{
"id": "2da9be93-85cd-4504-8ae8-91af351963a8",
"name": "currentPhaseA",
"displayName": "Current",
"displayNameEvent": "Current changed",
"type": "double",
"unit": "Ampere",
"defaultValue": 0
},
{
"id": "b4ea6bcb-39f6-45aa-893e-275cfbf0bcd3",
"name": "voltagePhaseA",
"displayName": "Voltage",
"displayNameEvent": "Voltage changed",
"type": "double",
"unit": "Volt",
"defaultValue": 0
},
{
"id": "f31947cc-8c48-4bc3-b61b-01cfc7b472bf",
"name": "overflow",
"displayName": "Overflow",
"displayNameEvent": "Overflow changed",
"type": "bool",
"defaultValue": false,
"cached": false
}
]
}
]
}