Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
```
320 changes: 320 additions & 0 deletions usermods/AHT10_v2/usermod_aht10.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
#pragma once

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

#define AHT10_SUCCESS 1

#define STATE_SETTING_ENABLED 0b00000001
#define STATE_SETTING_MQTT_PUBLISH 0b00000010
#define STATE_SETTING_MQTT_PUBLISHALWAYS 0b00000100
#define STATE_SETTING_MQTT_HOMEASSISTANT 0b00001000
#define STATE_INITDONE 0b00010000

#define STATE_ISSET(stateFlags, state) (((stateFlags) & (state)) == (state))
#define STATE_SET(stateFlags, state, set) ((set) ? ((stateFlags) |= (state)) : ((stateFlags) &= ~(state)))

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

unsigned long _lastLoopCheck = 0;

// To avoid storing N bools, this flags field will handle all of those
uint8_t _stateFlags = 0;

// 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 || !STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_PUBLISH) || !STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_HOMEASSISTANT))
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 && STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_PUBLISH) && (STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_PUBLISHALWAYS) || 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 + F("/") + 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 (!STATE_ISSET(_stateFlags, STATE_SETTING_ENABLED) || 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("AHTxx returned error, doing softReset");
if (!_aht->softReset())
{
DEBUG_PRINTLN("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);
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[F("u")];
if (user.isNull())
user = root.createNestedObject(F("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")] = STATE_ISSET(_stateFlags, STATE_SETTING_ENABLED);
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")] = STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_PUBLISH);
top[F("MqttPublishAlways")] = STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_PUBLISHALWAYS);
top[F("MqttHomeAssistantDiscovery")] = STATE_ISSET(_stateFlags, STATE_SETTING_MQTT_HOMEASSISTANT);
#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)
STATE_SET(_stateFlags, STATE_SETTING_ENABLED, 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)
STATE_SET(_stateFlags, STATE_SETTING_MQTT_PUBLISH, tmpBool);

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

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

if (STATE_ISSET(_stateFlags, STATE_INITDONE))
{
// Reloading config
initializeAht();

#ifndef WLED_DISABLE_MQTT
mqttInitialize();
#endif
}

STATE_SET(_stateFlags, STATE_INITDONE, true);
return configComplete;
}
};

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
}