From 82cb6a293f53f5847561ca4f7c8574ce168970a1 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Thu, 31 Oct 2024 22:06:32 +0100 Subject: [PATCH 1/7] Update usermods_list.cpp --- wled00/usermods_list.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 36bd122a51..6a5be5e98f 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -182,6 +182,10 @@ #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" #endif +#ifdef USERMOD_INA219 + #include "../usermods/INA219/usermod_ina219.h" +#endif + #if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) // This include of SD.h and SD_MMC.h must happen here, else they won't be // resolved correctly (when included in mod's header only) @@ -470,4 +474,8 @@ void registerUsermods() #ifdef USERMOD_POV_DISPLAY UsermodManager::add(new PovDisplayUsermod()); #endif + + #ifdef USERMOD_INA219 + UsermodManager::add(new UsermodINA219()); + #endif } From c2059a6eed8ba3ccfd9882414168d9879c24002b Mon Sep 17 00:00:00 2001 From: KrX3D Date: Thu, 31 Oct 2024 22:07:58 +0100 Subject: [PATCH 2/7] Update const.h --- wled00/const.h | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/const.h b/wled00/const.h index 07873deca1..0f345de300 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -203,6 +203,7 @@ #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" +#define USERMOD_ID_INA219 55 //Usermod "usermod_ina219.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot From 201c17e539f79be6be00eae86e090afb31b19529 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Thu, 31 Oct 2024 22:08:35 +0100 Subject: [PATCH 3/7] Add INA219 Usermod --- usermods/INA219/Readme.md | 132 +++++ usermods/INA219/img/homeassistant.png | Bin 0 -> 20513 bytes usermods/INA219/img/info.png | Bin 0 -> 48000 bytes usermods/INA219/img/usermod_settings.png | Bin 0 -> 12822 bytes usermods/INA219/usermod_ina219.h | 702 +++++++++++++++++++++++ 5 files changed, 834 insertions(+) create mode 100644 usermods/INA219/Readme.md create mode 100644 usermods/INA219/img/homeassistant.png create mode 100644 usermods/INA219/img/info.png create mode 100644 usermods/INA219/img/usermod_settings.png create mode 100644 usermods/INA219/usermod_ina219.h diff --git a/usermods/INA219/Readme.md b/usermods/INA219/Readme.md new file mode 100644 index 0000000000..727c365321 --- /dev/null +++ b/usermods/INA219/Readme.md @@ -0,0 +1,132 @@ +# INA219 WLED Usermod + +This Usermod integrates the INA219 sensor with WLED to monitor energy consumption. It can read voltage, current, power, and calculate daily, monthly and total energy usage. + +## Features + +- Monitors bus voltage, shunt voltage, current, and power. +- Calculates total energy consumed in kilowatt-hours (kWh). +- Supports MQTT publishing of sensor data. +- Publishes energy data to Home Assistant for easy integration. +- Displays daily, monthly and total energy used in the WLED GUI under the info section. +- Configurable through WLED's web interface. + +## Screenshots + +| Info screen | Settings page | Home Assistant | +|------------------------------------------------|-----------------------------------------------------------------|------------------------------------------------------------| +| ![Info screen](./img/info.png "Info screen") | ![Settings page](./img/usermod_settings.png "Settings page") | ![Settings page](./img/homeassistant.png "Home Assistant") | + +## Configuration Parameters + +| Parameter | Description | Default Value | Possible Values | +|-------------------------------|------------------------------------------------------------|----------------|--------------------------------------------------------| +| `INA219_ENABLED` | Enable or disable the INA219 Usermod | `false` | `true`, `false` | +| `INA219_SDA_PIN` | I2C data pin (SDA) | `8` | See options below for available GPIO pins. | +| `INA219_SCL_PIN` | I2C clock pin (SCL) | `9` | See options below for available GPIO pins. | +| `INA219_I2C_ADDRESS` | I2C address of the INA219 sensor | `0x40` | See options below for available addresses. | +| `INA219_CHECK_INTERVAL` | Interval for checking sensor values (seconds) | `5` | Any positive integer | +| `INA219_CONVERSION_TIME` | ADC conversion time (12-bit, 16-bit, etc.) | `BIT_MODE_12` | See options below for available modes. | +| `INA219_DECIMAL_FACTOR` | Number of decimal places for current and power readings | `3` | See options below for decimal places. | +| `INA219_SHUNT_RESISTOR` | Value of the shunt resistor in ohms | `0.1` | Any positive float value matching your INA219 resistor | +| `INA219_CORRECTION_FACTOR` | Correction factor for measurements | `1.0` | Any positive float value | +| `INA219_MQTT_PUBLISH` | Publish sensor data to MQTT | `false` | `true`, `false` | +| `INA219_MQTT_PUBLISH_ALWAYS` | Always publish values, regardless of change | `false` | `true`, `false` | +| `INA219_HA_DISCOVERY` | Enable Home Assistant discovery for sensors | `false` | `true`, `false` | + + ### Options for `INA219_CONVERSION_TIME` + + The `conversionTime` parameter can be set to the following ADC modes: + + | Value | Description | + |-------------------|----------------------------| + | `BIT_MODE_9` | 9-Bit (84 µs) | + | `BIT_MODE_10` | 10-Bit (148 µs) | + | `BIT_MODE_11` | 11-Bit (276 µs) | + | `BIT_MODE_12` | 12-Bit (532 µs) | + | `SAMPLE_MODE_2` | 2 samples (1.06 ms) | + | `SAMPLE_MODE_4` | 4 samples (2.13 ms) | + | `SAMPLE_MODE_8` | 8 samples (4.26 ms) | + | `SAMPLE_MODE_16` | 16 samples (8.51 ms) | + | `SAMPLE_MODE_32` | 32 samples (17.02 ms) | + | `SAMPLE_MODE_64` | 64 samples (34.05 ms) | + | `SAMPLE_MODE_128` | 128 samples (68.10 ms) | + + ### Options for `INA219_DECIMAL_FACTOR` + + The `decimalFactor` parameter can be set to: + + | Decimal Places | Value | Example | + |----------------|-------|------------------| + | 0 | 0 | 100 | + | 1 | 1 | 100.0 | + | 2 | 2 | 100.00 | + | 3 | 3 | 100.000 | + + ### Options for `INA219_I2C_ADDRESS` + + The `i2cAddress` parameter can be set to the following options: + + | Address | Description | Value | + |---------------------|------------------------------------|---------| + | `0x40` | 0x40 - Default | 0x40 | + | `0x41` | 0x41 - A0 soldered | 0x41 | + | `0x44` | 0x44 - A1 soldered | 0x44 | + | `0x45` | 0x45 - A0 and A1 soldered | 0x45 | + + ### Options for `INA219_SDA_PIN` and `INA219_SCL_PIN` + + The `SDA` and `SCL` pins can be set to any valid and unused GPIO pin, with defaults as follows: + + - **Default SDA Pin**: `8` + - **Default SCL Pin**: `9` + + Example GPIO pin values for configuration: + + | GPIO Pin | Description | + |----------|--------------| + | `8` | Default SDA | + | `9` | Default SCL | + | Other GPIO pins are available, depending on hardware. + +## Usage + +1. Include this usermod in your WLED project by adding `#define USERMOD_INA219` to the `my_config.h` file. + +2. **Dependencies** + These libraries must be added under `lib_deps` in your `platformio.ini` (or `platform_override.ini`): + - `wollewald/INA219_WE@~1.3.8` (by [wollewald](https://github.com/wollewald/INA219_WE)) + - `Wire` + +3. Configure the parameters in the web interface or via the JSON config file. + +4. Monitor your energy consumption through the WLED interface or via MQTT. + +5. Optional to predefine options: + + #define INA219_ENABLED false + #define INA219_SDA_PIN 8 + #define INA219_SCL_PIN 9 + #define INA219_I2C_ADDRESS 0x40 + #define INA219_CHECK_INTERVAL 5 + #define INA219_CONVERSION_TIME BIT_MODE_12 + #define INA219_DECIMAL_FACTOR 3 + #define INA219_SHUNT_RESISTOR 0.1 + #define INA219_CORRECTION_FACTOR 1.0 + #define INA219_MQTT_PUBLISH false + #define INA219_MQTT_PUBLISH_ALWAYS false + #define INA219_HA_DISCOVERY false + +## Energy Calculation + +- **Total Energy** is calculated continuously. +- **Daily Energy** resets after 24 hours. +- **Monthly Energy** resets after 30 days. + +To reset daily or monthly energy calculations, you can implement corresponding functions within your main application. + +## Dependencies + +Ensure you have the required libraries installed: + +- [INA219_WE](https://github.com/wollewald/INA219_WE) diff --git a/usermods/INA219/img/homeassistant.png b/usermods/INA219/img/homeassistant.png new file mode 100644 index 0000000000000000000000000000000000000000..64259fc511216fdb67f39859fcc4e93d577c2051 GIT binary patch literal 20513 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 literal 0 HcmV?d00001 diff --git a/usermods/INA219/img/info.png b/usermods/INA219/img/info.png new file mode 100644 index 0000000000000000000000000000000000000000..82caaa08f6dda4d58c170cb91c34d29037f7efee GIT binary patch literal 48000 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 literal 0 HcmV?d00001 diff --git a/usermods/INA219/usermod_ina219.h b/usermods/INA219/usermod_ina219.h new file mode 100644 index 0000000000..444c3bc716 --- /dev/null +++ b/usermods/INA219/usermod_ina219.h @@ -0,0 +1,702 @@ +#pragma once + +#include "wled.h" +#include +#include + +class UsermodINA219 : public Usermod { +private: + static const char _name[]; // Name of the usermod + + bool initDone = false; // Flag to check if initialization is complete + unsigned long lastCheck = 0; // Timestamp for the last check + + // Configurable settings for the INA219 Usermod + // Enabled setting + #ifdef INA219_ENABLED + bool enabled = INA219_ENABLED; + #else + bool enabled = false; // Default disabled value + #endif + + // I2C Address (default is 0x40 if not defined) + #ifdef INA219_I2C_ADDRESS + uint8_t _i2cAddress = INA219_I2C_ADDRESS; + #else + uint8_t _i2cAddress = 0x40; // Default I2C address + #endif + + // Check Interval (in seconds) + #ifdef INA219_CHECK_INTERVAL + uint16_t _checkInterval = INA219_CHECK_INTERVAL; + uint16_t checkInterval = _checkInterval * 1000; // Convert to milliseconds + #else + uint16_t _checkInterval = 5; // Default 5 seconds + uint16_t checkInterval = _checkInterval * 1000; // Default 5 seconds + #endif + + // Conversion Time + #ifdef INA219_CONVERSION_TIME + INA219_ADC_MODE conversionTime = static_cast(INA219_CONVERSION_TIME); // Cast from int if defined + #else + INA219_ADC_MODE conversionTime = BIT_MODE_12; // Default 12-bit resolution + #endif + + // Decimal factor for current/power readings + #ifdef INA219_DECIMAL_FACTOR + uint8_t _decimalFactor = INA219_DECIMAL_FACTOR; + #else + uint8_t _decimalFactor = 3; // Default 3 decimal places + #endif + + // Shunt Resistor value + #ifdef INA219_SHUNT_RESISTOR + float shuntResistor = INA219_SHUNT_RESISTOR; + #else + float shuntResistor = 0.1; // Default 0.1 ohms + #endif + + // Correction factor + #ifdef INA219_CORRECTION_FACTOR + float correctionFactor = INA219_CORRECTION_FACTOR; + #else + float correctionFactor = 1.0; // Default correction factor + #endif + + // MQTT Publish Settings + #ifdef INA219_MQTT_PUBLISH + bool mqttPublish = INA219_MQTT_PUBLISH; + bool mqttPublishSent = !INA219_MQTT_PUBLISH; + #else + bool mqttPublish = false; // Default: false (do not publish to MQTT) + bool mqttPublishSent = true; + #endif + + #ifdef INA219_MQTT_PUBLISH_ALWAYS + bool mqttPublishAlways = INA219_MQTT_PUBLISH_ALWAYS; + #else + bool mqttPublishAlways = false; // Default: false (only publish changes) + #endif + + #ifdef INA219_HA_DISCOVERY + bool haDiscovery = INA219_HA_DISCOVERY; + bool haDiscoverySent = !INA219_HA_DISCOVERY; + #else + bool haDiscovery = false; // Default: false (Home Assistant discovery disabled) + bool haDiscoverySent = true; + #endif + + // I2C SDA and SCL pins (default SDA = 8, SCL = 9 if not defined) + #ifdef INA219_SDA_PIN + uint8_t _sdaPin = INA219_SDA_PIN; + #else + uint8_t _sdaPin = 8; // Default SDA pin + #endif + + #ifdef INA219_SCL_PIN + uint8_t _sclPin = INA219_SCL_PIN; + #else + uint8_t _sclPin = 9; // Default SCL pin + #endif + + // Variables to store sensor readings + float busVoltage = 0; + float current = 0; + float current_mA = 0; + float power = 0; + float power_mW = 0; + float shuntVoltage = 0; + float loadVoltage = 0; + bool overflow = false; + + //Last sent variables + float last_sent_shuntVoltage = 0; + float last_sent_busVoltage = 0; + float last_sent_loadVoltage = 0; + float last_sent_current = 0; + float last_sent_current_mA = 0; + float last_sent_power = 0; + float last_sent_power_mW = 0; + bool last_sent_overflow = false; + + float totalEnergy_kWh = 0.0; // Total energy in kWh + float dailyEnergy_kWh = 0.0; // Daily energy in kWh + float monthlyEnergy_kWh = 0.0; // Monthly energy in kWh + unsigned long lastPublishTime = 0; // Track the last publish time + + // Variables to store last reset timestamps + unsigned long dailyResetTime = 0; // Reset time in seconds + unsigned long monthlyResetTime = 0; // Reset time in seconds + + // Variables to track last sent readings + float _lastCurrentSent = 0; + float _lastVoltageSent = 0; + float _lastPowerSent = 0; + float _lastShuntVoltageSent = 0; + + INA219_WE *_ina219 = nullptr; // INA219 sensor object + + // Function to truncate decimals based on the configured decimal factor + float truncateDecimals(float val) { + // If _decimalFactor is 0, round to the nearest whole number + if (_decimalFactor == 0) { + return roundf(val); + } + // For decimal factors 1 and above, round to the appropriate number of decimal places + float factor = pow(10, _decimalFactor); + return roundf(val * factor) / factor; + } + + // Function to update INA219 settings + void updateINA219Settings() { + // End current I2C if already initialized + Wire.end(); + + // Reinitialize I2C with the potentially updated SDA and SCL pins + Wire.begin(_sdaPin, _sclPin); + + // Reinitialize the INA219 instance with updated settings + if (_ina219 != nullptr) { + delete _ina219; + } + _ina219 = new INA219_WE(_i2cAddress); + if (!_ina219->init()) { + DEBUG_PRINTLN(F("INA219 initialization failed!")); + return; + } + _ina219->setShuntSizeInOhms(shuntResistor); + _ina219->setADCMode(conversionTime); + _ina219->setCorrectionFactor(correctionFactor); + } + +public: + // Destructor to clean up the INA219 object + ~UsermodINA219() { + delete _ina219; + _ina219 = nullptr; + } + + // ADC mode enumeration + enum class ADCMode { + BIT_MODE_9 = 0, + BIT_MODE_10 = 1, + BIT_MODE_11 = 2, + BIT_MODE_12 = 3, + SAMPLE_MODE_2 = 9, + SAMPLE_MODE_4 = 10, + SAMPLE_MODE_8 = 11, + SAMPLE_MODE_16 = 12, + SAMPLE_MODE_32 = 13, + SAMPLE_MODE_64 = 14, + SAMPLE_MODE_128 = 15 + }; + + // Setup function called once on boot or restart + void setup() override { + updateINA219Settings(); // Configure INA219 settings + initDone = true; // Mark initialization as complete + } + + // Loop function called continuously + void loop() override { + // Check if the usermod is enabled and the check interval has elapsed + if (enabled && millis() - lastCheck > checkInterval) { + lastCheck = millis(); // Update last check timestamp + + // Read sensor values + shuntVoltage = truncateDecimals(_ina219->getShuntVoltage_mV()); + busVoltage = truncateDecimals(_ina219->getBusVoltage_V()); + current_mA = truncateDecimals(_ina219->getCurrent_mA()); + current = truncateDecimals(_ina219->getCurrent_mA() / 1000.0); // Convert from mA to A + power_mW = truncateDecimals(_ina219->getBusPower()); + power = truncateDecimals(_ina219->getBusPower() / 1000.0); // Convert from mW to W + loadVoltage = truncateDecimals(busVoltage + (shuntVoltage / 1000)); + overflow = truncateDecimals(_ina219->getOverflow()); + + // Update energy values based on power for this duration + updateEnergy(power, lastCheck - lastPublishTime); + lastPublishTime = lastCheck; // Update last publish time + + #ifndef WLED_DISABLE_MQTT + // Publish to MQTT if enabled + if (mqttPublish) { + if (mqttPublishAlways || hasValueChanged()) { + publishMqtt(shuntVoltage, busVoltage, loadVoltage, current, current_mA, power, power_mW, overflow); + + last_sent_shuntVoltage = shuntVoltage; + last_sent_busVoltage = busVoltage; + last_sent_loadVoltage = loadVoltage; + last_sent_current = current; + last_sent_current_mA = current_mA; + last_sent_power = power; + last_sent_power_mW = power_mW; + last_sent_overflow = overflow; + + mqttPublishSent = true; + } + } else if (!mqttPublish && mqttPublishSent) { + char sensorTopic[128]; + snprintf_P(sensorTopic, 127, "%s/sensor/ina219", mqttDeviceTopic); // Discovery config topic for each sensor + + // Publish an empty message with retain to delete the sensor from Home Assistant + mqtt->publish(sensorTopic, 0, true, ""); + + mqttPublishSent = false; + } + + // Optionally publish to Home Assistant via MQTT discovery + if (haDiscovery && !haDiscoverySent) { + char topic[128]; + snprintf_P(topic, 127, "%s/sensor/ina219", mqttDeviceTopic); // Common topic for all INA219 data + + mqttCreateHassSensor(F("Current"), topic, F("current"), F("A"), F("current"), F("sensor")); + mqttCreateHassSensor(F("Voltage"), topic, F("voltage"), F("V"), F("bus_voltage_V"), F("sensor")); + mqttCreateHassSensor(F("Power"), topic, F("power"), F("W"), F("power"), F("sensor")); + mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("mV"), F("shunt_voltage_mV"), F("sensor")); + mqttCreateHassSensor(F("Shunt Resistor"), topic, F(""), F("Ω"), F("shunt_resistor_Ohms"), F("sensor")); + mqttCreateHassSensor(F("Overflow"), topic, F(""), F(""), F("overflow"), F("binary_sensor")); + mqttCreateHassSensor(F("Total Energy"), topic, F("energy"), F("kWh"), F("total_energy_kWh"), F("sensor")); + mqttCreateHassSensor(F("Daily Energy"), topic, F("energy"), F("kWh"), F("daily_energy_kWh"), F("sensor")); + mqttCreateHassSensor(F("Monthly Energy"), topic, F("energy"), F("kWh"), F("monthly_energy_kWh"), F("sensor")); + + // Mark as sent to avoid repeating + haDiscoverySent = true; + } else if (!haDiscovery && haDiscoverySent) { + // Remove previously created sensors + mqttRemoveHassSensor(F("Current"), F("sensor")); + mqttRemoveHassSensor(F("Voltage"), F("sensor")); + mqttRemoveHassSensor(F("Power"), F("sensor")); + mqttRemoveHassSensor(F("Shunt-Voltage"), F("sensor")); + mqttRemoveHassSensor(F("Total-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Daily-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Monthly-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Shunt-Resistor"), F("sensor")); + mqttRemoveHassSensor(F("Overflow"), F("binary_sensor")); + + // Mark as sent to avoid repeating + haDiscoverySent = false; + } + #endif + } + } + + bool hasSignificantChange(float oldValue, float newValue, float threshold = 0.01f) { + return fabsf(oldValue - newValue) > threshold; + } + + bool hasValueChanged() { + return hasSignificantChange(last_sent_shuntVoltage, shuntVoltage) || + hasSignificantChange(last_sent_busVoltage, busVoltage) || + hasSignificantChange(last_sent_loadVoltage, loadVoltage) || + hasSignificantChange(last_sent_current, current) || + hasSignificantChange(last_sent_current_mA, current_mA) || + hasSignificantChange(last_sent_power, power) || + hasSignificantChange(last_sent_power_mW, power_mW) || + (last_sent_overflow != overflow); + } + +#ifndef WLED_DISABLE_MQTT + /** + * Function to publish sensor data to MQTT + */ + bool onMqttMessage(char* topic, char* payload) override { + // Check if the message is for the correct topic + if (strcmp_P(topic, PSTR("/sensor/ina219")) == 0) { + StaticJsonDocument<512> jsonDoc; + + // Parse the JSON payload + DeserializationError error = deserializeJson(jsonDoc, payload); + if (error) { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(error.f_str()); + return false; + } + + // Update the energy values + totalEnergy_kWh = jsonDoc["total_energy_kWh"]; + dailyEnergy_kWh = jsonDoc["daily_energy_kWh"]; + monthlyEnergy_kWh = jsonDoc["monthly_energy_kWh"]; + dailyResetTime = jsonDoc["dailyResetTime"]; + monthlyResetTime = jsonDoc["monthlyResetTime"]; + + return true; + } + + return false; + } + + /** + * Subscribe to MQTT topic for controlling usermod + */ + void onMqttConnect(bool sessionPresent) override { + char subuf[64]; + if (mqttDeviceTopic[0] != 0) { + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/sensor/ina219")); + mqtt->subscribe(subuf, 0); + } + } + #endif + + /** + * Function to publish INA219 sensor data to MQTT + */ + void publishMqtt(float shuntVoltage, float busVoltage, float loadVoltage, + float current, float current_mA, float power, + float power_mW, bool overflow) { + // Publish to MQTT only if the WLED MQTT feature is enabled + #ifndef WLED_DISABLE_MQTT + + // Create a JSON document to hold sensor data + StaticJsonDocument<512> jsonDoc; + + // Populate the JSON document with sensor readings + jsonDoc["shunt_voltage_mV"] = shuntVoltage; // Shunt voltage in millivolts + jsonDoc["bus_voltage_V"] = busVoltage; // Bus voltage in volts + jsonDoc["load_voltage_V"] = loadVoltage; // Load voltage in volts + jsonDoc["current"] = current; // Current in unspecified units + jsonDoc["current_mA"] = current_mA; // Current in milliamperes + jsonDoc["power"] = power; // Power in unspecified units + jsonDoc["power_mW"] = power_mW; // Power in milliwatts + jsonDoc["overflow"] = overflow; // Overflow status (true/false) + //jsonDoc["overflow"] = overflow ? "on" : "off"; + jsonDoc["shunt_resistor_Ohms"] = shuntResistor; // Shunt resistor value in Ohms + + // Energy calculations + jsonDoc["total_energy_kWh"] = totalEnergy_kWh; // Total energy in kilowatt-hours + jsonDoc["daily_energy_kWh"] = dailyEnergy_kWh; // Daily energy in kilowatt-hours + jsonDoc["monthly_energy_kWh"] = monthlyEnergy_kWh; // Monthly energy in kilowatt-hours + + // Reset timestamps + jsonDoc["dailyResetTime"] = dailyResetTime; // Timestamp of the last daily reset + jsonDoc["monthlyResetTime"] = monthlyResetTime; // Timestamp of the last monthly reset + + // Serialize the JSON document into a character buffer + char buffer[512]; + serializeJson(jsonDoc, buffer); + + // Construct the MQTT topic using the device topic + char topic[128]; + snprintf_P(topic, sizeof(topic), "%s/sensor/ina219", mqttDeviceTopic); + + // Publish the serialized JSON data to the specified MQTT topic + mqtt->publish(topic, 0, true, buffer); + #endif + } + + /** + * Function to create Home Assistant sensor configuration + */ + void mqttCreateHassSensor(const String &name, const String &topic, + const String &deviceClass, const String &unitOfMeasurement, + const String &jsonKey, const String &SensorType) { + // Sanitize the name by replacing spaces with hyphens + String sanitizedName = name; + sanitizedName.replace(' ', '-'); + + String sanitizedMqttClientID = sanitizeMqttClientID(mqttClientID); + + // Construct the Home Assistant configuration topic + String t = String(F("homeassistant/")) + SensorType + "/" + sanitizedMqttClientID + "/" + sanitizedName + F("/config"); + + // Create a JSON document for the sensor configuration + StaticJsonDocument<600> doc; + + // Populate the JSON document with sensor configuration details + doc[F("name")] = name; // Sensor name + doc[F("state_topic")] = topic; // Topic for sensor state + doc[F("unique_id")] = String(sanitizedMqttClientID) + "-" + sanitizedName; // Unique ID for the sensor + + // Template to extract specific value from JSON + doc[F("value_template")] = String("{{ value_json.") + jsonKey + String(" }}"); + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; // Optional unit of measurement + if (deviceClass != "") + doc[F("device_class")] = deviceClass; // Optional device class + if (SensorType != "binary_sensor") + doc[F("expire_after")] = 1800; // Expiration time for non-binary sensors + + // Device details nested object + JsonObject device = doc.createNestedObject(F("device")); + device[F("name")] = serverDescription; // Server description as device name + device[F("identifiers")] = "wled-sensor-" + String(sanitizedMqttClientID); // Unique identifier for the device + device[F("manufacturer")] = F(WLED_BRAND); // Manufacturer name + device[F("model")] = F(WLED_PRODUCT_NAME); // Product model name + device[F("sw_version")] = versionString; // Software version + + // Serialize the JSON document into a temporary string + String temp; + serializeJson(doc, temp); + + // Debug output for the Home Assistant topic and configuration + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + // Publish the sensor configuration to Home Assistant + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + void mqttRemoveHassSensor(const String &name, const String &SensorType) { + char sensorTopic[128]; + snprintf_P(sensorTopic, 127, "homeassistant/%s/%s/%s/config", SensorType.c_str(), sanitizeMqttClientID(mqttClientID).c_str(), name.c_str()); // Discovery config topic for each sensor + + // Publish an empty message with retain to delete the sensor from Home Assistant + mqtt->publish(sensorTopic, 0, true, ""); + } + + // Function to sanitize the mqttClientID with nicer replacements + String sanitizeMqttClientID(const String &clientID) { + String sanitizedID; + + // Loop through the string + for (unsigned int i = 0; i < clientID.length(); i++) { + char c = clientID[i]; // Get the character directly + + // Handle specific cases for accented letters using byte values + if (c == '\xC3' && i + 1 < clientID.length()) { + if (clientID[i + 1] == '\xBC') { // ü + //sanitizedID += "ue"; // Replace ü with ue + sanitizedID += "u"; // Replace ü with ue + i++; // Skip the next byte + } else if (clientID[i + 1] == '\x9C') { // Ü + //sanitizedID += "Ue"; // Replace Ü with Ue + sanitizedID += "U"; // Replace Ü with Ue + i++; // Skip the next byte + } else if (clientID[i + 1] == '\xA4') { // ä + //sanitizedID += "ae"; // Replace ä with ae + sanitizedID += "a"; // Replace ä with ae + i++; // Skip the next byte + } else if (clientID[i + 1] == '\xC4') { // Ä + //sanitizedID += "Ae"; // Replace Ä with Ae + sanitizedID += "A"; // Replace Ä with Ae + i++; // Skip the next byte + } else if (clientID[i + 1] == '\xB6') { // ö + //sanitizedID += "oe"; // Replace ö with oe + sanitizedID += "o"; // Replace ö with oe + i++; // Skip the next byte + } else if (clientID[i + 1] == '\xD6') { // Ö + //sanitizedID += "Oe"; // Replace Ö with Oe + sanitizedID += "O"; // Replace Ö with Oe + i++; // Skip the next byte + } else if (clientID[i + 1] == '\x9F') { // ß + //sanitizedID += "ss"; // Replace ß with ss + sanitizedID += "s"; // Replace ß with ss + i++; // Skip the next byte + } + } + // Allow valid characters [a-zA-Z0-9_-] + else if ((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_') { + sanitizedID += c; // Directly append valid characters + } + // Replace any other invalid character with an underscore + else { + sanitizedID += '_'; // Replace invalid character with underscore + } + } + return sanitizedID; // Return the sanitized client ID + } + + /** + * Function to update energy calculations based on power and duration + */ + void updateEnergy(float power, unsigned long durationMs) { + // Convert duration from milliseconds to hours + float durationHours = durationMs / 3600000.0; + + // Convert power from watts to kilowatt-hours (kWh) + float energy_kWh = (power / 1000.0) * durationHours; + + // Update total energy consumed + totalEnergy_kWh += energy_kWh; + + // Update daily energy consumption + if (dailyResetTime >= 86400) { // 86400 seconds = 24 hours + dailyEnergy_kWh = 0; // Reset daily energy to zero + dailyResetTime = 0; // Reset daily reset time to zero + } + dailyEnergy_kWh += energy_kWh; // Add to daily energy + dailyResetTime += durationMs / 1000; // Increment daily reset time in seconds + + // Update monthly energy consumption + if (monthlyResetTime >= 2592000) { // 2592000 seconds = 30 days + monthlyEnergy_kWh = 0; // Reset monthly energy to zero + monthlyResetTime = 0; // Reset monthly reset time to zero + } + monthlyEnergy_kWh += energy_kWh; // Add to monthly energy + monthlyResetTime += durationMs / 1000; // Increment monthly reset time in seconds + } + + /** + * Function to add energy consumption data to a JSON object for reporting + */ + void addToJsonInfo(JsonObject &root) { + JsonObject user = root[F("u")]; + if (user.isNull()) { + user = root.createNestedObject(F("u")); // Create a nested object for user data + } + + // Create a nested array for energy data + JsonArray energy_json = user.createNestedArray(F("Energy Consumption")); + + if (!enabled) { + energy_json.add(F("disabled")); // Indicate that the module is disabled + } else { + // Create a nested array for total energy + JsonArray totalEnergy_json = user.createNestedArray(F("Total Energy")); + totalEnergy_json.add(totalEnergy_kWh); // Add total energy in kWh + totalEnergy_json.add(F("kWh")); // Add unit of measurement + + // Create a nested array for daily energy + JsonArray dailyEnergy_json = user.createNestedArray(F("Daily Energy")); + dailyEnergy_json.add(dailyEnergy_kWh); // Add daily energy in kWh + dailyEnergy_json.add(F("kWh")); // Add unit of measurement + + // Create a nested array for monthly energy + JsonArray monthlyEnergy_json = user.createNestedArray(F("Monthly Energy")); + monthlyEnergy_json.add(monthlyEnergy_kWh); // Add monthly energy in kWh + monthlyEnergy_json.add(F("kWh")); // Add unit of measurement + } + } + + /** + * Function to add the current state of energy consumption to a JSON object + */ + void addToJsonState(JsonObject& root) override { + if (!initDone) return; // Prevent crashes on boot if initialization is not done + + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) { + usermod = root.createNestedObject(FPSTR(_name)); // Create nested object for the usermod + } + + // Add energy consumption data to the usermod JSON object + usermod["totalEnergy_kWh"] = totalEnergy_kWh; + usermod["dailyEnergy_kWh"] = dailyEnergy_kWh; + usermod["monthlyEnergy_kWh"] = monthlyEnergy_kWh; + usermod["dailyResetTime"] = dailyResetTime; + usermod["monthlyResetTime"] = monthlyResetTime; + } + + /** + * Function to read energy consumption data from a JSON object + */ + void readFromJsonState(JsonObject& root) override { + if (!initDone) return; // Prevent crashes on boot if initialization is not done + + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + // Read values from JSON or retain existing values if not present + totalEnergy_kWh = usermod["totalEnergy_kWh"] | totalEnergy_kWh; + dailyEnergy_kWh = usermod["dailyEnergy_kWh"] | dailyEnergy_kWh; + monthlyEnergy_kWh = usermod["monthlyEnergy_kWh"] | monthlyEnergy_kWh; + dailyResetTime = usermod["dailyResetTime"] | dailyResetTime; + monthlyResetTime = usermod["monthlyResetTime"] | monthlyResetTime; + } + } + + /** + * Function to handle settings in the Usermod menu + */ + void addToConfig(JsonObject& root) override { + JsonObject top = root.createNestedObject(F("INA219")); // Create nested object for INA219 settings + top["Enabled"] = enabled; // Store enabled status + top["sda_pin"] = _sdaPin; // Store selected SDA pin + top["scl_pin"] = _sclPin; // Store selected SCL pin + top["i2c_address"] = static_cast(_i2cAddress); // Store I2C address + top["check_interval"] = checkInterval / 1000; // Store check interval in seconds + top["conversion_time"] = conversionTime; // Store conversion time + top["decimals"] = _decimalFactor; // Store decimal factor + top["shunt_resistor"] = shuntResistor; // Store shunt resistor value + + #ifndef WLED_DISABLE_MQTT + // Store MQTT settings if MQTT is not disabled + top["mqtt_publish"] = mqttPublish; + top["mqtt_publish_always"] = mqttPublishAlways; + top["ha_discovery"] = haDiscovery; + #endif + } + + /** + * Function to append configuration options to the Usermod menu + */ + void appendConfigData() override { + // Append the dropdown for I2C address selection + oappend(F("dd=addDropdown('INA219','i2c_address');")); + oappend(F("addOption(dd,'0x40 - Default',0x40, true);")); // Default option + oappend(F("addOption(dd,'0x41 - A0 soldered',0x41);")); + oappend(F("addOption(dd,'0x44 - A1 soldered',0x44);")); + oappend(F("addOption(dd,'0x45 - A0 and A1 soldered',0x45);")); + + // Append the dropdown for ADC mode (resolution + samples) + oappend(F("ct=addDropdown('INA219','conversion_time');")); + oappend("addOption(ct,'9-Bit (84 µs)',0);"); + oappend("addOption(ct,'10-Bit (148 µs)',1);"); + oappend("addOption(ct,'11-Bit (276 µs)',2);"); + oappend("addOption(ct,'12-Bit (532 µs)',3, true);"); // Default option + oappend("addOption(ct,'2 samples (1.06 ms)',9);"); + oappend("addOption(ct,'4 samples (2.13 ms)',10);"); + oappend("addOption(ct,'8 samples (4.26 ms)',11);"); + oappend("addOption(ct,'16 samples (8.51 ms)',12);"); + oappend("addOption(ct,'32 samples (17.02 ms)',13);"); + oappend("addOption(ct,'64 samples (34.05 ms)',14);"); + oappend("addOption(ct,'128 samples (68.10 ms)',15);"); + + // Append the dropdown for decimal precision (0 to 10) + oappend(F("df=addDropdown('INA219','decimals');")); + for (int i = 0; i <= 3; i++) { + oappend(String("addOption(df,'" + String(i) + "'," + String(i) + (i == 2 ? ", true);" : ");")).c_str()); // Default to 2 decimals + } + } + + /** + * Function to read settings from the Usermod menu configuration + */ + bool readFromConfig(JsonObject& root) override { + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); // Check if the configuration exists + + // Read configuration values and update local variables + configComplete &= getJsonValue(top["Enabled"], enabled); + configComplete &= getJsonValue(top["sda_pin"], _sdaPin); // Read selected SDA pin + configComplete &= getJsonValue(top["scl_pin"], _sclPin); // Read selected SCL pin + configComplete &= getJsonValue(top[F("i2c_address")], _i2cAddress); + + // Read check interval and convert to milliseconds if necessary + if (getJsonValue(top[F("check_interval")], checkInterval)) { + if (1 <= checkInterval && checkInterval <= 600) + checkInterval *= 1000; // Convert seconds to milliseconds + else + checkInterval = _checkInterval * 1000; // Fallback to defined value + } else { + configComplete = false; // Configuration is incomplete + } + + // Read other configuration values + configComplete &= getJsonValue(top["conversion_time"], conversionTime); + configComplete &= getJsonValue(top["decimals"], _decimalFactor); + configComplete &= getJsonValue(top["shunt_resistor"], shuntResistor); + + #ifndef WLED_DISABLE_MQTT + configComplete &= getJsonValue(top["mqtt_publish"], mqttPublish); + configComplete &= getJsonValue(top["mqtt_publish_always"], mqttPublishAlways); + configComplete &= getJsonValue(top["ha_discovery"], haDiscovery); + #endif + + updateINA219Settings(); // Apply any updated settings to the INA219 + + return configComplete; // Return whether the configuration was complete + } + + /** + * Function to get the unique identifier for this usermod + */ + uint16_t getId() override { + return USERMOD_ID_INA219; // Return the unique identifier for the INA219 usermod + } +}; + +const char UsermodINA219::_name[] PROGMEM = "INA219"; From 9e4852d26634af8e7dc2ddb02250ae2c4cca9151 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Thu, 31 Oct 2024 22:23:32 +0100 Subject: [PATCH 4/7] Update usermod_ina219.h --- usermods/INA219/usermod_ina219.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/usermods/INA219/usermod_ina219.h b/usermods/INA219/usermod_ina219.h index 444c3bc716..113922d480 100644 --- a/usermods/INA219/usermod_ina219.h +++ b/usermods/INA219/usermod_ina219.h @@ -119,9 +119,9 @@ class UsermodINA219 : public Usermod { float last_sent_power_mW = 0; bool last_sent_overflow = false; - float totalEnergy_kWh = 0.0; // Total energy in kWh float dailyEnergy_kWh = 0.0; // Daily energy in kWh float monthlyEnergy_kWh = 0.0; // Monthly energy in kWh + float totalEnergy_kWh = 0.0; // Total energy in kWh unsigned long lastPublishTime = 0; // Track the last publish time // Variables to store last reset timestamps @@ -255,9 +255,9 @@ class UsermodINA219 : public Usermod { mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("mV"), F("shunt_voltage_mV"), F("sensor")); mqttCreateHassSensor(F("Shunt Resistor"), topic, F(""), F("Ω"), F("shunt_resistor_Ohms"), F("sensor")); mqttCreateHassSensor(F("Overflow"), topic, F(""), F(""), F("overflow"), F("binary_sensor")); - mqttCreateHassSensor(F("Total Energy"), topic, F("energy"), F("kWh"), F("total_energy_kWh"), F("sensor")); mqttCreateHassSensor(F("Daily Energy"), topic, F("energy"), F("kWh"), F("daily_energy_kWh"), F("sensor")); mqttCreateHassSensor(F("Monthly Energy"), topic, F("energy"), F("kWh"), F("monthly_energy_kWh"), F("sensor")); + mqttCreateHassSensor(F("Total Energy"), topic, F("energy"), F("kWh"), F("total_energy_kWh"), F("sensor")); // Mark as sent to avoid repeating haDiscoverySent = true; @@ -267,9 +267,9 @@ class UsermodINA219 : public Usermod { mqttRemoveHassSensor(F("Voltage"), F("sensor")); mqttRemoveHassSensor(F("Power"), F("sensor")); mqttRemoveHassSensor(F("Shunt-Voltage"), F("sensor")); - mqttRemoveHassSensor(F("Total-Energy"), F("sensor")); mqttRemoveHassSensor(F("Daily-Energy"), F("sensor")); mqttRemoveHassSensor(F("Monthly-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Total-Energy"), F("sensor")); mqttRemoveHassSensor(F("Shunt-Resistor"), F("sensor")); mqttRemoveHassSensor(F("Overflow"), F("binary_sensor")); @@ -313,9 +313,9 @@ class UsermodINA219 : public Usermod { } // Update the energy values - totalEnergy_kWh = jsonDoc["total_energy_kWh"]; dailyEnergy_kWh = jsonDoc["daily_energy_kWh"]; monthlyEnergy_kWh = jsonDoc["monthly_energy_kWh"]; + totalEnergy_kWh = jsonDoc["total_energy_kWh"]; dailyResetTime = jsonDoc["dailyResetTime"]; monthlyResetTime = jsonDoc["monthlyResetTime"]; @@ -363,9 +363,9 @@ class UsermodINA219 : public Usermod { jsonDoc["shunt_resistor_Ohms"] = shuntResistor; // Shunt resistor value in Ohms // Energy calculations - jsonDoc["total_energy_kWh"] = totalEnergy_kWh; // Total energy in kilowatt-hours jsonDoc["daily_energy_kWh"] = dailyEnergy_kWh; // Daily energy in kilowatt-hours jsonDoc["monthly_energy_kWh"] = monthlyEnergy_kWh; // Monthly energy in kilowatt-hours + jsonDoc["total_energy_kWh"] = totalEnergy_kWh; // Total energy in kilowatt-hours // Reset timestamps jsonDoc["dailyResetTime"] = dailyResetTime; // Timestamp of the last daily reset @@ -543,12 +543,7 @@ class UsermodINA219 : public Usermod { if (!enabled) { energy_json.add(F("disabled")); // Indicate that the module is disabled - } else { - // Create a nested array for total energy - JsonArray totalEnergy_json = user.createNestedArray(F("Total Energy")); - totalEnergy_json.add(totalEnergy_kWh); // Add total energy in kWh - totalEnergy_json.add(F("kWh")); // Add unit of measurement - + } else { // Create a nested array for daily energy JsonArray dailyEnergy_json = user.createNestedArray(F("Daily Energy")); dailyEnergy_json.add(dailyEnergy_kWh); // Add daily energy in kWh @@ -558,6 +553,11 @@ class UsermodINA219 : public Usermod { JsonArray monthlyEnergy_json = user.createNestedArray(F("Monthly Energy")); monthlyEnergy_json.add(monthlyEnergy_kWh); // Add monthly energy in kWh monthlyEnergy_json.add(F("kWh")); // Add unit of measurement + + // Create a nested array for total energy + JsonArray totalEnergy_json = user.createNestedArray(F("Total Energy")); + totalEnergy_json.add(totalEnergy_kWh); // Add total energy in kWh + totalEnergy_json.add(F("kWh")); // Add unit of measurement } } From 378358cda4ba895ee38526fa161a3ae04b941529 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Fri, 1 Nov 2024 19:13:56 +0100 Subject: [PATCH 5/7] Update usermod_ina219.h Formatting --- usermods/INA219/usermod_ina219.h | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/usermods/INA219/usermod_ina219.h b/usermods/INA219/usermod_ina219.h index 113922d480..b441d6281a 100644 --- a/usermods/INA219/usermod_ina219.h +++ b/usermods/INA219/usermod_ina219.h @@ -14,89 +14,89 @@ class UsermodINA219 : public Usermod { // Configurable settings for the INA219 Usermod // Enabled setting #ifdef INA219_ENABLED - bool enabled = INA219_ENABLED; + bool enabled = INA219_ENABLED; #else - bool enabled = false; // Default disabled value + bool enabled = false; // Default disabled value #endif // I2C Address (default is 0x40 if not defined) #ifdef INA219_I2C_ADDRESS - uint8_t _i2cAddress = INA219_I2C_ADDRESS; + uint8_t _i2cAddress = INA219_I2C_ADDRESS; #else - uint8_t _i2cAddress = 0x40; // Default I2C address + uint8_t _i2cAddress = 0x40; // Default I2C address #endif // Check Interval (in seconds) #ifdef INA219_CHECK_INTERVAL - uint16_t _checkInterval = INA219_CHECK_INTERVAL; - uint16_t checkInterval = _checkInterval * 1000; // Convert to milliseconds + uint16_t _checkInterval = INA219_CHECK_INTERVAL; + uint16_t checkInterval = _checkInterval * 1000; // Convert to milliseconds #else - uint16_t _checkInterval = 5; // Default 5 seconds - uint16_t checkInterval = _checkInterval * 1000; // Default 5 seconds + uint16_t _checkInterval = 5; // Default 5 seconds + uint16_t checkInterval = _checkInterval * 1000; // Default 5 seconds #endif // Conversion Time #ifdef INA219_CONVERSION_TIME - INA219_ADC_MODE conversionTime = static_cast(INA219_CONVERSION_TIME); // Cast from int if defined + INA219_ADC_MODE conversionTime = static_cast(INA219_CONVERSION_TIME); // Cast from int if defined #else - INA219_ADC_MODE conversionTime = BIT_MODE_12; // Default 12-bit resolution + INA219_ADC_MODE conversionTime = BIT_MODE_12; // Default 12-bit resolution #endif // Decimal factor for current/power readings #ifdef INA219_DECIMAL_FACTOR - uint8_t _decimalFactor = INA219_DECIMAL_FACTOR; + uint8_t _decimalFactor = INA219_DECIMAL_FACTOR; #else - uint8_t _decimalFactor = 3; // Default 3 decimal places + uint8_t _decimalFactor = 3; // Default 3 decimal places #endif // Shunt Resistor value #ifdef INA219_SHUNT_RESISTOR - float shuntResistor = INA219_SHUNT_RESISTOR; + float shuntResistor = INA219_SHUNT_RESISTOR; #else - float shuntResistor = 0.1; // Default 0.1 ohms + float shuntResistor = 0.1; // Default 0.1 ohms #endif // Correction factor #ifdef INA219_CORRECTION_FACTOR - float correctionFactor = INA219_CORRECTION_FACTOR; + float correctionFactor = INA219_CORRECTION_FACTOR; #else - float correctionFactor = 1.0; // Default correction factor + float correctionFactor = 1.0; // Default correction factor #endif // MQTT Publish Settings #ifdef INA219_MQTT_PUBLISH - bool mqttPublish = INA219_MQTT_PUBLISH; + bool mqttPublish = INA219_MQTT_PUBLISH; bool mqttPublishSent = !INA219_MQTT_PUBLISH; #else - bool mqttPublish = false; // Default: false (do not publish to MQTT) + bool mqttPublish = false; // Default: false (do not publish to MQTT) bool mqttPublishSent = true; #endif #ifdef INA219_MQTT_PUBLISH_ALWAYS - bool mqttPublishAlways = INA219_MQTT_PUBLISH_ALWAYS; + bool mqttPublishAlways = INA219_MQTT_PUBLISH_ALWAYS; #else - bool mqttPublishAlways = false; // Default: false (only publish changes) + bool mqttPublishAlways = false; // Default: false (only publish changes) #endif #ifdef INA219_HA_DISCOVERY - bool haDiscovery = INA219_HA_DISCOVERY; + bool haDiscovery = INA219_HA_DISCOVERY; bool haDiscoverySent = !INA219_HA_DISCOVERY; #else - bool haDiscovery = false; // Default: false (Home Assistant discovery disabled) + bool haDiscovery = false; // Default: false (Home Assistant discovery disabled) bool haDiscoverySent = true; #endif // I2C SDA and SCL pins (default SDA = 8, SCL = 9 if not defined) #ifdef INA219_SDA_PIN - uint8_t _sdaPin = INA219_SDA_PIN; + uint8_t _sdaPin = INA219_SDA_PIN; #else - uint8_t _sdaPin = 8; // Default SDA pin + uint8_t _sdaPin = 8; // Default SDA pin #endif #ifdef INA219_SCL_PIN - uint8_t _sclPin = INA219_SCL_PIN; + uint8_t _sclPin = INA219_SCL_PIN; #else - uint8_t _sclPin = 9; // Default SCL pin + uint8_t _sclPin = 9; // Default SCL pin #endif // Variables to store sensor readings From 10b25f295c72e5dec4e3475ef90ada695eef35a1 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Fri, 15 Nov 2024 01:50:01 +0100 Subject: [PATCH 6/7] Update usermod_ina219.h changed uint8_t to int8_t for _sdaPin and _sclPin ---> to be able to pick "unused" in pin selection which is -1, --- usermods/INA219/usermod_ina219.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/usermods/INA219/usermod_ina219.h b/usermods/INA219/usermod_ina219.h index b441d6281a..2b525e820d 100644 --- a/usermods/INA219/usermod_ina219.h +++ b/usermods/INA219/usermod_ina219.h @@ -88,15 +88,15 @@ class UsermodINA219 : public Usermod { // I2C SDA and SCL pins (default SDA = 8, SCL = 9 if not defined) #ifdef INA219_SDA_PIN - uint8_t _sdaPin = INA219_SDA_PIN; + int8_t _sdaPin = INA219_SDA_PIN; #else - uint8_t _sdaPin = 8; // Default SDA pin + int8_t _sdaPin = 8; // Default SDA pin #endif #ifdef INA219_SCL_PIN - uint8_t _sclPin = INA219_SCL_PIN; + int8_t _sclPin = INA219_SCL_PIN; #else - uint8_t _sclPin = 9; // Default SCL pin + int8_t _sclPin = 9; // Default SCL pin #endif // Variables to store sensor readings From 42274ef3861cb882fbff1f8ba7e78791f35cd308 Mon Sep 17 00:00:00 2001 From: KrX3D Date: Mon, 16 Dec 2024 00:35:10 +0100 Subject: [PATCH 7/7] Update usermod_ina219.h Several optimizations for WLED_MQTT_CONNECTED and if Usermod is enabled/disabled --- usermods/INA219/usermod_ina219.h | 267 +++++++++++++++++-------------- 1 file changed, 143 insertions(+), 124 deletions(-) diff --git a/usermods/INA219/usermod_ina219.h b/usermods/INA219/usermod_ina219.h index 2b525e820d..33fa8bf098 100644 --- a/usermods/INA219/usermod_ina219.h +++ b/usermods/INA219/usermod_ina219.h @@ -219,62 +219,70 @@ class UsermodINA219 : public Usermod { #ifndef WLED_DISABLE_MQTT // Publish to MQTT if enabled - if (mqttPublish) { - if (mqttPublishAlways || hasValueChanged()) { - publishMqtt(shuntVoltage, busVoltage, loadVoltage, current, current_mA, power, power_mW, overflow); + if (WLED_MQTT_CONNECTED) { + if (mqttPublish) { + if (mqttPublishAlways || hasValueChanged()) { + publishMqtt(shuntVoltage, busVoltage, loadVoltage, current, current_mA, power, power_mW, overflow); + + last_sent_shuntVoltage = shuntVoltage; + last_sent_busVoltage = busVoltage; + last_sent_loadVoltage = loadVoltage; + last_sent_current = current; + last_sent_current_mA = current_mA; + last_sent_power = power; + last_sent_power_mW = power_mW; + last_sent_overflow = overflow; + + mqttPublishSent = true; + } + } else if (!mqttPublish && mqttPublishSent) { + char sensorTopic[128]; + snprintf_P(sensorTopic, 127, "%s/sensor/ina219", mqttDeviceTopic); // Discovery config topic for each sensor + + // Publish an empty message with retain to delete the sensor from Home Assistant + mqtt->publish(sensorTopic, 0, true, ""); - last_sent_shuntVoltage = shuntVoltage; - last_sent_busVoltage = busVoltage; - last_sent_loadVoltage = loadVoltage; - last_sent_current = current; - last_sent_current_mA = current_mA; - last_sent_power = power; - last_sent_power_mW = power_mW; - last_sent_overflow = overflow; - - mqttPublishSent = true; + mqttPublishSent = false; } - } else if (!mqttPublish && mqttPublishSent) { - char sensorTopic[128]; - snprintf_P(sensorTopic, 127, "%s/sensor/ina219", mqttDeviceTopic); // Discovery config topic for each sensor - - // Publish an empty message with retain to delete the sensor from Home Assistant - mqtt->publish(sensorTopic, 0, true, ""); - - mqttPublishSent = false; } // Optionally publish to Home Assistant via MQTT discovery if (haDiscovery && !haDiscoverySent) { - char topic[128]; - snprintf_P(topic, 127, "%s/sensor/ina219", mqttDeviceTopic); // Common topic for all INA219 data - - mqttCreateHassSensor(F("Current"), topic, F("current"), F("A"), F("current"), F("sensor")); - mqttCreateHassSensor(F("Voltage"), topic, F("voltage"), F("V"), F("bus_voltage_V"), F("sensor")); - mqttCreateHassSensor(F("Power"), topic, F("power"), F("W"), F("power"), F("sensor")); - mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("mV"), F("shunt_voltage_mV"), F("sensor")); - mqttCreateHassSensor(F("Shunt Resistor"), topic, F(""), F("Ω"), F("shunt_resistor_Ohms"), F("sensor")); - mqttCreateHassSensor(F("Overflow"), topic, F(""), F(""), F("overflow"), F("binary_sensor")); - mqttCreateHassSensor(F("Daily Energy"), topic, F("energy"), F("kWh"), F("daily_energy_kWh"), F("sensor")); - mqttCreateHassSensor(F("Monthly Energy"), topic, F("energy"), F("kWh"), F("monthly_energy_kWh"), F("sensor")); - mqttCreateHassSensor(F("Total Energy"), topic, F("energy"), F("kWh"), F("total_energy_kWh"), F("sensor")); - - // Mark as sent to avoid repeating - haDiscoverySent = true; + if (WLED_MQTT_CONNECTED) { + char topic[128]; + snprintf_P(topic, 127, "%s/sensor/ina219", mqttDeviceTopic); // Common topic for all INA219 data + + mqttCreateHassSensor(F("Current"), topic, F("current"), F("A"), F("current"), F("sensor")); + mqttCreateHassSensor(F("Voltage"), topic, F("voltage"), F("V"), F("bus_voltage_V"), F("sensor")); + mqttCreateHassSensor(F("Power"), topic, F("power"), F("W"), F("power"), F("sensor")); + mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("mV"), F("shunt_voltage_mV"), F("sensor")); + mqttCreateHassSensor(F("Shunt Resistor"), topic, F(""), F("Ω"), F("shunt_resistor_Ohms"), F("sensor")); + //mqttCreateHassSensor(F("Overflow"), topic, F(""), F(""), F("overflow"), F("binary_sensor")); //Binary Sensor does not show value in Home Assistant, so switching to sensor + mqttCreateHassSensor(F("Overflow"), topic, F(""), F(""), F("overflow"), F("sensor")); + mqttCreateHassSensor(F("Daily Energy"), topic, F("energy"), F("kWh"), F("daily_energy_kWh"), F("sensor")); + mqttCreateHassSensor(F("Monthly Energy"), topic, F("energy"), F("kWh"), F("monthly_energy_kWh"), F("sensor")); + mqttCreateHassSensor(F("Total Energy"), topic, F("energy"), F("kWh"), F("total_energy_kWh"), F("sensor")); + + // Mark as sent to avoid repeating + haDiscoverySent = true; + } } else if (!haDiscovery && haDiscoverySent) { - // Remove previously created sensors - mqttRemoveHassSensor(F("Current"), F("sensor")); - mqttRemoveHassSensor(F("Voltage"), F("sensor")); - mqttRemoveHassSensor(F("Power"), F("sensor")); - mqttRemoveHassSensor(F("Shunt-Voltage"), F("sensor")); - mqttRemoveHassSensor(F("Daily-Energy"), F("sensor")); - mqttRemoveHassSensor(F("Monthly-Energy"), F("sensor")); - mqttRemoveHassSensor(F("Total-Energy"), F("sensor")); - mqttRemoveHassSensor(F("Shunt-Resistor"), F("sensor")); - mqttRemoveHassSensor(F("Overflow"), F("binary_sensor")); - - // Mark as sent to avoid repeating - haDiscoverySent = false; + if (WLED_MQTT_CONNECTED) { + // Remove previously created sensors + mqttRemoveHassSensor(F("Current"), F("sensor")); + mqttRemoveHassSensor(F("Voltage"), F("sensor")); + mqttRemoveHassSensor(F("Power"), F("sensor")); + mqttRemoveHassSensor(F("Shunt-Voltage"), F("sensor")); + mqttRemoveHassSensor(F("Daily-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Monthly-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Total-Energy"), F("sensor")); + mqttRemoveHassSensor(F("Shunt-Resistor"), F("sensor")); + //mqttRemoveHassSensor(F("Overflow"), F("binary_sensor")); + mqttRemoveHassSensor(F("Overflow"), F("sensor")); + + // Mark as sent to avoid repeating + haDiscoverySent = false; + } } #endif } @@ -300,28 +308,28 @@ class UsermodINA219 : public Usermod { * Function to publish sensor data to MQTT */ bool onMqttMessage(char* topic, char* payload) override { - // Check if the message is for the correct topic - if (strcmp_P(topic, PSTR("/sensor/ina219")) == 0) { - StaticJsonDocument<512> jsonDoc; - - // Parse the JSON payload - DeserializationError error = deserializeJson(jsonDoc, payload); - if (error) { - Serial.print(F("deserializeJson() failed: ")); - Serial.println(error.f_str()); - return false; - } - - // Update the energy values - dailyEnergy_kWh = jsonDoc["daily_energy_kWh"]; - monthlyEnergy_kWh = jsonDoc["monthly_energy_kWh"]; - totalEnergy_kWh = jsonDoc["total_energy_kWh"]; - dailyResetTime = jsonDoc["dailyResetTime"]; - monthlyResetTime = jsonDoc["monthlyResetTime"]; - - return true; - } - + if (!WLED_MQTT_CONNECTED) return false; + if (enabled) { + // Check if the message is for the correct topic + if (strcmp_P(topic, PSTR("/sensor/ina219")) == 0) { + StaticJsonDocument<512> jsonDoc; + + // Parse the JSON payload + DeserializationError error = deserializeJson(jsonDoc, payload); + if (error) { + return false; + } + + // Update the energy values + dailyEnergy_kWh = jsonDoc["daily_energy_kWh"]; + monthlyEnergy_kWh = jsonDoc["monthly_energy_kWh"]; + totalEnergy_kWh = jsonDoc["total_energy_kWh"]; + dailyResetTime = jsonDoc["dailyResetTime"]; + monthlyResetTime = jsonDoc["monthlyResetTime"]; + + return true; + } + } return false; } @@ -329,12 +337,15 @@ class UsermodINA219 : public Usermod { * Subscribe to MQTT topic for controlling usermod */ void onMqttConnect(bool sessionPresent) override { - char subuf[64]; - if (mqttDeviceTopic[0] != 0) { - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/sensor/ina219")); - mqtt->subscribe(subuf, 0); - } + if (!enabled) return; + if (WLED_MQTT_CONNECTED) { + char subuf[64]; + if (mqttDeviceTopic[0] != 0) { + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/sensor/ina219")); + mqtt->subscribe(subuf, 0); + } + } } #endif @@ -347,40 +358,43 @@ class UsermodINA219 : public Usermod { // Publish to MQTT only if the WLED MQTT feature is enabled #ifndef WLED_DISABLE_MQTT - // Create a JSON document to hold sensor data - StaticJsonDocument<512> jsonDoc; - - // Populate the JSON document with sensor readings - jsonDoc["shunt_voltage_mV"] = shuntVoltage; // Shunt voltage in millivolts - jsonDoc["bus_voltage_V"] = busVoltage; // Bus voltage in volts - jsonDoc["load_voltage_V"] = loadVoltage; // Load voltage in volts - jsonDoc["current"] = current; // Current in unspecified units - jsonDoc["current_mA"] = current_mA; // Current in milliamperes - jsonDoc["power"] = power; // Power in unspecified units - jsonDoc["power_mW"] = power_mW; // Power in milliwatts - jsonDoc["overflow"] = overflow; // Overflow status (true/false) - //jsonDoc["overflow"] = overflow ? "on" : "off"; - jsonDoc["shunt_resistor_Ohms"] = shuntResistor; // Shunt resistor value in Ohms - - // Energy calculations - jsonDoc["daily_energy_kWh"] = dailyEnergy_kWh; // Daily energy in kilowatt-hours - jsonDoc["monthly_energy_kWh"] = monthlyEnergy_kWh; // Monthly energy in kilowatt-hours - jsonDoc["total_energy_kWh"] = totalEnergy_kWh; // Total energy in kilowatt-hours - - // Reset timestamps - jsonDoc["dailyResetTime"] = dailyResetTime; // Timestamp of the last daily reset - jsonDoc["monthlyResetTime"] = monthlyResetTime; // Timestamp of the last monthly reset - - // Serialize the JSON document into a character buffer - char buffer[512]; - serializeJson(jsonDoc, buffer); - - // Construct the MQTT topic using the device topic - char topic[128]; - snprintf_P(topic, sizeof(topic), "%s/sensor/ina219", mqttDeviceTopic); - - // Publish the serialized JSON data to the specified MQTT topic - mqtt->publish(topic, 0, true, buffer); + if (WLED_MQTT_CONNECTED) { + // Create a JSON document to hold sensor data + StaticJsonDocument<1024> jsonDoc; + + // Populate the JSON document with sensor readings + jsonDoc["shunt_voltage_mV"] = shuntVoltage; // Shunt voltage in millivolts + jsonDoc["bus_voltage_V"] = busVoltage; // Bus voltage in volts + jsonDoc["load_voltage_V"] = loadVoltage; // Load voltage in volts + jsonDoc["current"] = current; // Current in unspecified units + jsonDoc["current_mA"] = current_mA; // Current in milliamperes + jsonDoc["power"] = power; // Power in unspecified units + jsonDoc["power_mW"] = power_mW; // Power in milliwatts + jsonDoc["overflow"] = overflow; // Overflow status (true/false) + //jsonDoc["overflow"] = overflow ? "on" : "off"; + jsonDoc["shunt_resistor_Ohms"] = shuntResistor; // Shunt resistor value in Ohms + + // Energy calculations + jsonDoc["daily_energy_kWh"] = dailyEnergy_kWh; // Daily energy in kilowatt-hours + jsonDoc["monthly_energy_kWh"] = monthlyEnergy_kWh; // Monthly energy in kilowatt-hours + jsonDoc["total_energy_kWh"] = totalEnergy_kWh; // Total energy in kilowatt-hours + + // Reset timestamps + jsonDoc["dailyResetTime"] = dailyResetTime; // Timestamp of the last daily reset + jsonDoc["monthlyResetTime"] = monthlyResetTime; // Timestamp of the last monthly reset + + // Serialize the JSON document into a character buffer + char buffer[1024]; + size_t payload_size; + payload_size = serializeJson(jsonDoc, buffer); + + // Construct the MQTT topic using the device topic + char topic[128]; + snprintf_P(topic, sizeof(topic), "%s/sensor/ina219", mqttDeviceTopic); + + // Publish the serialized JSON data to the specified MQTT topic + mqtt->publish(topic, 0, true, buffer, payload_size); + } #endif } @@ -396,11 +410,8 @@ class UsermodINA219 : public Usermod { String sanitizedMqttClientID = sanitizeMqttClientID(mqttClientID); - // Construct the Home Assistant configuration topic - String t = String(F("homeassistant/")) + SensorType + "/" + sanitizedMqttClientID + "/" + sanitizedName + F("/config"); - // Create a JSON document for the sensor configuration - StaticJsonDocument<600> doc; + StaticJsonDocument<1024> doc; // Populate the JSON document with sensor configuration details doc[F("name")] = name; // Sensor name @@ -425,15 +436,19 @@ class UsermodINA219 : public Usermod { device[F("sw_version")] = versionString; // Software version // Serialize the JSON document into a temporary string - String temp; - serializeJson(doc, temp); + char buffer[1024]; + size_t payload_size; + payload_size = serializeJson(doc, buffer); + + char topic_S[128]; + snprintf_P(topic_S, sizeof(topic_S), "homeassistant/%s/%s/%s/config", SensorType, sanitizedMqttClientID, sanitizedName); // Debug output for the Home Assistant topic and configuration - DEBUG_PRINTLN(t); - DEBUG_PRINTLN(temp); + DEBUG_PRINTLN(topic_S); + DEBUG_PRINTLN(buffer); // Publish the sensor configuration to Home Assistant - mqtt->publish(t.c_str(), 0, true, temp.c_str()); + mqtt->publish(topic_S, 0, true, buffer, payload_size); } void mqttRemoveHassSensor(const String &name, const String &SensorType) { @@ -539,7 +554,9 @@ class UsermodINA219 : public Usermod { } // Create a nested array for energy data - JsonArray energy_json = user.createNestedArray(F("Energy Consumption")); + JsonArray energy_json_seperator = user.createNestedArray(F("------------------------------------")); + + JsonArray energy_json = user.createNestedArray(F("Energy Consumption:")); if (!enabled) { energy_json.add(F("disabled")); // Indicate that the module is disabled @@ -547,17 +564,17 @@ class UsermodINA219 : public Usermod { // Create a nested array for daily energy JsonArray dailyEnergy_json = user.createNestedArray(F("Daily Energy")); dailyEnergy_json.add(dailyEnergy_kWh); // Add daily energy in kWh - dailyEnergy_json.add(F("kWh")); // Add unit of measurement + dailyEnergy_json.add(F(" kWh")); // Add unit of measurement // Create a nested array for monthly energy JsonArray monthlyEnergy_json = user.createNestedArray(F("Monthly Energy")); monthlyEnergy_json.add(monthlyEnergy_kWh); // Add monthly energy in kWh - monthlyEnergy_json.add(F("kWh")); // Add unit of measurement + monthlyEnergy_json.add(F(" kWh")); // Add unit of measurement // Create a nested array for total energy JsonArray totalEnergy_json = user.createNestedArray(F("Total Energy")); totalEnergy_json.add(totalEnergy_kWh); // Add total energy in kWh - totalEnergy_json.add(F("kWh")); // Add unit of measurement + totalEnergy_json.add(F(" kWh")); // Add unit of measurement } } @@ -684,6 +701,8 @@ class UsermodINA219 : public Usermod { configComplete &= getJsonValue(top["mqtt_publish"], mqttPublish); configComplete &= getJsonValue(top["mqtt_publish_always"], mqttPublishAlways); configComplete &= getJsonValue(top["ha_discovery"], haDiscovery); + + haDiscoverySent = !haDiscovery; #endif updateINA219Settings(); // Apply any updated settings to the INA219