1+ #pragma once
2+
3+ #include " wled.h"
4+ #include < AHT10.h>
5+
6+ #define AHT10_SUCCESS 1
7+
8+ class UsermodAHT10 : public Usermod
9+ {
10+ private:
11+ static const char _name[];
12+
13+ unsigned long _lastLoopCheck = 0 ;
14+
15+ bool _settingEnabled : 1 ; // Enable the usermod
16+ bool _mqttPublish : 1 ; // Publish mqtt values
17+ bool _mqttPublishAlways : 1 ; // Publish always, regardless if there is a change
18+ bool _mqttHomeAssistant : 1 ; // Enable Home Assistant docs
19+ bool _initDone : 1 ; // Initialization is done
20+
21+ // Settings. Some of these are stored in a different format than they're user settings - so we don't have to convert at runtime
22+ uint8_t _i2cAddress = AHT10_ADDRESS_0X38;
23+ ASAIR_I2C_SENSOR _ahtType = AHT10_SENSOR;
24+ uint16_t _checkInterval = 60000 ; // milliseconds, user settings is in seconds
25+ 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, ..)
26+
27+ uint8_t _lastStatus = 0 ;
28+ float _lastHumidity = 0 ;
29+ float _lastTemperature = 0 ;
30+
31+ #ifndef WLED_MQTT_DISABLE
32+ float _lastHumiditySent = 0 ;
33+ float _lastTemperatureSent = 0 ;
34+ #endif
35+
36+ AHT10 *_aht = nullptr ;
37+
38+ float truncateDecimals (float val)
39+ {
40+ return roundf (val * _decimalFactor) / _decimalFactor;
41+ }
42+
43+ void initializeAht ()
44+ {
45+ if (_aht != nullptr )
46+ {
47+ delete _aht;
48+ }
49+
50+ _aht = new AHT10 (_i2cAddress, _ahtType);
51+
52+ _lastStatus = 0 ;
53+ _lastHumidity = 0 ;
54+ _lastTemperature = 0 ;
55+ }
56+
57+ ~UsermodAHT10 ()
58+ {
59+ delete _aht;
60+ _aht = nullptr ;
61+ }
62+
63+ #ifndef WLED_DISABLE_MQTT
64+ void mqttInitialize ()
65+ {
66+ // This is a generic "setup mqtt" function, So we must abort if we're not to do mqtt
67+ if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant)
68+ return ;
69+
70+ char topic[128 ];
71+ snprintf_P (topic, 127 , " %s/temperature" , mqttDeviceTopic);
72+ mqttCreateHassSensor (F (" Temperature" ), topic, F (" temperature" ), F (" °C" ));
73+
74+ snprintf_P (topic, 127 , " %s/humidity" , mqttDeviceTopic);
75+ mqttCreateHassSensor (F (" Humidity" ), topic, F (" humidity" ), F (" %" ));
76+ }
77+
78+ void mqttPublishIfChanged (const __FlashStringHelper *topic, float &lastState, float state, float minChange)
79+ {
80+ // Check if MQTT Connected, otherwise it will crash the 8266
81+ // Only report if the change is larger than the required diff
82+ if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf (lastState - state) > minChange))
83+ {
84+ char subuf[128 ];
85+ snprintf_P (subuf, 127 , PSTR (" %s/%s" ), mqttDeviceTopic, (const char *)topic);
86+ mqtt->publish (subuf, 0 , false , String (state).c_str ());
87+
88+ lastState = state;
89+ }
90+ }
91+
92+ // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.
93+ void mqttCreateHassSensor (const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)
94+ {
95+ String t = String (F (" homeassistant/sensor/" )) + mqttClientID + " /" + name + F (" /config" );
96+
97+ StaticJsonDocument<600 > doc;
98+
99+ doc[F (" name" )] = name;
100+ doc[F (" state_topic" )] = topic;
101+ doc[F (" unique_id" )] = String (mqttClientID) + name;
102+ if (unitOfMeasurement != " " )
103+ doc[F (" unit_of_measurement" )] = unitOfMeasurement;
104+ if (deviceClass != " " )
105+ doc[F (" device_class" )] = deviceClass;
106+ doc[F (" expire_after" )] = 1800 ;
107+
108+ JsonObject device = doc.createNestedObject (F (" device" )); // attach the sensor to the same device
109+ device[F (" name" )] = serverDescription;
110+ device[F (" identifiers" )] = " wled-sensor-" + String (mqttClientID);
111+ device[F (" manufacturer" )] = F (WLED_BRAND);
112+ device[F (" model" )] = F (WLED_PRODUCT_NAME);
113+ device[F (" sw_version" )] = versionString;
114+
115+ String temp;
116+ serializeJson (doc, temp);
117+ DEBUG_PRINTLN (t);
118+ DEBUG_PRINTLN (temp);
119+
120+ mqtt->publish (t.c_str (), 0 , true , temp.c_str ());
121+ }
122+ #endif
123+
124+ public:
125+ void setup ()
126+ {
127+ initializeAht ();
128+ }
129+
130+ void loop ()
131+ {
132+ // if usermod is disabled or called during strip updating just exit
133+ // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly
134+ if (!_settingEnabled || strip.isUpdating ())
135+ return ;
136+
137+ // do your magic here
138+ unsigned long currentTime = millis ();
139+
140+ if (currentTime - _lastLoopCheck < _checkInterval)
141+ return ;
142+ _lastLoopCheck = currentTime;
143+
144+ _lastStatus = _aht->readRawData ();
145+
146+ if (_lastStatus == AHT10_ERROR)
147+ {
148+ // Perform softReset and retry
149+ DEBUG_PRINTLN (F (" AHTxx returned error, doing softReset" ));
150+ if (!_aht->softReset ())
151+ {
152+ DEBUG_PRINTLN (F (" softReset failed" ));
153+ return ;
154+ }
155+
156+ _lastStatus = _aht->readRawData ();
157+ }
158+
159+ if (_lastStatus == AHT10_SUCCESS)
160+ {
161+ float temperature = truncateDecimals (_aht->readTemperature (AHT10_USE_READ_DATA));
162+ float humidity = truncateDecimals (_aht->readHumidity (AHT10_USE_READ_DATA));
163+
164+ #ifndef WLED_DISABLE_MQTT
165+ // Push to MQTT
166+
167+ // 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.
168+ // The AHT10/15/20 has an accuracy of 0.3C in the temperature readings
169+ mqttPublishIfChanged (F (" temperature" ), _lastTemperatureSent, temperature, 0 .1f );
170+
171+ // The AHT10/15/20 has an accuracy in the humidity sensor of 2%
172+ mqttPublishIfChanged (F (" humidity" ), _lastHumiditySent, humidity, 0 .5f );
173+ #endif
174+
175+ // Store
176+ _lastTemperature = temperature;
177+ _lastHumidity = humidity;
178+ }
179+ }
180+
181+ #ifndef WLED_DISABLE_MQTT
182+ void onMqttConnect (bool sessionPresent)
183+ {
184+ mqttInitialize ();
185+ }
186+ #endif
187+
188+ uint16_t getId ()
189+ {
190+ return USERMOD_ID_AHT10;
191+ }
192+
193+ void addToJsonInfo (JsonObject &root) override
194+ {
195+ // if "u" object does not exist yet wee need to create it
196+ JsonObject user = root[" u" ];
197+ if (user.isNull ())
198+ user = root.createNestedObject (" u" );
199+
200+ #ifdef USERMOD_AHT10_DEBUG
201+ JsonArray temp = user.createNestedArray (F (" AHT last loop" ));
202+ temp.add (_lastLoopCheck);
203+
204+ temp = user.createNestedArray (F (" AHT last status" ));
205+ temp.add (_lastStatus);
206+ #endif
207+
208+ JsonArray jsonTemp = user.createNestedArray (F (" Temperature" ));
209+ JsonArray jsonHumidity = user.createNestedArray (F (" Humidity" ));
210+
211+ if (_lastLoopCheck == 0 )
212+ {
213+ // Before first run
214+ jsonTemp.add (F (" Not read yet" ));
215+ jsonHumidity.add (F (" Not read yet" ));
216+ return ;
217+ }
218+
219+ if (_lastStatus != AHT10_SUCCESS)
220+ {
221+ jsonTemp.add (F (" An error occurred" ));
222+ jsonHumidity.add (F (" An error occurred" ));
223+ return ;
224+ }
225+
226+ jsonTemp.add (_lastTemperature);
227+ jsonTemp.add (F (" °C" ));
228+
229+ jsonHumidity.add (_lastHumidity);
230+ jsonHumidity.add (F (" %" ));
231+ }
232+
233+ void addToConfig (JsonObject &root)
234+ {
235+ JsonObject top = root.createNestedObject (FPSTR (_name));
236+ top[F (" Enabled" )] = _settingEnabled;
237+ top[F (" I2CAddress" )] = static_cast <uint8_t >(_i2cAddress);
238+ top[F (" SensorType" )] = _ahtType;
239+ top[F (" CheckInterval" )] = _checkInterval / 1000 ;
240+ top[F (" Decimals" )] = log10f (_decimalFactor);
241+ #ifndef WLED_DISABLE_MQTT
242+ top[F (" MqttPublish" )] = _mqttPublish;
243+ top[F (" MqttPublishAlways" )] = _mqttPublishAlways;
244+ top[F (" MqttHomeAssistantDiscovery" )] = _mqttHomeAssistant;
245+ #endif
246+
247+ DEBUG_PRINTLN (F (" AHT10 config saved." ));
248+ }
249+
250+ bool readFromConfig (JsonObject &root) override
251+ {
252+ // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor
253+ // 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)
254+
255+ JsonObject top = root[FPSTR (_name)];
256+
257+ bool configComplete = !top.isNull ();
258+ if (!configComplete)
259+ return false ;
260+
261+ bool tmpBool = false ;
262+ configComplete &= getJsonValue (top[F (" Enabled" )], tmpBool);
263+ if (configComplete)
264+ _settingEnabled = tmpBool;
265+
266+ configComplete &= getJsonValue (top[F (" I2CAddress" )], _i2cAddress);
267+ configComplete &= getJsonValue (top[F (" CheckInterval" )], _checkInterval);
268+ if (configComplete)
269+ {
270+ if (1 <= _checkInterval && _checkInterval <= 600 )
271+ _checkInterval *= 1000 ;
272+ else
273+ // Invalid input
274+ _checkInterval = 60000 ;
275+ }
276+
277+ configComplete &= getJsonValue (top[F (" Decimals" )], _decimalFactor);
278+ if (configComplete)
279+ {
280+ if (0 <= _decimalFactor && _decimalFactor <= 5 )
281+ _decimalFactor = pow10f (_decimalFactor);
282+ else
283+ // Invalid input
284+ _decimalFactor = 100 ;
285+ }
286+
287+ uint8_t tmpAhtType;
288+ configComplete &= getJsonValue (top[F (" SensorType" )], tmpAhtType);
289+ if (configComplete)
290+ {
291+ if (0 <= tmpAhtType && tmpAhtType <= 2 )
292+ _ahtType = static_cast <ASAIR_I2C_SENSOR>(tmpAhtType);
293+ else
294+ // Invalid input
295+ _ahtType = ASAIR_I2C_SENSOR::AHT10_SENSOR;
296+ }
297+
298+ #ifndef WLED_DISABLE_MQTT
299+ configComplete &= getJsonValue (top[F (" MqttPublish" )], tmpBool);
300+ if (configComplete)
301+ _mqttPublish = tmpBool;
302+
303+ configComplete &= getJsonValue (top[F (" MqttPublishAlways" )], tmpBool);
304+ if (configComplete)
305+ _mqttPublishAlways = tmpBool;
306+
307+ configComplete &= getJsonValue (top[F (" MqttHomeAssistantDiscovery" )], tmpBool);
308+ if (configComplete)
309+ _mqttHomeAssistant = tmpBool;
310+ #endif
311+
312+ if (_initDone)
313+ {
314+ // Reloading config
315+ initializeAht ();
316+
317+ #ifndef WLED_DISABLE_MQTT
318+ mqttInitialize ();
319+ #endif
320+ }
321+
322+ _initDone = true ;
323+ return configComplete;
324+ }
325+ };
326+
327+ const char UsermodAHT10::_name[] PROGMEM = " AHTxx" ;
0 commit comments