Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a usermod for AHT10, AHT15 and AHT20 temperature/humidity sensors #3977

Merged
merged 10 commits into from
May 21, 2024
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ wled-update.sh
/wled00/my_config.h
/wled00/Release
/wled00/wled00.ino.cpp
/wled00/html_*.h
/wled00/html_*.h
3 changes: 3 additions & 0 deletions platformio_override.sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ build_flags = ${common.build_flags} ${esp8266.build_flags}
; -D USERMOD_AUTO_SAVE
; -D AUTOSAVE_AFTER_SEC=90
;
; Use AHT10/AHT15/AHT20 usermod
; -D USERMOD_AHT10
;
; Use INA226 usermod
; -D USERMOD_INA226
;
Expand Down
36 changes: 36 additions & 0 deletions usermods/AHT10_v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Usermod AHT10
This Usermod is designed to read a `AHT10`, `AHT15` or `AHT20` sensor and output the following:
- Temperature
- Humidity

Configuration is performed via the Usermod menu. The following settings can be configured in the Usermod Menu:
- I2CAddress: The i2c address in decimal. Set it to either 56 (0x38, the default) or 57 (0x39).
- SensorType, one of:
- 0 - AHT10
- 1 - AHT15
- 2 - AHT20
- CheckInterval: Number of seconds between readings
- Decimals: Number of decimals to put in the output

