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
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 4 Line Display usermod with SPI display
; -D USERMOD_FOUR_LINE_DISPLAY
; -D USE_ALT_DISPlAY # mandatory
Expand Down
37 changes: 37 additions & 0 deletions usermods/AHT10_v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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
Wire
```
LordMike marked this conversation as resolved.
Show resolved Hide resolved
318 changes: 318 additions & 0 deletions usermods/AHT10_v2/usermod_aht10.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
#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;

struct
{
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
unsigned : 3;
} _stateFlags;
LordMike marked this conversation as resolved.
Show resolved Hide resolved

// 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;

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 || !_stateFlags.mqttPublish || !_stateFlags.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)
{
// Check if MQTT Connected, otherwise it will crash the 8266
if (WLED_MQTT_CONNECTED && _stateFlags.mqttPublish && (_stateFlags.mqttPublishAlways || lastState != state))
{
char subuf[128];
snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic);
mqtt->publish(subuf, 0, false, String(state).c_str());
}
}

// 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 (!_stateFlags.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
mqttPublishIfChanged(F("temperature"), _lastTemperature, temperature);
LordMike marked this conversation as resolved.
Show resolved Hide resolved
mqttPublishIfChanged(F("humidity"), _lastHumidity, humidity);
#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")] = _stateFlags.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")] = _stateFlags.mqttPublish;
top[F("MqttPublishAlways")] = _stateFlags.mqttPublishAlways;
top[F("MqttHomeAssistantDiscovery")] = _stateFlags.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)
_stateFlags.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)
_stateFlags.mqttPublish = tmpBool;

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

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

if (_stateFlags.initDone)
{
// Reloading config
initializeAht();

#ifndef WLED_DISABLE_MQTT
mqttInitialize();
#endif
}

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

const char UsermodAHT10::_name[] PROGMEM = "AHTxx";
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
#define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h"
#define USERMOD_ID_TETRISAI 47 //Usermod "usermod_v2_tetris.h"
#define USERMOD_ID_MAX17048 48 //Usermod "usermod_max17048.h"
#define USERMOD_ID_AHT10 49 //Usermod "usermod_aht10.h"

//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot
Expand Down
8 changes: 8 additions & 0 deletions wled00/usermods_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@
#include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h"
#endif

#ifdef USERMOD_AHT10
#include "../usermods/AHT10_v2/usermod_aht10.h"
#endif

void registerUsermods()
{
/*
Expand Down Expand Up @@ -421,4 +425,8 @@ void registerUsermods()
#ifdef USERMOD_TETRISAI
usermods.add(new TetrisAIUsermod());
#endif

#ifdef USERMOD_AHT10
usermods.add(new UsermodAHT10());
#endif
}