From ce599e43c9f6b3644a22a4aff9afaa7b134752fd Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 3 Nov 2021 00:35:39 +0100 Subject: [PATCH] I2CDevices: Add support for the INA219 energy meter --- i2cdevices/README.md | 23 +++ i2cdevices/i2cdevices.pro | 2 + i2cdevices/ina219.cpp | 187 ++++++++++++++++++++ i2cdevices/ina219.h | 64 +++++++ i2cdevices/integrationplugini2cdevices.cpp | 66 ++++++- i2cdevices/integrationplugini2cdevices.h | 2 + i2cdevices/integrationplugini2cdevices.json | 96 ++++++++++ 7 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 i2cdevices/ina219.cpp create mode 100644 i2cdevices/ina219.h diff --git a/i2cdevices/README.md b/i2cdevices/README.md index 6ea4a0d4..ba106ba9 100644 --- a/i2cdevices/README.md +++ b/i2cdevices/README.md @@ -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. diff --git a/i2cdevices/i2cdevices.pro b/i2cdevices/i2cdevices.pro index b6b22cbd..e9afbbe7 100644 --- a/i2cdevices/i2cdevices.pro +++ b/i2cdevices/i2cdevices.pro @@ -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 diff --git a/i2cdevices/ina219.cpp b/i2cdevices/ina219.cpp new file mode 100644 index 00000000..a9844525 --- /dev/null +++ b/i2cdevices/ina219.cpp @@ -0,0 +1,187 @@ +#include "ina219.h" + +#include +#include +#include +#include +#include + +#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); +} diff --git a/i2cdevices/ina219.h b/i2cdevices/ina219.h new file mode 100644 index 00000000..446529db --- /dev/null +++ b/i2cdevices/ina219.h @@ -0,0 +1,64 @@ +#ifndef INA219_H +#define INA219_H + +#include +#include + +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 diff --git a/i2cdevices/integrationplugini2cdevices.cpp b/i2cdevices/integrationplugini2cdevices.cpp index e3f3b0b6..ee8eb5ab 100644 --- a/i2cdevices/integrationplugini2cdevices.cpp +++ b/i2cdevices/integrationplugini2cdevices.cpp @@ -33,10 +33,12 @@ #include "pi16adcchannel.h" #include "ads1115channel.h" +#include "ina219.h" #include #include +#include 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) diff --git a/i2cdevices/integrationplugini2cdevices.h b/i2cdevices/integrationplugini2cdevices.h index b8d8ca06..0540c881 100644 --- a/i2cdevices/integrationplugini2cdevices.h +++ b/i2cdevices/integrationplugini2cdevices.h @@ -33,6 +33,8 @@ #include +#include "extern-plugininfo.h" + class I2CDevice; class IntegrationPluginI2CDevices: public IntegrationPlugin diff --git a/i2cdevices/integrationplugini2cdevices.json b/i2cdevices/integrationplugini2cdevices.json index cb68e09a..f5e54fe5 100644 --- a/i2cdevices/integrationplugini2cdevices.json +++ b/i2cdevices/integrationplugini2cdevices.json @@ -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 + } + ] } ] }