Dependencies, These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`).
- Libraries
- `enjoyneering/AHT10@~1.1.0` (by [enjoyneering](https://registry.platformio.org/libraries/enjoyneering/AHT10))
- `Wire`

## Author
[@LordMike](https://github.com/LordMike)

# Compiling

To enable, compile with `USERMOD_AHT10` defined (e.g. in `platformio_override.ini`)
```ini
[env:aht10_example]
extends = env:esp32dev
build_flags =
${common.build_flags} ${esp32.build_flags}
-D USERMOD_AHT10
; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal
lib_deps =
${esp32.lib_deps}
enjoyneering/AHT10@~1.1.0
```
LordMike marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions usermods/AHT10_v2/platformio_override.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[env:aht10_example]
extends = env:esp32dev
build_flags =
${common.build_flags} ${esp32.build_flags}
-D USERMOD_AHT10
; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal
lib_deps =
${esp32.lib_deps}
enjoyneering/AHT10@~1.1.0
327 changes: 327 additions & 0 deletions usermods/AHT10_v2/usermod_aht10.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
#pragma once

#include "wled.h"
#include <AHT10.h>

#define AHT10_SUCCESS 1

class UsermodAHT10 : public Usermod
{
private:
static const char _name[];

unsigned long _lastLoopCheck = 0;

bool _settingEnabled : 1; // Enable the usermod
bool _mqttPublish : 1; // Publish mqtt values
bool _mqttPublishAlways : 1; // Publish always, regardless if there is a change
bool _mqttHomeAssistant : 1; // Enable Home Assistant docs
bool _initDone : 1; // Initialization is done

// Settings. Some of these are stored in a different format than they're user settings - so we don't have to convert at runtime
uint8_t _i2cAddress = AHT10_ADDRESS_0X38;
ASAIR_I2C_SENSOR _ahtType = AHT10_SENSOR;
uint16_t _checkInterval = 60000; // milliseconds, user settings is in seconds
float _decimalFactor = 100; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)

uint8_t _lastStatus = 0;
float _lastHumidity = 0;
float _lastTemperature = 0;

#ifndef WLED_MQTT_DISABLE
float _lastHumiditySent = 0;
float _lastTemperatureSent = 0;
#endif

AHT10 *_aht = nullptr;

float truncateDecimals(float val)
{
return roundf(val * _decimalFactor) / _decimalFactor;
}

void initializeAht()
{
if (_aht != nullptr)
{
delete _aht;
}

_aht = new AHT10(_i2cAddress, _ahtType);

_lastStatus = 0;
_lastHumidity = 0;
_lastTemperature = 0;
}

~UsermodAHT10()
{
delete _aht;
_aht = nullptr;
}

#ifndef WLED_DISABLE_MQTT
void mqttInitialize()
{
// This is a generic "setup mqtt" function, So we must abort if we're not to do mqtt
if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant)
return;

char topic[128];
snprintf_P(topic, 127, "%s/temperature", mqttDeviceTopic);
mqttCreateHassSensor(F("Temperature"), topic, F("temperature"), F("°C"));

snprintf_P(topic, 127, "%s/humidity", mqttDeviceTopic);
mqttCreateHassSensor(F("Humidity"), topic, F("humidity"), F("%"));
}

void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange)
{
// Check if MQTT Connected, otherwise it will crash the 8266
// Only report if the change is larger than the required diff
if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange))
{
char subuf[128];
snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic);
mqtt->publish(subuf, 0, false, String(state).c_str());

lastState = state;
}
}

// Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.
void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)
{
String t = String(F("homeassistant/sensor/")) + mqttClientID + "/" + name + F("/config");

StaticJsonDocument<600> doc;

doc[F("name")] = name;
doc[F("state_topic")] = topic;
doc[F("unique_id")] = String(mqttClientID) + name;
if (unitOfMeasurement != "")
doc[F("unit_of_measurement")] = unitOfMeasurement;
if (deviceClass != "")
doc[F("device_class")] = deviceClass;
doc[F("expire_after")] = 1800;

JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device
device[F("name")] = serverDescription;
device[F("identifiers")] = "wled-sensor-" + String(mqttClientID);
device[F("manufacturer")] = F(WLED_BRAND);
device[F("model")] = F(WLED_PRODUCT_NAME);
device[F("sw_version")] = versionString;

String temp;
serializeJson(doc, temp);
DEBUG_PRINTLN(t);
DEBUG_PRINTLN(temp);

mqtt->publish(t.c_str(), 0, true, temp.c_str());
}
#endif

public:
void setup()
{
initializeAht();
}

void loop()
{
// if usermod is disabled or called during strip updating just exit
// NOTE: on very long strips strip.isUpdating() may always return true so update accordingly
if (!_settingEnabled || strip.isUpdating())
return;

// do your magic here
unsigned long currentTime = millis();

if (currentTime - _lastLoopCheck < _checkInterval)
return;
_lastLoopCheck = currentTime;

_lastStatus = _aht->readRawData();

if (_lastStatus == AHT10_ERROR)
{
// Perform softReset and retry
DEBUG_PRINTLN(F("AHTxx returned error, doing softReset"));
if (!_aht->softReset())
{
DEBUG_PRINTLN(F("softReset failed"));
return;
}

_lastStatus = _aht->readRawData();
}

if (_lastStatus == AHT10_SUCCESS)
{
float temperature = truncateDecimals(_aht->readTemperature(AHT10_USE_READ_DATA));
float humidity = truncateDecimals(_aht->readHumidity(AHT10_USE_READ_DATA));

#ifndef WLED_DISABLE_MQTT
// Push to MQTT

// We can avoid reporting if the change is insignificant. The threshold chosen is below the level of accuracy, but way above 0.01 which is the precision of the value provided.
// The AHT10/15/20 has an accuracy of 0.3C in the temperature readings
mqttPublishIfChanged(F("temperature"), _lastTemperatureSent, temperature, 0.1f);

// The AHT10/15/20 has an accuracy in the humidity sensor of 2%
mqttPublishIfChanged(F("humidity"), _lastHumiditySent, humidity, 0.5f);
#endif

// Store
_lastTemperature = temperature;
_lastHumidity = humidity;
}
}

#ifndef WLED_DISABLE_MQTT
void onMqttConnect(bool sessionPresent)
{
mqttInitialize();
}
#endif

uint16_t getId()
{
return USERMOD_ID_AHT10;
}

void addToJsonInfo(JsonObject &root) override
{
// if "u" object does not exist yet wee need to create it
JsonObject user = root["u"];
if (user.isNull())
user = root.createNestedObject("u");

#ifdef USERMOD_AHT10_DEBUG
JsonArray temp = user.createNestedArray(F("AHT last loop"));
temp.add(_lastLoopCheck);

temp = user.createNestedArray(F("AHT last status"));
temp.add(_lastStatus);
#endif

JsonArray jsonTemp = user.createNestedArray(F("Temperature"));
JsonArray jsonHumidity = user.createNestedArray(F("Humidity"));

if (_lastLoopCheck == 0)
{
// Before first run
jsonTemp.add(F("Not read yet"));
jsonHumidity.add(F("Not read yet"));
return;
}

if (_lastStatus != AHT10_SUCCESS)
{
jsonTemp.add(F("An error occurred"));
jsonHumidity.add(F("An error occurred"));
return;
}

jsonTemp.add(_lastTemperature);
jsonTemp.add(F("°C"));

jsonHumidity.add(_lastHumidity);
jsonHumidity.add(F("%"));
}

void addToConfig(JsonObject &root)
{
JsonObject top = root.createNestedObject(FPSTR(_name));
top[F("Enabled")] = _settingEnabled;
top[F("I2CAddress")] = static_cast<uint8_t>(_i2cAddress);
top[F("SensorType")] = _ahtType;
top[F("CheckInterval")] = _checkInterval / 1000;
top[F("Decimals")] = log10f(_decimalFactor);
#ifndef WLED_DISABLE_MQTT
top[F("MqttPublish")] = _mqttPublish;
top[F("MqttPublishAlways")] = _mqttPublishAlways;
top[F("MqttHomeAssistantDiscovery")] = _mqttHomeAssistant;
#endif

DEBUG_PRINTLN(F("AHT10 config saved."));
}

bool readFromConfig(JsonObject &root) override
{
// default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor
// setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)

JsonObject top = root[FPSTR(_name)];

bool configComplete = !top.isNull();
if (!configComplete)
return false;

bool tmpBool = false;
configComplete &= getJsonValue(top[F("Enabled")], tmpBool);
if (configComplete)
_settingEnabled = tmpBool;

configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress);
configComplete &= getJsonValue(top[F("CheckInterval")], _checkInterval);
if (configComplete)
{
if (1 <= _checkInterval && _checkInterval <= 600)
_checkInterval *= 1000;
else
// Invalid input
_checkInterval = 60000;
}

configComplete &= getJsonValue(top[F("Decimals")], _decimalFactor);
if (configComplete)
{
if (0 <= _decimalFactor && _decimalFactor <= 5)
_decimalFactor = pow10f(_decimalFactor);
else
// Invalid input
_decimalFactor = 100;
}

uint8_t tmpAhtType;
configComplete &= getJsonValue(top[F("SensorType")], tmpAhtType);
if (configComplete)
{
if (0 <= tmpAhtType && tmpAhtType <= 2)
_ahtType = static_cast<ASAIR_I2C_SENSOR>(tmpAhtType);
else
// Invalid input
_ahtType = ASAIR_I2C_SENSOR::AHT10_SENSOR;
}

#ifndef WLED_DISABLE_MQTT
configComplete &= getJsonValue(top[F("MqttPublish")], tmpBool);
if (configComplete)
_mqttPublish = tmpBool;

configComplete &= getJsonValue(top[F("MqttPublishAlways")], tmpBool);
if (configComplete)
_mqttPublishAlways = tmpBool;

configComplete &= getJsonValue(top[F("MqttHomeAssistantDiscovery")], tmpBool);
if (configComplete)
_mqttHomeAssistant = tmpBool;
#endif

if (_initDone)
{
// Reloading config
initializeAht();

#ifndef WLED_DISABLE_MQTT
mqttInitialize();
#endif
}

_initDone = true;
return configComplete;
}
};
LordMike marked this conversation as resolved.
Show resolved Hide resolved

const char UsermodAHT10::_name[] PROGMEM = "AHTxx";
Loading