#pragma once #include "wled.h" #include "OneWire.h" //Pin defaults for QuinLed Dig-Uno if not overriden #ifndef TEMPERATURE_PIN #ifdef ARDUINO_ARCH_ESP32 #define TEMPERATURE_PIN 18 #else //ESP8266 boards #define TEMPERATURE_PIN 14 #endif #endif // the frequency to check temperature, 1 minute #ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL #define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 #endif class UsermodTemperature : public Usermod { private: bool initDone = false; OneWire *oneWire; // GPIO pin used for sensor (with a default compile-time fallback) int8_t temperaturePin = TEMPERATURE_PIN; // measurement unit (true==°C, false==°F) bool degC = true; // using parasite power on the sensor bool parasite = false; int8_t parasitePin = -1; // how often do we read from sensor? unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; // set last reading as "40 sec before boot", so first reading is taken after 20 sec unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; // last time requestTemperatures was called // used to determine when we can read the sensors temperature // we have to wait at least 93.75 ms after requestTemperatures() is called unsigned long lastTemperaturesRequest; float temperature; // indicates requestTemperatures has been called but the sensor measurement is not complete bool waitingForConversion = false; // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting // temperature if flashed to a board without a sensor attached byte sensorFound; bool enabled = true; bool HApublished = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _readInterval[]; static const char _parasite[]; static const char _parasitePin[]; //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float readDallas(); void requestTemperatures(); void readTemperature(); bool findSensor(); #ifndef WLED_DISABLE_MQTT void publishHomeAssistantAutodiscovery(); #endif public: /* * API calls te enable data exchange between WLED modules */ inline float getTemperatureC() { return temperature; } inline float getTemperatureF() { return temperature * 1.8f + 32.0f; } float getTemperature(); const char *getTemperatureUnit(); uint16_t getId() { return USERMOD_ID_TEMPERATURE; } void setup(); void loop(); //void connected(); #ifndef WLED_DISABLE_MQTT void onMqttConnect(bool sessionPresent); #endif //void onUpdateBegin(bool init); //bool handleButton(uint8_t b); //void handleOverlayDraw(); void addToJsonInfo(JsonObject& root); //void addToJsonState(JsonObject &root); //void readFromJsonState(JsonObject &root); void addToConfig(JsonObject &root); bool readFromConfig(JsonObject &root); void appendConfigData(); }; //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float UsermodTemperature::readDallas() { byte data[9]; int16_t result; // raw data from sensor float retVal = -127.0f; if (oneWire->reset()) { // if reset() fails there are no OneWire devices oneWire->skip(); // skip ROM oneWire->write(0xBE); // read (temperature) from EEPROM oneWire->read_bytes(data, 9); // first 2 bytes contain temperature #ifdef WLED_DEBUG if (OneWire::crc8(data,8) != data[8]) { DEBUG_PRINTLN(F("CRC error reading temperature.")); for (byte i=0; i < 9; i++) DEBUG_PRINTF("0x%02X ", data[i]); DEBUG_PRINT(F(" => ")); DEBUG_PRINTF("0x%02X\n", OneWire::crc8(data,8)); } #endif switch(sensorFound) { case 0x10: // DS18S20 has 9-bit precision result = (data[1] << 8) | data[0]; retVal = float(result) * 0.5f; break; case 0x22: // DS18B20 case 0x28: // DS1822 case 0x3B: // DS1825 case 0x42: // DS28EA00 result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning if (data[1] & 0x80) result |= 0xF000; // fix negative value retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f); break; } } for (byte i=1; i<9; i++) data[0] &= data[i]; return data[0]==0xFF ? -127.0f : retVal; } void UsermodTemperature::requestTemperatures() { DEBUG_PRINTLN(F("Requesting temperature.")); oneWire->reset(); oneWire->skip(); // skip ROM oneWire->write(0x44,parasite); // request new temperature reading if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, HIGH); // has to happen within 10us (open MOSFET) lastTemperaturesRequest = millis(); waitingForConversion = true; } void UsermodTemperature::readTemperature() { if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) temperature = readDallas(); lastMeasurement = millis(); waitingForConversion = false; //DEBUG_PRINTF("Read temperature %2.1f.\n", temperature); // does not work properly on 8266 DEBUG_PRINT(F("Read temperature ")); DEBUG_PRINTLN(temperature); } bool UsermodTemperature::findSensor() { DEBUG_PRINTLN(F("Searching for sensor...")); uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0}; // find out if we have DS18xxx sensor attached oneWire->reset_search(); delay(10); while (oneWire->search(deviceAddress)) { DEBUG_PRINTLN(F("Found something...")); if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { switch (deviceAddress[0]) { case 0x10: // DS18S20 case 0x22: // DS18B20 case 0x28: // DS1822 case 0x3B: // DS1825 case 0x42: // DS28EA00 DEBUG_PRINTLN(F("Sensor found.")); sensorFound = deviceAddress[0]; DEBUG_PRINTF("0x%02X\n", sensorFound); return true; } } } DEBUG_PRINTLN(F("Sensor NOT found.")); return false; } #ifndef WLED_DISABLE_MQTT void UsermodTemperature::publishHomeAssistantAutodiscovery() { if (!WLED_MQTT_CONNECTED) return; char json_str[1024], buf[128]; size_t payload_size; StaticJsonDocument<1024> json; sprintf_P(buf, PSTR("%s Temperature"), serverDescription); json[F("name")] = buf; strcpy(buf, mqttDeviceTopic); strcat_P(buf, PSTR("/temperature")); json[F("state_topic")] = buf; json[F("device_class")] = F("temperature"); json[F("unique_id")] = escapedMac.c_str(); json[F("unit_of_measurement")] = F("°C"); payload_size = serializeJson(json, json_str); sprintf_P(buf, PSTR("homeassistant/sensor/%s/config"), escapedMac.c_str()); mqtt->publish(buf, 0, true, json_str, payload_size); HApublished = true; } #endif void UsermodTemperature::setup() { int retries = 10; sensorFound = 0; temperature = -127.0f; // default to -127, DS18B20 only goes down to -50C if (enabled) { // config says we are enabled DEBUG_PRINTLN(F("Allocating temperature pin...")); // pin retrieved from cfg.json (readFromConfig()) prior to running setup() if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { oneWire = new OneWire(temperaturePin); if (oneWire->reset()) { while (!findSensor() && retries--) { delay(25); // try to find sensor } } if (parasite && pinManager.allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { pinMode(parasitePin, OUTPUT); digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) } else { parasitePin = -1; } } else { if (temperaturePin >= 0) { DEBUG_PRINTLN(F("Temperature pin allocation failed.")); } temperaturePin = -1; // allocation failed } } lastMeasurement = millis() - readingInterval + 10000; initDone = true; } void UsermodTemperature::loop() { if (!enabled || !sensorFound || strip.isUpdating()) return; static uint8_t errorCount = 0; unsigned long now = millis(); // check to see if we are due for taking a measurement // lastMeasurement will not be updated until the conversion // is complete the the reading is finished if (now - lastMeasurement < readingInterval) return; // we are due for a measurement, if we are not already waiting // for a conversion to complete, then make a new request for temps if (!waitingForConversion) { requestTemperatures(); return; } // we were waiting for a conversion to complete, have we waited log enough? if (now - lastTemperaturesRequest >= 750 /* 93.75ms per the datasheet but can be up to 750ms */) { readTemperature(); if (getTemperatureC() < -100.0f) { if (++errorCount > 10) sensorFound = 0; lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms return; } errorCount = 0; #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { char subuf[64]; strcpy(subuf, mqttDeviceTopic); if (temperature > -100.0f) { // dont publish super low temperature as the graph will get messed up // the DallasTemperature library returns -127C or -196.6F when problem // reading the sensor strcat_P(subuf, PSTR("/temperature")); mqtt->publish(subuf, 0, false, String(getTemperatureC()).c_str()); strcat_P(subuf, PSTR("_f")); mqtt->publish(subuf, 0, false, String(getTemperatureF()).c_str()); } else { // publish something else to indicate status? } } #endif } } /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ //void UsermodTemperature::connected() {} #ifndef WLED_DISABLE_MQTT /** * subscribe to MQTT topic if needed */ void UsermodTemperature::onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics //char subuf[64]; if (mqttDeviceTopic[0] != 0) { publishHomeAssistantAutodiscovery(); } } #endif /* * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor */ void UsermodTemperature::addToJsonInfo(JsonObject& root) { // dont add temperature to info if we are disabled if (!enabled) return; JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray temp = user.createNestedArray(FPSTR(_name)); if (temperature <= -100.0f) { temp.add(0); temp.add(F(" Sensor Error!")); return; } temp.add(getTemperature()); temp.add(getTemperatureUnit()); JsonObject sensor = root[F("sensor")]; if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); temp = sensor.createNestedArray(F("temperature")); temp.add(getTemperature()); temp.add(getTemperatureUnit()); } /** * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ //void UsermodTemperature::addToJsonState(JsonObject &root) //{ //} /** * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients * Read "_" from json state and and change settings (i.e. GPIO pin) used. */ //void UsermodTemperature::readFromJsonState(JsonObject &root) { // if (!initDone) return; // prevent crash on boot applyPreset() //} /** * addToConfig() (called from set.cpp) stores persistent properties to cfg.json */ void UsermodTemperature::addToConfig(JsonObject &root) { // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top["pin"] = temperaturePin; // usermodparam top["degC"] = degC; // usermodparam top[FPSTR(_readInterval)] = readingInterval / 1000; top[FPSTR(_parasite)] = parasite; top[FPSTR(_parasitePin)] = parasitePin; DEBUG_PRINTLN(F("Temperature config saved.")); } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool UsermodTemperature::readFromConfig(JsonObject &root) { // we look for JSON object: {"Temperature": {"pin": 0, "degC": true}} int8_t newTemperaturePin = temperaturePin; DEBUG_PRINT(FPSTR(_name)); JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } enabled = top[FPSTR(_enabled)] | enabled; newTemperaturePin = top["pin"] | newTemperaturePin; degC = top["degC"] | degC; readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; readingInterval = min(120,max(10,(int)readingInterval)) * 1000; // convert to ms parasite = top[FPSTR(_parasite)] | parasite; parasitePin = top[FPSTR(_parasitePin)] | parasitePin; if (!initDone) { // first run: reading from cfg.json temperaturePin = newTemperaturePin; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing paramters from settings page if (newTemperaturePin != temperaturePin) { DEBUG_PRINTLN(F("Re-init temperature.")); // deallocate pin and release memory delete oneWire; pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); temperaturePin = newTemperaturePin; pinManager.deallocatePin(parasitePin, PinOwner::UM_Temperature); // initialise setup(); } } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_parasitePin)].isNull(); } void UsermodTemperature::appendConfigData() { oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); oappend(SET_F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { return degC ? getTemperatureC() : getTemperatureF(); } const char *UsermodTemperature::getTemperatureUnit() { return degC ? "°C" : "°F"; } // strings to reduce flash memory usage (used more than twice) const char UsermodTemperature::_name[] PROGMEM = "Temperature"; const char UsermodTemperature::_enabled[] PROGMEM = "enabled"; const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr"; const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin";